@oxyhq/services 22.10.0 → 22.11.1
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 +71 -39
- 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 +71 -40
- 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 +11 -7
- 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 +73 -44
- 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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
|
-
import { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
3
3
|
import {
|
|
4
4
|
View,
|
|
5
5
|
Pressable,
|
|
@@ -17,6 +17,7 @@ import { getAccountDisplayName, getAccountFallbackHandle } from '@oxyhq/core';
|
|
|
17
17
|
import { useAuth } from '../hooks/useAuth';
|
|
18
18
|
import { useOxy } from '../context/OxyContext';
|
|
19
19
|
import { useI18n } from '../hooks/useI18n';
|
|
20
|
+
import { registerAccountDialogConsumerHooks } from '../navigation/accountDialogManager';
|
|
20
21
|
|
|
21
22
|
const isWeb = Platform.OS === 'web';
|
|
22
23
|
|
|
@@ -102,6 +103,9 @@ export interface ProfileButtonProps {
|
|
|
102
103
|
const ProfileButton: React.FC<ProfileButtonProps> = ({
|
|
103
104
|
expanded = true,
|
|
104
105
|
avatarSize,
|
|
106
|
+
onNavigateManage,
|
|
107
|
+
onAddAccount,
|
|
108
|
+
onNavigateProfile,
|
|
105
109
|
className,
|
|
106
110
|
style,
|
|
107
111
|
}) => {
|
|
@@ -127,6 +131,17 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
|
|
|
127
131
|
openAccountDialog('accounts');
|
|
128
132
|
}, [openAccountDialog]);
|
|
129
133
|
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
if (!onNavigateManage && !onAddAccount && !onNavigateProfile) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
return registerAccountDialogConsumerHooks({
|
|
139
|
+
onNavigateManage,
|
|
140
|
+
onAddAccount,
|
|
141
|
+
onNavigateProfile,
|
|
142
|
+
});
|
|
143
|
+
}, [onNavigateManage, onAddAccount, onNavigateProfile]);
|
|
144
|
+
|
|
130
145
|
const avatarUrl = useMemo(
|
|
131
146
|
() => (user?.avatar ? oxyServices.getFileDownloadUrl(user.avatar, 'thumb') : undefined),
|
|
132
147
|
[user?.avatar, oxyServices],
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
2
|
import { View, type ViewStyle } from 'react-native';
|
|
3
|
-
import { Ionicons } from '@expo/vector-icons';
|
|
4
|
-
import { useTheme } from '@oxyhq/bloom/theme';
|
|
5
3
|
import { H4, Text } from '@oxyhq/bloom/typography';
|
|
6
4
|
import { Avatar } from '@oxyhq/bloom/avatar';
|
|
7
5
|
import { Card } from '@oxyhq/bloom/card';
|
|
8
6
|
import { PressableScale } from '@oxyhq/bloom/pressable-scale';
|
|
7
|
+
import AvatarCameraBadge from './AvatarCameraBadge';
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
10
|
* ProfileSummaryCard — the shared "who am I" summary block rendered at the top
|
|
@@ -37,18 +36,6 @@ export interface ProfileSummaryCardProps {
|
|
|
37
36
|
avatarAccessibilityLabel?: string;
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
const badgeStyle: ViewStyle = {
|
|
41
|
-
position: 'absolute',
|
|
42
|
-
right: 0,
|
|
43
|
-
bottom: 0,
|
|
44
|
-
width: 28,
|
|
45
|
-
height: 28,
|
|
46
|
-
borderRadius: 14,
|
|
47
|
-
alignItems: 'center',
|
|
48
|
-
justifyContent: 'center',
|
|
49
|
-
borderWidth: 2,
|
|
50
|
-
};
|
|
51
|
-
|
|
52
39
|
const cardStyle: ViewStyle = {
|
|
53
40
|
borderRadius: 20,
|
|
54
41
|
marginBottom: 16,
|
|
@@ -67,8 +54,6 @@ const ProfileSummaryCard: React.FC<ProfileSummaryCardProps> = ({
|
|
|
67
54
|
showCameraBadge = false,
|
|
68
55
|
avatarAccessibilityLabel,
|
|
69
56
|
}) => {
|
|
70
|
-
const bloomTheme = useTheme();
|
|
71
|
-
|
|
72
57
|
const avatar = <Avatar source={avatarUri} name={displayName} size={avatarSize} />;
|
|
73
58
|
|
|
74
59
|
const avatarNode = onAvatarPress ? (
|
|
@@ -80,15 +65,7 @@ const ProfileSummaryCard: React.FC<ProfileSummaryCardProps> = ({
|
|
|
80
65
|
className="mb-space-12"
|
|
81
66
|
>
|
|
82
67
|
{avatar}
|
|
83
|
-
{showCameraBadge ?
|
|
84
|
-
<View style={badgeStyle} className="bg-fill-brand border-border-image">
|
|
85
|
-
<Ionicons
|
|
86
|
-
name="camera"
|
|
87
|
-
size={14}
|
|
88
|
-
color={bloomTheme.colors.primaryForeground}
|
|
89
|
-
/>
|
|
90
|
-
</View>
|
|
91
|
-
) : null}
|
|
68
|
+
{showCameraBadge ? <AvatarCameraBadge /> : null}
|
|
92
69
|
</PressableScale>
|
|
93
70
|
) : (
|
|
94
71
|
<View className="mb-space-12">{avatar}</View>
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useMemo, useState, type ErrorInfo } from 'react';
|
|
2
|
-
import { StyleSheet, Text, View } from 'react-native';
|
|
2
|
+
import { StyleSheet, Text, View, useWindowDimensions } from 'react-native';
|
|
3
3
|
import { useStore } from 'zustand';
|
|
4
4
|
import { useTheme } from '@oxyhq/bloom/theme';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
useDialogFrame,
|
|
7
|
+
useDialogHeader,
|
|
8
|
+
type DialogHeaderConfig,
|
|
9
|
+
} from '@oxyhq/bloom/dialog';
|
|
6
10
|
import type { SurfaceControls } from '@oxyhq/bloom/surfaces';
|
|
7
11
|
import type { RouteName } from '../navigation/routes';
|
|
8
12
|
import { getScreenComponent } from '../navigation/routes';
|
|
9
13
|
import { pushSurfaceBackHandler } from '../navigation/surfaceBackBridge';
|
|
10
14
|
import type { SurfaceNavStack } from '../navigation/surfaceNavStack';
|
|
11
|
-
import { getSurfaceConfig
|
|
15
|
+
import { getSurfaceConfig } from '../navigation/surfaceRegistry';
|
|
12
16
|
import {
|
|
13
17
|
closeAllRouteSurfaces,
|
|
14
18
|
navigateWithinOrPresent,
|
|
@@ -76,8 +80,6 @@ export interface SurfaceScreenProps {
|
|
|
76
80
|
navStack: SurfaceNavStack;
|
|
77
81
|
/** Bloom's per-surface controls (dismiss / present a child) — the DEPTH axis. */
|
|
78
82
|
surface: SurfaceControls;
|
|
79
|
-
/** This surface's presentation, so `navigate` knows when to drill in vs. stack. */
|
|
80
|
-
presentation: SurfacePresentation;
|
|
81
83
|
/** Whether backdrop / Escape / back may dismiss at the root frame. Defaults to true. */
|
|
82
84
|
dismissOnBackdrop?: boolean;
|
|
83
85
|
}
|
|
@@ -94,7 +96,6 @@ export interface SurfaceScreenProps {
|
|
|
94
96
|
function SurfaceScreen({
|
|
95
97
|
navStack,
|
|
96
98
|
surface,
|
|
97
|
-
presentation,
|
|
98
99
|
dismissOnBackdrop = true,
|
|
99
100
|
}: SurfaceScreenProps) {
|
|
100
101
|
const theme = useTheme();
|
|
@@ -109,17 +110,24 @@ function SurfaceScreen({
|
|
|
109
110
|
|
|
110
111
|
const navigate = useCallback(
|
|
111
112
|
(route: RouteName, props?: Record<string, unknown>) =>
|
|
112
|
-
navigateWithinOrPresent(
|
|
113
|
-
[navStack
|
|
113
|
+
navigateWithinOrPresent(navStack, route, props),
|
|
114
|
+
[navStack],
|
|
114
115
|
);
|
|
115
116
|
|
|
116
117
|
const replace = useCallback(
|
|
117
118
|
(route: RouteName, props?: Record<string, unknown>) =>
|
|
118
|
-
replaceWithinOrPresent(
|
|
119
|
-
[navStack
|
|
119
|
+
replaceWithinOrPresent(navStack, route, props),
|
|
120
|
+
[navStack],
|
|
120
121
|
);
|
|
121
122
|
|
|
122
|
-
|
|
123
|
+
// Screens' `dismiss(result)` resolves an active sub-flow (morphed-in avatar
|
|
124
|
+
// picker) back to its caller and pops to its entry frame; with no flow it
|
|
125
|
+
// dismisses the whole surface (a screen presented as its own surface) — the
|
|
126
|
+
// historical behaviour. `navStack.resolveFlowOrDismiss` owns that fork.
|
|
127
|
+
const dismiss = useCallback(
|
|
128
|
+
(result?: unknown) => navStack.resolveFlowOrDismiss(result),
|
|
129
|
+
[navStack],
|
|
130
|
+
);
|
|
123
131
|
|
|
124
132
|
const canGoBack = useCallback(() => navStack.canGoBack(), [navStack]);
|
|
125
133
|
|
|
@@ -165,9 +173,35 @@ function SurfaceScreen({
|
|
|
165
173
|
const [headerContent, setHeaderContent] = useState<SurfaceHeaderContent | null>(null);
|
|
166
174
|
const headerContext = useMemo(() => ({ setContent: setHeaderContent }), []);
|
|
167
175
|
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
176
|
+
const config = useMemo(() => getSurfaceConfig(top.route, top.props), [top.route, top.props]);
|
|
177
|
+
const headerMode = config.header;
|
|
178
|
+
|
|
179
|
+
// --- Size morphing on nav-within ---------------------------------------
|
|
180
|
+
// The Dialog cannot see a drill-in from its own props (only this host
|
|
181
|
+
// re-renders when the nav stack moves), so it is told which frame is on
|
|
182
|
+
// screen. When that identity changes the surface MORPHS — the panel animates
|
|
183
|
+
// between the two frames' sizes instead of hard-cutting. A wizard step counts
|
|
184
|
+
// as a frame: it swaps the visible content inside one screen the same way.
|
|
185
|
+
// A `stacks` route opens its OWN surface (never a frame here); everything else
|
|
186
|
+
// morphs.
|
|
187
|
+
//
|
|
188
|
+
// A route may declare an EXPLICIT large morph target (`frameSize`) — the media
|
|
189
|
+
// selector's own-scroller grid, which the panel can't measure, grows the
|
|
190
|
+
// container to a near-full-height, wider card and shrinks back on exit. Resolve
|
|
191
|
+
// its viewport-relative height to px here (the Dialog clamps to the viewport).
|
|
192
|
+
const { height: viewportHeight } = useWindowDimensions();
|
|
193
|
+
const frameSize = useMemo(() => {
|
|
194
|
+
const spec = config.frameSize;
|
|
195
|
+
if (!spec) return undefined;
|
|
196
|
+
const height =
|
|
197
|
+
spec.heightRatio !== undefined ? Math.round(viewportHeight * spec.heightRatio) : undefined;
|
|
198
|
+
return { height, maxWidth: spec.maxWidth };
|
|
199
|
+
}, [config.frameSize, viewportHeight]);
|
|
200
|
+
useDialogFrame(
|
|
201
|
+
useMemo(
|
|
202
|
+
() => ({ key: `${top.route}#${top.step}`, morph: config.morph, size: frameSize }),
|
|
203
|
+
[top.route, top.step, config.morph, frameSize],
|
|
204
|
+
),
|
|
171
205
|
);
|
|
172
206
|
// Show a back affordance whenever the surface can navigate back — either an
|
|
173
207
|
// earlier frame in this surface's stack, or an earlier wizard step.
|
|
@@ -42,36 +42,88 @@ export const authChooserStyles = StyleSheet.create({
|
|
|
42
42
|
fontSize: 12.5,
|
|
43
43
|
marginTop: 1,
|
|
44
44
|
},
|
|
45
|
-
/**
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Animated-collapse container for the account switch list — the outer clip and
|
|
47
|
+
* the inner measuring wrapper. Style-based (NOT NativeWind): the container
|
|
48
|
+
* carries the animated `height`/`opacity` off a reanimated style, and the
|
|
49
|
+
* measuring wrapper must stay className-free so its `onLayout` fires on RN-Web.
|
|
50
|
+
*/
|
|
51
|
+
collapse: {
|
|
52
|
+
overflow: 'hidden',
|
|
53
|
+
},
|
|
54
|
+
collapseMeasure: {
|
|
55
|
+
width: '100%',
|
|
56
|
+
},
|
|
57
|
+
/**
|
|
58
|
+
* The HERO block that opens the account menu: the current account's large
|
|
59
|
+
* avatar, a greeting, and the "Manage your Oxy account" pill — centred and
|
|
60
|
+
* deliberately chrome-free (no card, no grouped-section surface) so the
|
|
61
|
+
* grouped cards below read as the menu proper. The account's email sits above
|
|
62
|
+
* it in the Dialog's own nav bar, not here.
|
|
63
|
+
*/
|
|
64
|
+
hero: {
|
|
48
65
|
alignItems: 'center',
|
|
66
|
+
paddingTop: 4,
|
|
67
|
+
paddingBottom: 20,
|
|
49
68
|
gap: 12,
|
|
50
|
-
paddingVertical: 8,
|
|
51
|
-
paddingHorizontal: 4,
|
|
52
69
|
},
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
70
|
+
/**
|
|
71
|
+
* The current account's accent ring in the switch list, drawn OUTSIDE its
|
|
72
|
+
* avatar as an overlay so it adds no width to the row — every avatar in the
|
|
73
|
+
* list keeps the same footprint and the same content line. The 3px offset is
|
|
74
|
+
* the ring's 2px stroke plus a 1px gap.
|
|
75
|
+
*/
|
|
76
|
+
currentAvatarRing: {
|
|
77
|
+
position: 'absolute',
|
|
78
|
+
top: -3,
|
|
79
|
+
left: -3,
|
|
80
|
+
right: -3,
|
|
81
|
+
bottom: -3,
|
|
82
|
+
borderWidth: 2,
|
|
83
|
+
borderRadius: 9999,
|
|
56
84
|
},
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
85
|
+
/**
|
|
86
|
+
* The hero avatar's pressable wrapper — the "change your photo" entry point.
|
|
87
|
+
* `relative` so the accent ring + camera badge (both absolutely positioned)
|
|
88
|
+
* anchor to the avatar box.
|
|
89
|
+
*/
|
|
90
|
+
heroAvatarPressable: {
|
|
91
|
+
position: 'relative',
|
|
92
|
+
},
|
|
93
|
+
/** The hero avatar's accent ring — an overlay, like the row-level one. */
|
|
94
|
+
heroAvatarRing: {
|
|
95
|
+
position: 'absolute',
|
|
96
|
+
top: -4,
|
|
97
|
+
left: -4,
|
|
98
|
+
right: -4,
|
|
99
|
+
bottom: -4,
|
|
100
|
+
borderWidth: 3,
|
|
101
|
+
borderRadius: 9999,
|
|
60
102
|
},
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
103
|
+
/** Greeting + address stacked tight, centred (the hero's own 12px gap sits
|
|
104
|
+
* between this block and the avatar / manage pill, not between these lines). */
|
|
105
|
+
heroNameBlock: {
|
|
106
|
+
alignItems: 'center',
|
|
107
|
+
gap: 2,
|
|
64
108
|
},
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
109
|
+
heroGreeting: {
|
|
110
|
+
fontSize: 22,
|
|
111
|
+
fontWeight: '600',
|
|
112
|
+
textAlign: 'center',
|
|
69
113
|
},
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
114
|
+
/** The account address (`username@oxy.so` / `@handle`) under the greeting. */
|
|
115
|
+
heroAddress: {
|
|
116
|
+
fontSize: 14,
|
|
117
|
+
textAlign: 'center',
|
|
118
|
+
},
|
|
119
|
+
/**
|
|
120
|
+
* The switch row's disclosure affordance — a filled circle around the chevron,
|
|
121
|
+
* right-aligned to the same 12px row inset every other trailing element uses.
|
|
122
|
+
*/
|
|
123
|
+
chevronCircle: {
|
|
124
|
+
width: 32,
|
|
125
|
+
height: 32,
|
|
126
|
+
borderRadius: 16,
|
|
75
127
|
alignItems: 'center',
|
|
76
128
|
justifyContent: 'center',
|
|
77
129
|
},
|
|
@@ -1,12 +1,70 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import { View, Text, useWindowDimensions, type LayoutChangeEvent } from 'react-native';
|
|
1
|
+
import React, { useMemo, useState, useCallback } from 'react';
|
|
2
|
+
import { View, Text, Image, useWindowDimensions, type LayoutChangeEvent } from 'react-native';
|
|
3
|
+
import { useQuery, keepPreviousData } from '@tanstack/react-query';
|
|
3
4
|
import type { FileMetadata } from '@oxyhq/core';
|
|
4
5
|
|
|
6
|
+
type PhotoSize = { width: number; height: number };
|
|
7
|
+
|
|
8
|
+
/** Stable empty map so an unresolved query yields a referentially-stable value. */
|
|
9
|
+
const EMPTY_DIMENSIONS: Record<string, PhotoSize> = {};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Measure the intrinsic size of every photo whose thumbnail URL has resolved,
|
|
13
|
+
* via `Image.getSize`. Pure/async (no React state) so it can be a React Query
|
|
14
|
+
* `queryFn`. Photos without a resolved URL are skipped and re-measured on the
|
|
15
|
+
* next signature change; a load error falls back to 1×1 (→ the 4/3 default).
|
|
16
|
+
*/
|
|
17
|
+
async function measurePhotoDimensions(
|
|
18
|
+
photos: FileMetadata[],
|
|
19
|
+
getThumbUrl: (photo: FileMetadata) => string | undefined,
|
|
20
|
+
): Promise<Record<string, PhotoSize>> {
|
|
21
|
+
const measured: Record<string, PhotoSize> = {};
|
|
22
|
+
await Promise.all(
|
|
23
|
+
photos.map(
|
|
24
|
+
(photo) =>
|
|
25
|
+
new Promise<void>((resolve) => {
|
|
26
|
+
const url = getThumbUrl(photo);
|
|
27
|
+
if (!url) {
|
|
28
|
+
resolve();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
Image.getSize(
|
|
32
|
+
url,
|
|
33
|
+
(width, height) => {
|
|
34
|
+
measured[photo.id] = { width, height };
|
|
35
|
+
resolve();
|
|
36
|
+
},
|
|
37
|
+
() => {
|
|
38
|
+
measured[photo.id] = { width: 1, height: 1 };
|
|
39
|
+
resolve();
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
}),
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
return measured;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Fixed photos-per-row for the justified grid (Apple Photos-style trio). */
|
|
49
|
+
export const PHOTOS_PER_ROW = 3;
|
|
50
|
+
|
|
51
|
+
/** Chunk a photo list into fixed-length rows (last row may be short). */
|
|
52
|
+
export function chunkPhotos(photos: FileMetadata[], perRow: number): FileMetadata[][] {
|
|
53
|
+
const rows: FileMetadata[][] = [];
|
|
54
|
+
for (let i = 0; i < photos.length; i += perRow) {
|
|
55
|
+
rows.push(photos.slice(i, i + perRow));
|
|
56
|
+
}
|
|
57
|
+
return rows;
|
|
58
|
+
}
|
|
59
|
+
|
|
5
60
|
export interface JustifiedPhotoGridProps {
|
|
6
61
|
photos: FileMetadata[];
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Resolve a private-safe thumbnail URL for a photo (or `undefined` while the
|
|
64
|
+
* URL is still resolving). The grid measures each photo's intrinsic size from
|
|
65
|
+
* this URL and re-measures automatically once a pending URL resolves.
|
|
66
|
+
*/
|
|
67
|
+
getThumbUrl: (photo: FileMetadata) => string | undefined;
|
|
10
68
|
renderJustifiedPhotoItem: (photo: FileMetadata, width: number, height: number, isLast: boolean) => React.ReactElement;
|
|
11
69
|
textColor: string;
|
|
12
70
|
/**
|
|
@@ -22,12 +80,15 @@ export interface JustifiedPhotoGridProps {
|
|
|
22
80
|
/**
|
|
23
81
|
* Responsive justified photo grid that stretches to the provided containerWidth.
|
|
24
82
|
* Uses flex rows with proportional children widths instead of absolute pixel widths so it always fills.
|
|
83
|
+
*
|
|
84
|
+
* Owns its own intrinsic-dimension measurement: it calls `Image.getSize` on each
|
|
85
|
+
* photo's resolved thumbnail URL and reflows once real aspect ratios are known
|
|
86
|
+
* (falling back to 4/3 until then). Measurement re-runs whenever a photo is added
|
|
87
|
+
* OR its (asynchronously-resolved) private-safe URL first appears.
|
|
25
88
|
*/
|
|
26
89
|
const JustifiedPhotoGrid: React.FC<JustifiedPhotoGridProps> = ({
|
|
27
90
|
photos,
|
|
28
|
-
|
|
29
|
-
loadPhotoDimensions,
|
|
30
|
-
createJustifiedRows,
|
|
91
|
+
getThumbUrl,
|
|
31
92
|
renderJustifiedPhotoItem,
|
|
32
93
|
textColor,
|
|
33
94
|
containerWidth: explicitWidth,
|
|
@@ -52,11 +113,27 @@ const JustifiedPhotoGrid: React.FC<JustifiedPhotoGridProps> = ({
|
|
|
52
113
|
const w = e.nativeEvent.layout.width;
|
|
53
114
|
setMeasuredWidth(prev => (prev === w ? prev : w));
|
|
54
115
|
}, [resolvedExplicitWidth]);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
116
|
+
|
|
117
|
+
// Signature of every photo id + its currently-resolved URL — the measurement
|
|
118
|
+
// query key. A private tile's URL resolves asynchronously after it enters the
|
|
119
|
+
// list, so keying on THIS (not just the photo set) re-measures the moment a
|
|
120
|
+
// URL lands, not just when photos are added.
|
|
121
|
+
const urlSignature = useMemo(
|
|
122
|
+
() => photos.map((p) => `${p.id}:${getThumbUrl(p) ?? ''}`).join('|'),
|
|
123
|
+
[photos, getThumbUrl],
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Intrinsic dimensions via React Query (no effect): re-runs when the signature
|
|
127
|
+
// changes and reflows the justified rows. `keepPreviousData` holds the last
|
|
128
|
+
// measured map during a re-measure so tiles never flash back to the 4/3 default.
|
|
129
|
+
const dimensionsQuery = useQuery({
|
|
130
|
+
queryKey: ['justifiedPhotoDimensions', urlSignature],
|
|
131
|
+
queryFn: () => measurePhotoDimensions(photos, getThumbUrl),
|
|
132
|
+
placeholderData: keepPreviousData,
|
|
133
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
134
|
+
gcTime: 5 * 60 * 1000,
|
|
135
|
+
});
|
|
136
|
+
const photoDimensions = dimensionsQuery.data ?? EMPTY_DIMENSIONS;
|
|
60
137
|
|
|
61
138
|
// Group photos by date first
|
|
62
139
|
const photosByDate = useMemo(() => {
|
|
@@ -80,77 +157,74 @@ const JustifiedPhotoGrid: React.FC<JustifiedPhotoGridProps> = ({
|
|
|
80
157
|
<View style={{ width: '100%' }} onLayout={onLayoutContainer}>
|
|
81
158
|
{sortedDates.map((date: string) => {
|
|
82
159
|
const dayPhotos = photosByDate[date];
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
</View>
|
|
152
|
-
);
|
|
153
|
-
})}
|
|
160
|
+
// Fall back to the overall width until this section is measured.
|
|
161
|
+
const dateWidth = dateWidths[date] ?? effectiveWidth;
|
|
162
|
+
const rows = dateWidth > 0 ? chunkPhotos(dayPhotos, PHOTOS_PER_ROW) : [];
|
|
163
|
+
return (
|
|
164
|
+
<View
|
|
165
|
+
key={date}
|
|
166
|
+
style={{ marginBottom: 24, width: '100%' }}
|
|
167
|
+
onLayout={e => onLayoutDate(date, e.nativeEvent.layout.width)}
|
|
168
|
+
>
|
|
169
|
+
<Text style={{ fontSize: 14, fontWeight: '600', marginBottom: 12, color: textColor }}>
|
|
170
|
+
{new Date(date).toLocaleDateString(dateFormatLocale, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
|
|
171
|
+
</Text>
|
|
172
|
+
<View style={{ width: '100%' }}>
|
|
173
|
+
{rows.map((row, rowIndex) => {
|
|
174
|
+
// Total aspect ratio of the row, using measured dims (fallback 4/3).
|
|
175
|
+
const aspects = row.map(p => {
|
|
176
|
+
const dims = photoDimensions[p.id];
|
|
177
|
+
return dims ? dims.width / dims.height : 4 / 3;
|
|
178
|
+
});
|
|
179
|
+
const totalAspect = aspects.reduce((a, b) => a + b, 0);
|
|
180
|
+
const gapsTotal = gap * (row.length - 1);
|
|
181
|
+
const availableWidth = dateWidth - gapsTotal;
|
|
182
|
+
// Ideal height that perfectly fills width when preserving aspect ratios.
|
|
183
|
+
const idealHeight = availableWidth / totalAspect;
|
|
184
|
+
// Clamp to min/max, then distribute the leftover/overflow proportionally.
|
|
185
|
+
let rowHeight = idealHeight;
|
|
186
|
+
let widthAdjustment = 0;
|
|
187
|
+
if (idealHeight < minRowHeight) {
|
|
188
|
+
rowHeight = minRowHeight;
|
|
189
|
+
widthAdjustment = availableWidth - rowHeight * totalAspect;
|
|
190
|
+
} else if (idealHeight > maxRowHeight) {
|
|
191
|
+
rowHeight = maxRowHeight;
|
|
192
|
+
widthAdjustment = availableWidth - rowHeight * totalAspect;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let widths = aspects.map(ar => ar * rowHeight);
|
|
196
|
+
if (widthAdjustment !== 0) {
|
|
197
|
+
const widthSum = widths.reduce((a, b) => a + b, 0);
|
|
198
|
+
widths = widths.map(w => w + (w / widthSum) * widthAdjustment);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Correct any rounding drift on the last tile so the row fills exactly.
|
|
202
|
+
const widthSumRounded = widths.reduce((a, b) => a + b, 0);
|
|
203
|
+
const roundingDiff = availableWidth - widthSumRounded;
|
|
204
|
+
if (Math.abs(roundingDiff) > 0.5) {
|
|
205
|
+
widths[widths.length - 1] += roundingDiff;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<View key={row[0]?.id ?? rowIndex} style={{ flexDirection: 'row', width: '100%', marginBottom: 4 }}>
|
|
210
|
+
{row.map((p, i) => {
|
|
211
|
+
const photoWidth = widths[i];
|
|
212
|
+
return (
|
|
213
|
+
<View
|
|
214
|
+
key={p.id}
|
|
215
|
+
style={{ width: photoWidth, height: rowHeight, marginRight: i === row.length - 1 ? 0 : gap }}
|
|
216
|
+
>
|
|
217
|
+
{renderJustifiedPhotoItem(p, photoWidth, rowHeight, i === row.length - 1)}
|
|
218
|
+
</View>
|
|
219
|
+
);
|
|
220
|
+
})}
|
|
221
|
+
</View>
|
|
222
|
+
);
|
|
223
|
+
})}
|
|
224
|
+
</View>
|
|
225
|
+
</View>
|
|
226
|
+
);
|
|
227
|
+
})}
|
|
154
228
|
</View>
|
|
155
229
|
);
|
|
156
230
|
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { FileMetadata } from '@oxyhq/core';
|
|
2
|
+
import { chunkPhotos, PHOTOS_PER_ROW } from '../JustifiedPhotoGrid';
|
|
3
|
+
|
|
4
|
+
// `chunkPhotos` is the row-building logic the justified photo grid owns (moved
|
|
5
|
+
// out of FileManagementScreen's former `createJustifiedRows`). It is pure, so it
|
|
6
|
+
// is unit-testable — unlike the grid's intrinsic-dimension measurement, which
|
|
7
|
+
// depends on native `Image.getSize`.
|
|
8
|
+
const photo = (id: string): FileMetadata => ({
|
|
9
|
+
id,
|
|
10
|
+
filename: `${id}.jpg`,
|
|
11
|
+
contentType: 'image/jpeg',
|
|
12
|
+
length: 1,
|
|
13
|
+
chunkSize: 0,
|
|
14
|
+
uploadDate: '2026-01-01T00:00:00.000Z',
|
|
15
|
+
metadata: {},
|
|
16
|
+
variants: [],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const ids = (rows: FileMetadata[][]): string[][] => rows.map((row) => row.map((p) => p.id));
|
|
20
|
+
|
|
21
|
+
describe('chunkPhotos', () => {
|
|
22
|
+
it('returns no rows for an empty list', () => {
|
|
23
|
+
expect(chunkPhotos([], PHOTOS_PER_ROW)).toEqual([]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('groups photos into full rows of the given size', () => {
|
|
27
|
+
const photos = ['a', 'b', 'c', 'd', 'e', 'f'].map(photo);
|
|
28
|
+
expect(ids(chunkPhotos(photos, PHOTOS_PER_ROW))).toEqual([
|
|
29
|
+
['a', 'b', 'c'],
|
|
30
|
+
['d', 'e', 'f'],
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('leaves the final row short when the count is not a multiple of the row size', () => {
|
|
35
|
+
const photos = ['a', 'b', 'c', 'd'].map(photo);
|
|
36
|
+
expect(ids(chunkPhotos(photos, PHOTOS_PER_ROW))).toEqual([
|
|
37
|
+
['a', 'b', 'c'],
|
|
38
|
+
['d'],
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('preserves order and never drops or duplicates a photo', () => {
|
|
43
|
+
const photos = Array.from({ length: 10 }, (_, i) => photo(`p${i}`));
|
|
44
|
+
const flat = chunkPhotos(photos, PHOTOS_PER_ROW).flat().map((p) => p.id);
|
|
45
|
+
expect(flat).toEqual(photos.map((p) => p.id));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('defaults to a trio per row', () => {
|
|
49
|
+
expect(PHOTOS_PER_ROW).toBe(3);
|
|
50
|
+
});
|
|
51
|
+
});
|