@oxyhq/services 22.10.0 → 22.11.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/AvatarCameraBadge.js +42 -0
- package/lib/commonjs/ui/components/AvatarCameraBadge.js.map +1 -0
- package/lib/commonjs/ui/components/OxyAccountDialogScreen.js +41 -19
- package/lib/commonjs/ui/components/OxyAccountDialogScreen.js.map +1 -1
- package/lib/commonjs/ui/components/OxyAuthChooser.js +640 -168
- package/lib/commonjs/ui/components/OxyAuthChooser.js.map +1 -1
- package/lib/commonjs/ui/components/ProfileButton.js +14 -0
- package/lib/commonjs/ui/components/ProfileButton.js.map +1 -1
- package/lib/commonjs/ui/components/ProfileSummaryCard.js +3 -23
- package/lib/commonjs/ui/components/ProfileSummaryCard.js.map +1 -1
- package/lib/commonjs/ui/components/SurfaceScreen.js +41 -5
- package/lib/commonjs/ui/components/SurfaceScreen.js.map +1 -1
- package/lib/commonjs/ui/components/oxyAuthChooserStyles.js +78 -26
- package/lib/commonjs/ui/components/oxyAuthChooserStyles.js.map +1 -1
- package/lib/commonjs/ui/components/photogrid/JustifiedPhotoGrid.js +83 -23
- package/lib/commonjs/ui/components/photogrid/JustifiedPhotoGrid.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +73 -37
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/queryKeys.js +18 -1
- package/lib/commonjs/ui/hooks/queries/queryKeys.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/useFileQueries.js +129 -0
- package/lib/commonjs/ui/hooks/queries/useFileQueries.js.map +1 -0
- package/lib/commonjs/ui/hooks/useAvatarPicker.js +36 -48
- package/lib/commonjs/ui/hooks/useAvatarPicker.js.map +1 -1
- package/lib/commonjs/ui/hooks/useReduceMotion.js +57 -0
- package/lib/commonjs/ui/hooks/useReduceMotion.js.map +1 -0
- package/lib/commonjs/ui/hooks/useSurfaceHeader.js +1 -1
- package/lib/commonjs/ui/hooks/useSurfaceHeader.js.map +1 -1
- package/lib/commonjs/ui/navigation/accountDialogManager.js +22 -0
- package/lib/commonjs/ui/navigation/accountDialogManager.js.map +1 -1
- package/lib/commonjs/ui/navigation/bottomSheetManager.js +5 -6
- package/lib/commonjs/ui/navigation/bottomSheetManager.js.map +1 -1
- package/lib/commonjs/ui/navigation/routes.js +1 -0
- package/lib/commonjs/ui/navigation/routes.js.map +1 -1
- package/lib/commonjs/ui/navigation/surfaceNavStack.js +62 -15
- package/lib/commonjs/ui/navigation/surfaceNavStack.js.map +1 -1
- package/lib/commonjs/ui/navigation/surfaceRegistry.js +38 -17
- package/lib/commonjs/ui/navigation/surfaceRegistry.js.map +1 -1
- package/lib/commonjs/ui/navigation/surfaces.js +98 -21
- package/lib/commonjs/ui/navigation/surfaces.js.map +1 -1
- package/lib/commonjs/ui/screens/AvatarCropScreen.js +281 -279
- package/lib/commonjs/ui/screens/AvatarCropScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/ChangeAvatarScreen.js +301 -0
- package/lib/commonjs/ui/screens/ChangeAvatarScreen.js.map +1 -0
- package/lib/commonjs/ui/screens/FileManagementScreen.js +194 -431
- package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js +16 -48
- package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/fileManagement/FileLibraryError.js +63 -0
- package/lib/commonjs/ui/screens/fileManagement/FileLibraryError.js.map +1 -0
- package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js +54 -41
- package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
- package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js +44 -81
- package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js.map +1 -1
- package/lib/commonjs/ui/stores/resetSessionScopedStores.js +8 -3
- package/lib/commonjs/ui/stores/resetSessionScopedStores.js.map +1 -1
- package/lib/commonjs/ui/utils/avatarUtils.js +11 -2
- package/lib/commonjs/ui/utils/avatarUtils.js.map +1 -1
- package/lib/commonjs/ui/utils/fileManagement.js +0 -79
- package/lib/commonjs/ui/utils/fileManagement.js.map +1 -1
- package/lib/module/ui/components/AvatarCameraBadge.js +40 -0
- package/lib/module/ui/components/AvatarCameraBadge.js.map +1 -0
- package/lib/module/ui/components/OxyAccountDialogScreen.js +41 -19
- package/lib/module/ui/components/OxyAccountDialogScreen.js.map +1 -1
- package/lib/module/ui/components/OxyAuthChooser.js +642 -171
- package/lib/module/ui/components/OxyAuthChooser.js.map +1 -1
- package/lib/module/ui/components/ProfileButton.js +15 -1
- package/lib/module/ui/components/ProfileButton.js.map +1 -1
- package/lib/module/ui/components/ProfileSummaryCard.js +2 -23
- package/lib/module/ui/components/ProfileSummaryCard.js.map +1 -1
- package/lib/module/ui/components/SurfaceScreen.js +43 -7
- package/lib/module/ui/components/SurfaceScreen.js.map +1 -1
- package/lib/module/ui/components/oxyAuthChooserStyles.js +78 -26
- package/lib/module/ui/components/oxyAuthChooserStyles.js.map +1 -1
- package/lib/module/ui/components/photogrid/JustifiedPhotoGrid.js +83 -25
- package/lib/module/ui/components/photogrid/JustifiedPhotoGrid.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +74 -38
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/hooks/queries/queryKeys.js +16 -0
- package/lib/module/ui/hooks/queries/queryKeys.js.map +1 -1
- package/lib/module/ui/hooks/queries/useFileQueries.js +120 -0
- package/lib/module/ui/hooks/queries/useFileQueries.js.map +1 -0
- package/lib/module/ui/hooks/useAvatarPicker.js +37 -49
- package/lib/module/ui/hooks/useAvatarPicker.js.map +1 -1
- package/lib/module/ui/hooks/useReduceMotion.js +53 -0
- package/lib/module/ui/hooks/useReduceMotion.js.map +1 -0
- package/lib/module/ui/hooks/useSurfaceHeader.js +1 -1
- package/lib/module/ui/hooks/useSurfaceHeader.js.map +1 -1
- package/lib/module/ui/navigation/accountDialogManager.js +20 -0
- package/lib/module/ui/navigation/accountDialogManager.js.map +1 -1
- package/lib/module/ui/navigation/bottomSheetManager.js +6 -7
- package/lib/module/ui/navigation/bottomSheetManager.js.map +1 -1
- package/lib/module/ui/navigation/routes.js +1 -0
- package/lib/module/ui/navigation/routes.js.map +1 -1
- package/lib/module/ui/navigation/surfaceNavStack.js +62 -15
- package/lib/module/ui/navigation/surfaceNavStack.js.map +1 -1
- package/lib/module/ui/navigation/surfaceRegistry.js +38 -17
- package/lib/module/ui/navigation/surfaceRegistry.js.map +1 -1
- package/lib/module/ui/navigation/surfaces.js +97 -22
- package/lib/module/ui/navigation/surfaces.js.map +1 -1
- package/lib/module/ui/screens/AvatarCropScreen.js +283 -281
- package/lib/module/ui/screens/AvatarCropScreen.js.map +1 -1
- package/lib/module/ui/screens/ChangeAvatarScreen.js +298 -0
- package/lib/module/ui/screens/ChangeAvatarScreen.js.map +1 -0
- package/lib/module/ui/screens/FileManagementScreen.js +196 -433
- package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/module/ui/screens/WelcomeNewUserScreen.js +17 -49
- package/lib/module/ui/screens/WelcomeNewUserScreen.js.map +1 -1
- package/lib/module/ui/screens/fileManagement/FileLibraryError.js +59 -0
- package/lib/module/ui/screens/fileManagement/FileLibraryError.js.map +1 -0
- package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js +54 -42
- package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
- package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js +45 -81
- package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js.map +1 -1
- package/lib/module/ui/stores/resetSessionScopedStores.js +8 -3
- package/lib/module/ui/stores/resetSessionScopedStores.js.map +1 -1
- package/lib/module/ui/utils/avatarUtils.js +11 -2
- package/lib/module/ui/utils/avatarUtils.js.map +1 -1
- package/lib/module/ui/utils/fileManagement.js +0 -78
- package/lib/module/ui/utils/fileManagement.js.map +1 -1
- package/lib/typescript/commonjs/ui/components/AvatarCameraBadge.d.ts +4 -0
- package/lib/typescript/commonjs/ui/components/AvatarCameraBadge.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/OxyAccountDialogScreen.d.ts +2 -1
- package/lib/typescript/commonjs/ui/components/OxyAccountDialogScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ProfileButton.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ProfileSummaryCard.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/SurfaceScreen.d.ts +1 -4
- package/lib/typescript/commonjs/ui/components/SurfaceScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/oxyAuthChooserStyles.d.ts +71 -19
- package/lib/typescript/commonjs/ui/components/oxyAuthChooserStyles.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/photogrid/JustifiedPhotoGrid.d.ts +10 -8
- package/lib/typescript/commonjs/ui/components/photogrid/JustifiedPhotoGrid.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/queries/queryKeys.d.ts +9 -0
- package/lib/typescript/commonjs/ui/hooks/queries/queryKeys.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/queries/useFileQueries.d.ts +50 -0
- package/lib/typescript/commonjs/ui/hooks/queries/useFileQueries.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/hooks/useAvatarPicker.d.ts +15 -10
- package/lib/typescript/commonjs/ui/hooks/useAvatarPicker.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useReduceMotion.d.ts +14 -0
- package/lib/typescript/commonjs/ui/hooks/useReduceMotion.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/hooks/useSurfaceHeader.d.ts +1 -1
- package/lib/typescript/commonjs/ui/hooks/useSurfaceHeader.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/navigation/accountDialogManager.d.ts +13 -0
- package/lib/typescript/commonjs/ui/navigation/accountDialogManager.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/navigation/bottomSheetManager.d.ts +4 -5
- package/lib/typescript/commonjs/ui/navigation/bottomSheetManager.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/navigation/routes.d.ts +1 -1
- package/lib/typescript/commonjs/ui/navigation/routes.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/navigation/surfaceNavStack.d.ts +21 -0
- package/lib/typescript/commonjs/ui/navigation/surfaceNavStack.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/navigation/surfaceRegistry.d.ts +73 -13
- package/lib/typescript/commonjs/ui/navigation/surfaceRegistry.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/navigation/surfaces.d.ts +42 -11
- package/lib/typescript/commonjs/ui/navigation/surfaces.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/AvatarCropScreen.d.ts +18 -5
- package/lib/typescript/commonjs/ui/screens/AvatarCropScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/ChangeAvatarScreen.d.ts +34 -0
- package/lib/typescript/commonjs/ui/screens/ChangeAvatarScreen.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/WelcomeNewUserScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/FileLibraryError.d.ts +23 -0
- package/lib/typescript/commonjs/ui/screens/fileManagement/FileLibraryError.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts +4 -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 +10 -2
- package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/stores/resetSessionScopedStores.d.ts +8 -1
- package/lib/typescript/commonjs/ui/stores/resetSessionScopedStores.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/types/navigation.d.ts +2 -0
- package/lib/typescript/commonjs/ui/types/navigation.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/avatarUtils.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/fileManagement.d.ts +0 -48
- package/lib/typescript/commonjs/ui/utils/fileManagement.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/AvatarCameraBadge.d.ts +4 -0
- package/lib/typescript/module/ui/components/AvatarCameraBadge.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/OxyAccountDialogScreen.d.ts +2 -1
- package/lib/typescript/module/ui/components/OxyAccountDialogScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ProfileButton.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ProfileSummaryCard.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/SurfaceScreen.d.ts +1 -4
- package/lib/typescript/module/ui/components/SurfaceScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/oxyAuthChooserStyles.d.ts +71 -19
- package/lib/typescript/module/ui/components/oxyAuthChooserStyles.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/photogrid/JustifiedPhotoGrid.d.ts +10 -8
- package/lib/typescript/module/ui/components/photogrid/JustifiedPhotoGrid.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/queries/queryKeys.d.ts +9 -0
- package/lib/typescript/module/ui/hooks/queries/queryKeys.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/queries/useFileQueries.d.ts +50 -0
- package/lib/typescript/module/ui/hooks/queries/useFileQueries.d.ts.map +1 -0
- package/lib/typescript/module/ui/hooks/useAvatarPicker.d.ts +15 -10
- package/lib/typescript/module/ui/hooks/useAvatarPicker.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useReduceMotion.d.ts +14 -0
- package/lib/typescript/module/ui/hooks/useReduceMotion.d.ts.map +1 -0
- package/lib/typescript/module/ui/hooks/useSurfaceHeader.d.ts +1 -1
- package/lib/typescript/module/ui/hooks/useSurfaceHeader.d.ts.map +1 -1
- package/lib/typescript/module/ui/navigation/accountDialogManager.d.ts +13 -0
- package/lib/typescript/module/ui/navigation/accountDialogManager.d.ts.map +1 -1
- package/lib/typescript/module/ui/navigation/bottomSheetManager.d.ts +4 -5
- package/lib/typescript/module/ui/navigation/bottomSheetManager.d.ts.map +1 -1
- package/lib/typescript/module/ui/navigation/routes.d.ts +1 -1
- package/lib/typescript/module/ui/navigation/routes.d.ts.map +1 -1
- package/lib/typescript/module/ui/navigation/surfaceNavStack.d.ts +21 -0
- package/lib/typescript/module/ui/navigation/surfaceNavStack.d.ts.map +1 -1
- package/lib/typescript/module/ui/navigation/surfaceRegistry.d.ts +73 -13
- package/lib/typescript/module/ui/navigation/surfaceRegistry.d.ts.map +1 -1
- package/lib/typescript/module/ui/navigation/surfaces.d.ts +42 -11
- package/lib/typescript/module/ui/navigation/surfaces.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/AvatarCropScreen.d.ts +18 -5
- package/lib/typescript/module/ui/screens/AvatarCropScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/ChangeAvatarScreen.d.ts +34 -0
- package/lib/typescript/module/ui/screens/ChangeAvatarScreen.d.ts.map +1 -0
- package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/WelcomeNewUserScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/FileLibraryError.d.ts +23 -0
- package/lib/typescript/module/ui/screens/fileManagement/FileLibraryError.d.ts.map +1 -0
- package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts +4 -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 +10 -2
- package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
- package/lib/typescript/module/ui/stores/resetSessionScopedStores.d.ts +8 -1
- package/lib/typescript/module/ui/stores/resetSessionScopedStores.d.ts.map +1 -1
- package/lib/typescript/module/ui/types/navigation.d.ts +2 -0
- package/lib/typescript/module/ui/types/navigation.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/avatarUtils.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/fileManagement.d.ts +0 -48
- package/lib/typescript/module/ui/utils/fileManagement.d.ts.map +1 -1
- package/package.json +8 -4
- package/src/ui/components/AvatarCameraBadge.tsx +35 -0
- package/src/ui/components/OxyAccountDialogScreen.tsx +38 -15
- package/src/ui/components/OxyAuthChooser.tsx +722 -185
- package/src/ui/components/ProfileButton.tsx +16 -1
- package/src/ui/components/ProfileSummaryCard.tsx +2 -25
- package/src/ui/components/SurfaceScreen.tsx +48 -14
- package/src/ui/components/oxyAuthChooserStyles.ts +75 -23
- package/src/ui/components/photogrid/JustifiedPhotoGrid.tsx +158 -84
- package/src/ui/components/photogrid/__tests__/chunkPhotos.test.ts +51 -0
- package/src/ui/context/OxyContext.tsx +74 -34
- package/src/ui/hooks/queries/queryKeys.ts +15 -0
- package/src/ui/hooks/queries/useFileQueries.ts +162 -0
- package/src/ui/hooks/useAvatarPicker.ts +38 -53
- package/src/ui/hooks/useReduceMotion.ts +58 -0
- package/src/ui/hooks/useSurfaceHeader.ts +2 -1
- package/src/ui/navigation/__tests__/getSurfaceConfig.test.ts +55 -7
- package/src/ui/navigation/__tests__/surfaceNavStack.test.ts +81 -0
- package/src/ui/navigation/accountDialogManager.ts +28 -0
- package/src/ui/navigation/bottomSheetManager.ts +6 -7
- package/src/ui/navigation/routes.ts +3 -1
- package/src/ui/navigation/surfaceNavStack.ts +72 -9
- package/src/ui/navigation/surfaceRegistry.ts +91 -17
- package/src/ui/navigation/surfaces.ts +108 -29
- package/src/ui/screens/AvatarCropScreen.tsx +336 -298
- package/src/ui/screens/ChangeAvatarScreen.tsx +359 -0
- package/src/ui/screens/FileManagementScreen.tsx +189 -432
- package/src/ui/screens/WelcomeNewUserScreen.tsx +18 -48
- package/src/ui/screens/fileManagement/FileLibraryError.tsx +56 -0
- package/src/ui/screens/fileManagement/PhotoPickerSection.tsx +56 -46
- package/src/ui/screens/fileManagement/hooks/useFileUploadState.ts +55 -71
- package/src/ui/stores/resetSessionScopedStores.ts +8 -3
- package/src/ui/types/navigation.ts +2 -0
- package/src/ui/utils/avatarUtils.ts +11 -2
- package/src/ui/utils/fileManagement.ts +1 -101
- package/lib/commonjs/ui/stores/fileStore.js +0 -153
- package/lib/commonjs/ui/stores/fileStore.js.map +0 -1
- package/lib/module/ui/stores/fileStore.js +0 -145
- package/lib/module/ui/stores/fileStore.js.map +0 -1
- package/lib/typescript/commonjs/ui/stores/fileStore.d.ts +0 -31
- package/lib/typescript/commonjs/ui/stores/fileStore.d.ts.map +0 -1
- package/lib/typescript/module/ui/stores/fileStore.d.ts +0 -31
- package/lib/typescript/module/ui/stores/fileStore.d.ts.map +0 -1
- package/src/ui/stores/fileStore.ts +0 -118
|
@@ -45,7 +45,12 @@ 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 {
|
|
48
|
+
import {
|
|
49
|
+
presentDetached,
|
|
50
|
+
topRouteSurface,
|
|
51
|
+
type SurfaceInstance,
|
|
52
|
+
type TrackedSurface,
|
|
53
|
+
} from '../navigation/surfaces';
|
|
49
54
|
import { useQueryClient, onlineManager } from '@tanstack/react-query';
|
|
50
55
|
import { clearQueryCache } from '../hooks/queryClient';
|
|
51
56
|
import { useAvatarPicker } from '../hooks/useAvatarPicker';
|
|
@@ -652,6 +657,28 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
652
657
|
// surface settling (`result.finally`) flips it false — no other write site.
|
|
653
658
|
const accountDialogSurfaceRef = useRef<SurfaceInstance<'AccountDialog'> | null>(null);
|
|
654
659
|
|
|
660
|
+
// When the account dialog is shown by MORPHING an already-open route surface
|
|
661
|
+
// (e.g. ManageAccount → the account switcher) it is pushed as a frame ONTO that
|
|
662
|
+
// surface rather than presented as its own. This holds that host surface so the
|
|
663
|
+
// close path can pop the AccountDialog frame back to it. `null` when the dialog
|
|
664
|
+
// is its OWN detached surface (opened cold, with no surface underneath).
|
|
665
|
+
const accountDialogHostRef = useRef<TrackedSurface | null>(null);
|
|
666
|
+
|
|
667
|
+
// The single teardown for the account dialog wherever it lives: pop the morphed
|
|
668
|
+
// frame back to its host surface (reshaping Manage ← switcher), or dismiss the
|
|
669
|
+
// detached surface (whose settle then flips `accountDialogOpen`). Reads only
|
|
670
|
+
// stable refs + `setAccountDialogOpen`, so its identity never changes.
|
|
671
|
+
const dismissAccountDialogSurface = useCallback((): void => {
|
|
672
|
+
const host = accountDialogHostRef.current;
|
|
673
|
+
if (host) {
|
|
674
|
+
accountDialogHostRef.current = null;
|
|
675
|
+
setAccountDialogOpen(false);
|
|
676
|
+
host.goBack();
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
accountDialogSurfaceRef.current?.dismiss();
|
|
680
|
+
}, []);
|
|
681
|
+
|
|
655
682
|
const accountDialogControllerRef = useRef<AccountDialogController | null>(null);
|
|
656
683
|
if (!accountDialogControllerRef.current) {
|
|
657
684
|
accountDialogControllerRef.current = createAccountDialogController({
|
|
@@ -665,11 +692,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
665
692
|
commitSession: (session) => handleWebSessionRef.current(session),
|
|
666
693
|
commitSwitchedSession: (session) => commitSwitchedSessionRef.current(session),
|
|
667
694
|
onSignedIn: () => {
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
|
|
671
|
-
// flip here.
|
|
672
|
-
accountDialogSurfaceRef.current?.dismiss();
|
|
695
|
+
// Close the dialog: pop the morphed frame back to its host surface, or
|
|
696
|
+
// dismiss the detached surface (whose settle flips `accountDialogOpen`).
|
|
697
|
+
dismissAccountDialogSurface();
|
|
673
698
|
},
|
|
674
699
|
openUrl: (url) => {
|
|
675
700
|
if (isWebBrowser()) {
|
|
@@ -694,40 +719,55 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
694
719
|
const accountDialogController = accountDialogControllerRef.current;
|
|
695
720
|
|
|
696
721
|
const openAccountDialog = useCallback((view?: AccountDialogView): void => {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
//
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
if (!accountDialogSurfaceRef.current) {
|
|
705
|
-
const instance = presentDetached(
|
|
706
|
-
'AccountDialog',
|
|
707
|
-
{ initialView: view ?? 'accounts' },
|
|
708
|
-
{ placement: { base: 'bottom', md: 'center' }, dismissOnBackdrop: false, maxWidth: 420 },
|
|
709
|
-
);
|
|
710
|
-
accountDialogSurfaceRef.current = instance;
|
|
711
|
-
// The surface settling is the ONE place `accountDialogOpen` flips false —
|
|
712
|
-
// covering every dismiss path (close button, `onSignedIn`, programmatic
|
|
713
|
-
// `closeAccountDialog`, host unmount). So the stack owns the open state.
|
|
714
|
-
instance.result.finally(() => {
|
|
715
|
-
if (accountDialogSurfaceRef.current === instance) {
|
|
716
|
-
accountDialogSurfaceRef.current = null;
|
|
717
|
-
setAccountDialogOpen(false);
|
|
718
|
-
}
|
|
719
|
-
});
|
|
722
|
+
const nextView = view ?? 'accounts';
|
|
723
|
+
accountDialogControllerRef.current?.setView(nextView);
|
|
724
|
+
|
|
725
|
+
// Its own detached surface is already open → just re-point the view above.
|
|
726
|
+
if (accountDialogSurfaceRef.current) {
|
|
727
|
+
setAccountDialogOpen(true);
|
|
728
|
+
return;
|
|
720
729
|
}
|
|
730
|
+
|
|
731
|
+
// When a route surface (e.g. ManageAccount) is already open, MORPH it in
|
|
732
|
+
// place: push the AccountDialog as a frame ONTO that surface, so the panel
|
|
733
|
+
// reshapes from that surface to the switcher and a back returns to it. With no
|
|
734
|
+
// surface open (opened cold from a sign-in button), present the dialog as its
|
|
735
|
+
// OWN detached surface — kept OUT of the `showBottomSheet` route-surface
|
|
736
|
+
// lineage so closing the bottom-sheet session never touches it and vice-versa.
|
|
737
|
+
// `navigate` is idempotent for the top frame (it replaces rather than
|
|
738
|
+
// duplicates), so calling this while already morphed in just re-points the
|
|
739
|
+
// view, and calling it after a back re-pushes the frame — no stale guard.
|
|
740
|
+
const host = topRouteSurface();
|
|
741
|
+
if (host) {
|
|
742
|
+
accountDialogHostRef.current = host;
|
|
743
|
+
host.navigate('AccountDialog', { initialView: nextView });
|
|
744
|
+
setAccountDialogOpen(true);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const instance = presentDetached(
|
|
749
|
+
'AccountDialog',
|
|
750
|
+
{ initialView: nextView },
|
|
751
|
+
{ placement: { base: 'bottom', md: 'center' }, dismissOnBackdrop: false, maxWidth: 420 },
|
|
752
|
+
);
|
|
753
|
+
accountDialogSurfaceRef.current = instance;
|
|
754
|
+
// The surface settling is the ONE place `accountDialogOpen` flips false for
|
|
755
|
+
// the detached path — covering every dismiss (close button, `onSignedIn`,
|
|
756
|
+
// programmatic `closeAccountDialog`, host unmount). The morphed path clears
|
|
757
|
+
// its own state in `dismissAccountDialogSurface`.
|
|
758
|
+
instance.result.finally(() => {
|
|
759
|
+
if (accountDialogSurfaceRef.current === instance) {
|
|
760
|
+
accountDialogSurfaceRef.current = null;
|
|
761
|
+
setAccountDialogOpen(false);
|
|
762
|
+
}
|
|
763
|
+
});
|
|
721
764
|
setAccountDialogOpen(true);
|
|
722
765
|
}, []);
|
|
723
766
|
|
|
724
767
|
const closeAccountDialog = useCallback((): void => {
|
|
725
768
|
accountDialogControllerRef.current?.cancelSignIn();
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
// the single source of truth, so a manual write would be a second authority.
|
|
729
|
-
accountDialogSurfaceRef.current?.dismiss();
|
|
730
|
-
}, []);
|
|
769
|
+
dismissAccountDialogSurface();
|
|
770
|
+
}, [dismissAccountDialogSurface]);
|
|
731
771
|
|
|
732
772
|
// Start driving the dialog on mount; tear it down on unmount.
|
|
733
773
|
useEffect(() => {
|
|
@@ -100,6 +100,13 @@ export const queryKeys = {
|
|
|
100
100
|
usage: (accountScope?: string) => [...queryKeys.storage.all, 'usage', accountScope] as const,
|
|
101
101
|
},
|
|
102
102
|
|
|
103
|
+
// User file library (infinite-paginated; owner-scoped so switching the active
|
|
104
|
+
// account shows that account's files without a manual reset)
|
|
105
|
+
files: {
|
|
106
|
+
all: ['files'] as const,
|
|
107
|
+
list: (ownerId?: string) => [...queryKeys.files.all, 'list', ownerId || 'anonymous'] as const,
|
|
108
|
+
},
|
|
109
|
+
|
|
103
110
|
// Connected apps (OAuth grants the user has authorized)
|
|
104
111
|
connectedApps: {
|
|
105
112
|
all: ['connectedApps'] as const,
|
|
@@ -204,3 +211,11 @@ export const invalidateConnectedAppsQueries = (queryClient: QueryClient): void =
|
|
|
204
211
|
queryClient.invalidateQueries({ queryKey: queryKeys.connectedApps.all });
|
|
205
212
|
};
|
|
206
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Helper to invalidate the user's file library (all owners). Pass an ownerId via
|
|
216
|
+
* `queryKeys.files.list(ownerId)` for a scoped invalidation instead.
|
|
217
|
+
*/
|
|
218
|
+
export const invalidateFileQueries = (queryClient: QueryClient): void => {
|
|
219
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.files.all });
|
|
220
|
+
};
|
|
221
|
+
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User file-library queries + cache helpers.
|
|
3
|
+
*
|
|
4
|
+
* The file list is owned by React Query (`useInfiniteQuery`), NOT a zustand
|
|
5
|
+
* store: server pages live in the query cache, and every optimistic mutation
|
|
6
|
+
* (upload / delete / visibility) edits that cache via the pure `*InCache`
|
|
7
|
+
* helpers below. The `queryFn` is side-effect-free — it only fetches and maps a
|
|
8
|
+
* page. This is what lets the file manager stay effect-free while preserving
|
|
9
|
+
* infinite scroll, pull-to-refresh, and optimistic upload reconciliation.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
useInfiniteQuery,
|
|
14
|
+
type InfiniteData,
|
|
15
|
+
type QueryClient,
|
|
16
|
+
} from '@tanstack/react-query';
|
|
17
|
+
import type { FileMetadata } from '@oxyhq/core';
|
|
18
|
+
import { useOxy } from '../../context/OxyContext';
|
|
19
|
+
import { queryKeys } from './queryKeys';
|
|
20
|
+
|
|
21
|
+
/** Files fetched per page. */
|
|
22
|
+
export const FILES_PAGE_SIZE = 40;
|
|
23
|
+
|
|
24
|
+
/** Raw file record as returned by `oxyServices.listUserFiles` / `assetUpload`. */
|
|
25
|
+
export interface RawUserFile {
|
|
26
|
+
id: string;
|
|
27
|
+
originalName?: string;
|
|
28
|
+
sha256?: string;
|
|
29
|
+
mime?: string;
|
|
30
|
+
size?: number;
|
|
31
|
+
createdAt?: string;
|
|
32
|
+
metadata?: Record<string, unknown>;
|
|
33
|
+
variants?: unknown[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** One page of the infinite file list. */
|
|
37
|
+
export interface UserFilesPage {
|
|
38
|
+
files: FileMetadata[];
|
|
39
|
+
total: number;
|
|
40
|
+
hasMore: boolean;
|
|
41
|
+
nextOffset: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type UserFilesInfinite = InfiniteData<UserFilesPage>;
|
|
45
|
+
|
|
46
|
+
/** Normalize a raw API file record into the client `FileMetadata` shape. */
|
|
47
|
+
export function mapRawFileToMetadata(raw: RawUserFile): FileMetadata {
|
|
48
|
+
return {
|
|
49
|
+
id: raw.id,
|
|
50
|
+
filename: raw.originalName ?? raw.sha256 ?? '',
|
|
51
|
+
contentType: raw.mime ?? '',
|
|
52
|
+
length: raw.size ?? 0,
|
|
53
|
+
chunkSize: 0,
|
|
54
|
+
uploadDate: raw.createdAt ?? '',
|
|
55
|
+
metadata: raw.metadata ?? {},
|
|
56
|
+
variants: (raw.variants ?? []) as FileMetadata['variants'],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Infinite-paginated list of the owner's files. Fires on mount and whenever
|
|
62
|
+
* `ownerId` changes (a new key); `staleTime: 0` reloads on every open. The
|
|
63
|
+
* rendered list is `data.pages.flatMap(p => p.files)`.
|
|
64
|
+
*/
|
|
65
|
+
export const useUserFilesInfinite = (ownerId?: string) => {
|
|
66
|
+
const { oxyServices } = useOxy();
|
|
67
|
+
return useInfiniteQuery<UserFilesPage, Error>({
|
|
68
|
+
queryKey: queryKeys.files.list(ownerId),
|
|
69
|
+
queryFn: async ({ pageParam }) => {
|
|
70
|
+
const offset = typeof pageParam === 'number' ? pageParam : 0;
|
|
71
|
+
const response = await oxyServices.listUserFiles(FILES_PAGE_SIZE, offset);
|
|
72
|
+
const raw = (response.files ?? []) as RawUserFile[];
|
|
73
|
+
const files = raw.map(mapRawFileToMetadata);
|
|
74
|
+
return {
|
|
75
|
+
files,
|
|
76
|
+
total: response.total ?? offset + files.length,
|
|
77
|
+
hasMore: Boolean(response.hasMore),
|
|
78
|
+
nextOffset: offset + FILES_PAGE_SIZE,
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
initialPageParam: 0,
|
|
82
|
+
getNextPageParam: (lastPage) => (lastPage.hasMore ? lastPage.nextOffset : undefined),
|
|
83
|
+
enabled: Boolean(ownerId),
|
|
84
|
+
staleTime: 0,
|
|
85
|
+
refetchOnWindowFocus: false,
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// --- Pure cache transforms (optimistic updates) -----------------------------
|
|
90
|
+
// Each edits the cached `InfiniteData` for one owner. All no-op when nothing is
|
|
91
|
+
// cached yet (e.g. an upload before the first page loads) — the subsequent
|
|
92
|
+
// `invalidateQueries` reconcile then brings the authoritative record.
|
|
93
|
+
|
|
94
|
+
const fileListKey = (ownerId?: string) => queryKeys.files.list(ownerId);
|
|
95
|
+
|
|
96
|
+
const mapPages = (
|
|
97
|
+
data: UserFilesInfinite,
|
|
98
|
+
fn: (files: FileMetadata[]) => FileMetadata[],
|
|
99
|
+
): UserFilesInfinite => ({
|
|
100
|
+
...data,
|
|
101
|
+
pages: data.pages.map((page) => ({ ...page, files: fn(page.files) })),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
/** Prepend an (optimistic) file to the first page. */
|
|
105
|
+
export function prependFileToCache(
|
|
106
|
+
queryClient: QueryClient,
|
|
107
|
+
ownerId: string | undefined,
|
|
108
|
+
file: FileMetadata,
|
|
109
|
+
): void {
|
|
110
|
+
queryClient.setQueryData<UserFilesInfinite>(fileListKey(ownerId), (data) => {
|
|
111
|
+
if (!data || data.pages.length === 0) return data;
|
|
112
|
+
return {
|
|
113
|
+
...data,
|
|
114
|
+
pages: data.pages.map((page, index) =>
|
|
115
|
+
index === 0 ? { ...page, files: [file, ...page.files] } : page,
|
|
116
|
+
),
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Remove a file (by id) from every page. */
|
|
122
|
+
export function removeFileFromCache(
|
|
123
|
+
queryClient: QueryClient,
|
|
124
|
+
ownerId: string | undefined,
|
|
125
|
+
fileId: string,
|
|
126
|
+
): void {
|
|
127
|
+
queryClient.setQueryData<UserFilesInfinite>(fileListKey(ownerId), (data) =>
|
|
128
|
+
data ? mapPages(data, (files) => files.filter((f) => f.id !== fileId)) : data,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Swap an optimistic file (by id) for its persisted counterpart. */
|
|
133
|
+
export function replaceFileInCache(
|
|
134
|
+
queryClient: QueryClient,
|
|
135
|
+
ownerId: string | undefined,
|
|
136
|
+
oldId: string,
|
|
137
|
+
newFile: FileMetadata,
|
|
138
|
+
): void {
|
|
139
|
+
queryClient.setQueryData<UserFilesInfinite>(fileListKey(ownerId), (data) =>
|
|
140
|
+
data ? mapPages(data, (files) => files.map((f) => (f.id === oldId ? newFile : f))) : data,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Shallow-merge a metadata patch into a cached file (e.g. visibility change). */
|
|
145
|
+
export function patchFileMetadataInCache(
|
|
146
|
+
queryClient: QueryClient,
|
|
147
|
+
ownerId: string | undefined,
|
|
148
|
+
fileId: string,
|
|
149
|
+
metadataPatch: Record<string, unknown>,
|
|
150
|
+
): void {
|
|
151
|
+
queryClient.setQueryData<UserFilesInfinite>(fileListKey(ownerId), (data) =>
|
|
152
|
+
data
|
|
153
|
+
? mapPages(data, (files) =>
|
|
154
|
+
files.map((f) =>
|
|
155
|
+
f.id === fileId
|
|
156
|
+
? { ...f, metadata: { ...f.metadata, ...metadataPatch } as FileMetadata['metadata'] }
|
|
157
|
+
: f,
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
: data,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Avatar Picker Hook
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* The ONE write path for a user's profile picture. It owns no UI: the whole
|
|
5
|
+
* choose-and-crop experience is a single surface.
|
|
5
6
|
*
|
|
6
|
-
* Flow (typed
|
|
7
|
-
* 1. `
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
7
|
+
* Flow (typed promise over the shared surface stack — no callback props):
|
|
8
|
+
* 1. `openWithinOrPresent('ChangeAvatar')` opens the source list (device gallery,
|
|
9
|
+
* camera, existing Oxy files, remove) — MORPHING into the caller's surface
|
|
10
|
+
* when one is open, else presenting cold. Whichever source the user picks, it
|
|
11
|
+
* navigates WITHIN to the crop editor — so there is exactly one cropper and
|
|
12
|
+
* one entry into it.
|
|
13
|
+
* 2. The surface resolves with the cropped JPEG, with `{ removed: true }` when
|
|
14
|
+
* the user removed their photo, or `undefined` when they cancelled.
|
|
15
|
+
* 3. A crop is uploaded as a NEW file via `oxyServices.assetUpload` and set as
|
|
16
|
+
* the avatar; a removal clears the avatar field.
|
|
13
17
|
*
|
|
14
|
-
* `expo-image-manipulator` is required for the crop step
|
|
15
|
-
*
|
|
18
|
+
* `expo-image-manipulator` is required for the crop step and `expo-image-picker`
|
|
19
|
+
* for the device/camera sources (both declared as optional peers in the
|
|
20
|
+
* consuming app) — see AvatarCropScreen / ChangeAvatarScreen.
|
|
16
21
|
*/
|
|
17
22
|
|
|
18
23
|
import { useCallback } from 'react';
|
|
@@ -21,7 +26,7 @@ import { translate, updateAvatarVisibility } from '@oxyhq/core';
|
|
|
21
26
|
import type { QueryClient } from '@tanstack/react-query';
|
|
22
27
|
import { toast } from '@oxyhq/bloom';
|
|
23
28
|
import { updateProfileWithAvatar } from '../utils/avatarUtils';
|
|
24
|
-
import {
|
|
29
|
+
import { openWithinOrPresent } from '../navigation/surfaces';
|
|
25
30
|
import type { AvatarCropResult } from '../screens/AvatarCropScreen';
|
|
26
31
|
|
|
27
32
|
interface UseAvatarPickerOptions {
|
|
@@ -98,56 +103,36 @@ export function useAvatarPicker({
|
|
|
98
103
|
[activeSessionId, currentLanguage, oxyServices, queryClient],
|
|
99
104
|
);
|
|
100
105
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
// the user taps one, or `undefined` if they cancel.
|
|
104
|
-
const file = await surfaces.present('FileManagement', {
|
|
105
|
-
selectMode: true,
|
|
106
|
-
multiSelect: false,
|
|
107
|
-
disabledMimeTypes: ['video/', 'audio/', 'application/pdf'],
|
|
108
|
-
});
|
|
109
|
-
if (!file) return;
|
|
110
|
-
|
|
111
|
-
if (!file.contentType?.startsWith('image/')) {
|
|
112
|
-
toast.error(
|
|
113
|
-
translate(currentLanguage ?? undefined, 'editProfile.toasts.selectImage') ||
|
|
114
|
-
'Please select an image file',
|
|
115
|
-
);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// 2. Resolve a working source URL. The picked file is usually PRIVATE, so
|
|
120
|
-
// the synchronous `getFileDownloadUrl` (public CDN origin) would 404 and
|
|
121
|
-
// the crop screen's `Image.getSize` would fail silently. `assetGetUrl`
|
|
122
|
-
// throws on failure (unlike `getFileDownloadUrlAsync`, which swallows
|
|
123
|
-
// errors and falls back to the broken public URL), so a failure surfaces
|
|
124
|
-
// a real user-facing error instead of a blank crop canvas. No variant is
|
|
125
|
-
// requested — cropping needs the original.
|
|
126
|
-
let sourceUri: string;
|
|
106
|
+
/** Clear the avatar so the profile falls back to the user's initials. */
|
|
107
|
+
const removeAvatar = useCallback(async () => {
|
|
127
108
|
try {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
sourceUri = resolved.url;
|
|
109
|
+
await updateProfileWithAvatar({ avatar: '' }, oxyServices, activeSessionId, queryClient);
|
|
110
|
+
toast.success(
|
|
111
|
+
translate(currentLanguage ?? undefined, 'editProfile.toasts.avatarRemoved'),
|
|
112
|
+
);
|
|
133
113
|
} catch (e: unknown) {
|
|
134
114
|
const message = e instanceof Error ? e.message : undefined;
|
|
135
115
|
toast.error(
|
|
136
116
|
message ||
|
|
137
|
-
translate(currentLanguage ?? undefined, 'editProfile.toasts.
|
|
138
|
-
'Could not load the selected image',
|
|
117
|
+
translate(currentLanguage ?? undefined, 'editProfile.toasts.updateAvatarFailed'),
|
|
139
118
|
);
|
|
140
|
-
return;
|
|
141
119
|
}
|
|
120
|
+
}, [activeSessionId, currentLanguage, oxyServices, queryClient]);
|
|
142
121
|
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
//
|
|
149
|
-
await
|
|
150
|
-
|
|
122
|
+
const openAvatarPicker = useCallback(async () => {
|
|
123
|
+
// The source list + crop editor are ONE flow. When triggered from a screen
|
|
124
|
+
// already inside a surface (EditProfile, ManageAccount, WelcomeNewUser — every
|
|
125
|
+
// current entry point), it MORPHS into that surface and pops back on finish;
|
|
126
|
+
// triggered with no surface open it presents cold. Either way it resolves with
|
|
127
|
+
// the cropped JPEG, a removal, or `undefined` when the user backs out.
|
|
128
|
+
const result = await openWithinOrPresent('ChangeAvatar');
|
|
129
|
+
if (!result) return;
|
|
130
|
+
if ('removed' in result) {
|
|
131
|
+
await removeAvatar();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
await finalizeCroppedAvatar(result);
|
|
135
|
+
}, [finalizeCroppedAvatar, removeAvatar]);
|
|
151
136
|
|
|
152
137
|
return { openAvatarPicker };
|
|
153
138
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useReduceMotion
|
|
3
|
+
*
|
|
4
|
+
* Reactive boolean reflecting the OS "reduce motion" accessibility preference.
|
|
5
|
+
* Wraps `AccessibilityInfo` as an external store via `useSyncExternalStore` — the
|
|
6
|
+
* effect-free, React-Compiler-safe way to read an external mutable source. The
|
|
7
|
+
* initial value is read asynchronously once (the platform API is a Promise) and
|
|
8
|
+
* cached module-side; subsequent changes arrive via the `reduceMotionChanged`
|
|
9
|
+
* event. Mirrors the `useOnlineStatus` pattern.
|
|
10
|
+
*
|
|
11
|
+
* Consumers use it to pin entrance/stagger animations off under reduce-motion.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useSyncExternalStore } from 'react';
|
|
15
|
+
import { AccessibilityInfo } from 'react-native';
|
|
16
|
+
|
|
17
|
+
let cachedReduceMotion = false;
|
|
18
|
+
let initialized = false;
|
|
19
|
+
const listeners = new Set<() => void>();
|
|
20
|
+
|
|
21
|
+
const notifyAll = (): void => {
|
|
22
|
+
for (const listener of listeners) listener();
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const setCached = (value: boolean): void => {
|
|
26
|
+
if (cachedReduceMotion === value) return;
|
|
27
|
+
cachedReduceMotion = value;
|
|
28
|
+
notifyAll();
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const subscribeReduceMotion = (notify: () => void): (() => void) => {
|
|
32
|
+
// Populate the cache the first time anyone subscribes (the platform read is
|
|
33
|
+
// async, so it cannot be a synchronous `getSnapshot`).
|
|
34
|
+
if (!initialized) {
|
|
35
|
+
initialized = true;
|
|
36
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
37
|
+
.then(setCached)
|
|
38
|
+
.catch(() => {
|
|
39
|
+
// No preference available — the `false` default stands.
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
listeners.add(notify);
|
|
43
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setCached);
|
|
44
|
+
return () => {
|
|
45
|
+
listeners.delete(notify);
|
|
46
|
+
sub.remove();
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const getSnapshot = (): boolean => cachedReduceMotion;
|
|
51
|
+
|
|
52
|
+
// SSR/web-first paint: assume motion is allowed (no reduce) until the async read
|
|
53
|
+
// resolves — matches the prior default.
|
|
54
|
+
const getServerSnapshot = (): boolean => false;
|
|
55
|
+
|
|
56
|
+
export function useReduceMotion(): boolean {
|
|
57
|
+
return useSyncExternalStore(subscribeReduceMotion, getSnapshot, getServerSnapshot);
|
|
58
|
+
}
|
|
@@ -13,7 +13,7 @@ import type { DialogHeaderConfig } from '@oxyhq/bloom/dialog';
|
|
|
13
13
|
*/
|
|
14
14
|
export type SurfaceHeaderContent = Pick<
|
|
15
15
|
DialogHeaderConfig,
|
|
16
|
-
'title' | 'subtitle' | 'largeTitle' | 'left' | 'right' | 'onBack'
|
|
16
|
+
'title' | 'titleContent' | 'subtitle' | 'largeTitle' | 'left' | 'right' | 'onBack'
|
|
17
17
|
>;
|
|
18
18
|
|
|
19
19
|
interface SurfaceHeaderContextValue {
|
|
@@ -47,6 +47,7 @@ export function useSurfaceHeader(content: SurfaceHeaderContent | null | undefine
|
|
|
47
47
|
}, [
|
|
48
48
|
set,
|
|
49
49
|
content?.title,
|
|
50
|
+
content?.titleContent,
|
|
50
51
|
content?.subtitle,
|
|
51
52
|
content?.largeTitle,
|
|
52
53
|
content?.left,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { getSurfaceConfig, getSurfacePresentation } from '../surfaceRegistry';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* `getSurfaceConfig` resolves
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* `getSurfaceConfig` resolves how a route renders. EVERY route is a responsive
|
|
5
|
+
* `'sheet'` that MORPHS in place; NO route stacks (genuine overlays are Bloom-raw
|
|
6
|
+
* calls outside this registry). The flagship image picker is a sheet that owns its
|
|
7
|
+
* own scrolling + chrome. These guard the routing contract.
|
|
8
8
|
*/
|
|
9
9
|
describe('getSurfaceConfig presentation', () => {
|
|
10
10
|
const imageOnlyPickerProps = {
|
|
@@ -18,11 +18,59 @@ describe('getSurfaceConfig presentation', () => {
|
|
|
18
18
|
expect(getSurfacePresentation('ManageAccount', {})).toBe('sheet');
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
it('
|
|
21
|
+
it('renders the FileManagement image-only picker as a morphing own-scroller sheet', () => {
|
|
22
22
|
const config = getSurfaceConfig('FileManagement', imageOnlyPickerProps);
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
// It morphs in place like every screen — a sheet, not a stacked full-bleed.
|
|
24
|
+
expect(config.presentation).toBe('sheet');
|
|
25
|
+
expect(config.stacks).toBe(false);
|
|
26
|
+
// PhotoPickerView owns its own FlatList → the host must not wrap it in a
|
|
27
|
+
// ScrollView; and its own translucent bar → no Dialog nav header.
|
|
25
28
|
expect(config.scrollable).toBe(false);
|
|
29
|
+
expect(config.header).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('morphs the surface for every route, the picker included', () => {
|
|
33
|
+
// NAV-WITHIN: drilling into these swaps the content inside ONE surface,
|
|
34
|
+
// which reshapes the panel instead of hard-cutting.
|
|
35
|
+
expect(getSurfaceConfig('ManageAccount', {}).morph).toBe(true);
|
|
36
|
+
expect(getSurfaceConfig('FAQ', {}).morph).toBe(true);
|
|
37
|
+
expect(getSurfaceConfig('FollowersList', {}).morph).toBe(true);
|
|
38
|
+
expect(getSurfaceConfig('FileManagement', imageOnlyPickerProps).morph).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('never stacks any route — every screen morphs in place', () => {
|
|
42
|
+
// `stacks` is the DECLARATIVE morph-vs-stack signal, and it is FALSE for
|
|
43
|
+
// every route: overlays (action sheets / confirm / prompt) are Bloom-raw
|
|
44
|
+
// `surfaces.present`/`confirm` calls outside this registry.
|
|
45
|
+
expect(getSurfaceConfig('ManageAccount', {}).stacks).toBe(false);
|
|
46
|
+
expect(getSurfaceConfig('AvatarCrop', {}).stacks).toBe(false);
|
|
47
|
+
expect(getSurfaceConfig('ChangeAvatar', {}).stacks).toBe(false);
|
|
48
|
+
expect(getSurfaceConfig('AccountDialog', {}).stacks).toBe(false);
|
|
49
|
+
expect(getSurfaceConfig('FileManagement', imageOnlyPickerProps).stacks).toBe(false);
|
|
50
|
+
expect(getSurfaceConfig('FileManagement', {}).stacks).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('lets the avatar flow morph list → crop inside ONE surface', () => {
|
|
54
|
+
// ChangeAvatar navigates WITHIN its surface to AvatarCrop, so both frames
|
|
55
|
+
// must share a presentation (else the crop would open on top instead of
|
|
56
|
+
// morphing) and both must opt IN to the morph.
|
|
57
|
+
const list = getSurfaceConfig('ChangeAvatar', {});
|
|
58
|
+
const crop = getSurfaceConfig('AvatarCrop', {});
|
|
59
|
+
expect(crop.presentation).toBe(list.presentation);
|
|
60
|
+
expect(list.morph).toBe(true);
|
|
61
|
+
expect(crop.morph).toBe(true);
|
|
62
|
+
// The morph can only RESIZE the panel when the Dialog owns the scroll
|
|
63
|
+
// container — an own-scroller surface cross-fades but never reshapes.
|
|
64
|
+
expect(list.scrollable).toBe(true);
|
|
65
|
+
expect(crop.scrollable).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('gives the avatar flow the Dialog nav header on both frames', () => {
|
|
69
|
+
// The Dialog's `header` is fixed for a surface's whole life, so a
|
|
70
|
+
// headerless crop frame inside a header-mode surface would stack two
|
|
71
|
+
// bars. Both frames declare their chrome through `useSurfaceHeader`.
|
|
72
|
+
expect(getSurfaceConfig('ChangeAvatar', {}).header).toBe(true);
|
|
73
|
+
expect(getSurfaceConfig('AvatarCrop', {}).header).toBe(true);
|
|
26
74
|
});
|
|
27
75
|
|
|
28
76
|
it('keeps FileManagement on the sheet when it is not an image-only picker', () => {
|