@oxyhq/services 22.6.3 → 22.7.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.
Files changed (52) hide show
  1. package/android/src/main/java/so/oxy/identity/OxyIdentityStore.kt +109 -14
  2. package/lib/commonjs/ui/boot/runProviderColdBoot.js +66 -1
  3. package/lib/commonjs/ui/boot/runProviderColdBoot.js.map +1 -1
  4. package/lib/commonjs/ui/components/fileManagement/AnimatedButton.js +7 -1
  5. package/lib/commonjs/ui/components/fileManagement/AnimatedButton.js.map +1 -1
  6. package/lib/commonjs/ui/components/photogrid/JustifiedPhotoGrid.js.map +1 -1
  7. package/lib/commonjs/ui/context/OxyContext.js +43 -0
  8. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  9. package/lib/commonjs/ui/screens/FileManagementScreen.js +17 -106
  10. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  11. package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js +133 -53
  12. package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
  13. package/lib/module/ui/boot/runProviderColdBoot.js +64 -0
  14. package/lib/module/ui/boot/runProviderColdBoot.js.map +1 -1
  15. package/lib/module/ui/components/fileManagement/AnimatedButton.js +7 -1
  16. package/lib/module/ui/components/fileManagement/AnimatedButton.js.map +1 -1
  17. package/lib/module/ui/components/photogrid/JustifiedPhotoGrid.js.map +1 -1
  18. package/lib/module/ui/context/OxyContext.js +44 -1
  19. package/lib/module/ui/context/OxyContext.js.map +1 -1
  20. package/lib/module/ui/screens/FileManagementScreen.js +13 -102
  21. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  22. package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js +133 -53
  23. package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
  24. package/lib/typescript/commonjs/ui/boot/runProviderColdBoot.d.ts +12 -0
  25. package/lib/typescript/commonjs/ui/boot/runProviderColdBoot.d.ts.map +1 -1
  26. package/lib/typescript/commonjs/ui/components/fileManagement/AnimatedButton.d.ts +1 -0
  27. package/lib/typescript/commonjs/ui/components/fileManagement/AnimatedButton.d.ts.map +1 -1
  28. package/lib/typescript/commonjs/ui/components/photogrid/JustifiedPhotoGrid.d.ts +0 -1
  29. package/lib/typescript/commonjs/ui/components/photogrid/JustifiedPhotoGrid.d.ts.map +1 -1
  30. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +2 -2
  32. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  33. package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts.map +1 -1
  34. package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
  35. package/lib/typescript/module/ui/boot/runProviderColdBoot.d.ts +12 -0
  36. package/lib/typescript/module/ui/boot/runProviderColdBoot.d.ts.map +1 -1
  37. package/lib/typescript/module/ui/components/fileManagement/AnimatedButton.d.ts +1 -0
  38. package/lib/typescript/module/ui/components/fileManagement/AnimatedButton.d.ts.map +1 -1
  39. package/lib/typescript/module/ui/components/photogrid/JustifiedPhotoGrid.d.ts +0 -1
  40. package/lib/typescript/module/ui/components/photogrid/JustifiedPhotoGrid.d.ts.map +1 -1
  41. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  42. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +2 -2
  43. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  44. package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts.map +1 -1
  45. package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
  46. package/package.json +4 -4
  47. package/src/ui/boot/runProviderColdBoot.ts +67 -0
  48. package/src/ui/components/fileManagement/AnimatedButton.tsx +9 -1
  49. package/src/ui/components/photogrid/JustifiedPhotoGrid.tsx +0 -1
  50. package/src/ui/context/OxyContext.tsx +43 -1
  51. package/src/ui/screens/FileManagementScreen.tsx +17 -112
  52. package/src/ui/screens/fileManagement/PhotoPickerSection.tsx +132 -51
@@ -12,6 +12,7 @@ import {
12
12
  Animated,
13
13
  Easing,
14
14
  AccessibilityInfo,
15
+ Platform,
15
16
  useWindowDimensions,
16
17
  } from 'react-native';
17
18
  import { Image as ExpoImage } from 'expo-image';
@@ -39,6 +40,7 @@ import { useFileUploadState } from './fileManagement/hooks/useFileUploadState';
39
40
  import PhotoPickerView from './fileManagement/PhotoPickerSection';
40
41
  import FileListSection, { type FileListItem } from './fileManagement/FileListSection';
41
42
  import UploadBar from './fileManagement/UploadBar';
43
+ import { AnimatedButton } from '../components/fileManagement/AnimatedButton';
42
44
 
43
45
  // Genuinely-inline-only styles: `viewModeButton` is spread into an Animated.View
44
46
  // style array (interpolated backgroundColor), and the photo tiles are
@@ -57,60 +59,6 @@ const screenStyles = StyleSheet.create({
57
59
  justifiedPhotoImage: { width: '100%', height: '100%', borderRadius: 6 },
58
60
  });
59
61
 
60
- // Animated button component for smooth transitions
61
- const AnimatedButton: React.FC<{
62
- isSelected: boolean;
63
- onPress: () => void;
64
- icon: string;
65
- primaryColor: string;
66
- textColor: string;
67
- style: Record<string, unknown>;
68
- accessibilityLabel: string;
69
- }> = ({ isSelected, onPress, icon, primaryColor, textColor, style, accessibilityLabel }) => {
70
- const animatedValue = useRef(new Animated.Value(isSelected ? 1 : 0)).current;
71
-
72
- useEffect(() => {
73
- Animated.timing(animatedValue, {
74
- toValue: isSelected ? 1 : 0,
75
- duration: 200,
76
- easing: Easing.out(Easing.ease),
77
- useNativeDriver: false,
78
- }).start();
79
- }, [isSelected, animatedValue]);
80
-
81
- const backgroundColor = animatedValue.interpolate({
82
- inputRange: [0, 1],
83
- outputRange: ['transparent', primaryColor],
84
- });
85
-
86
- return (
87
- <TouchableOpacity
88
- onPress={onPress}
89
- activeOpacity={0.7}
90
- accessibilityRole="button"
91
- accessibilityLabel={accessibilityLabel}
92
- accessibilityState={{ selected: isSelected }}
93
- >
94
- <Animated.View
95
- style={[
96
- style,
97
- {
98
- backgroundColor,
99
- },
100
- ]}
101
- >
102
- <Animated.View>
103
- <MaterialCommunityIcons
104
- name={icon as React.ComponentProps<typeof MaterialCommunityIcons>['name']}
105
- size={16}
106
- color={isSelected ? '#FFFFFF' : textColor}
107
- />
108
- </Animated.View>
109
- </Animated.View>
110
- </TouchableOpacity>
111
- );
112
- };
113
-
114
62
  const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
115
63
  onClose,
116
64
  theme,
@@ -1008,21 +956,12 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1008
956
  const photoIndex = photos.findIndex(p => p.id === lastSelectedFileId);
1009
957
 
1010
958
  if (photoIndex >= 0) {
1011
- // Estimate photo item height based on grid layout
1012
- // Calculate items per row
1013
- let itemsPerRow = 3;
1014
- if (safeContainerWidth > 768) itemsPerRow = 6;
1015
- else if (safeContainerWidth > 480) itemsPerRow = 4;
1016
-
1017
- const scrollContainerPadding = 32;
1018
- const gaps = (itemsPerRow - 1) * 4;
1019
- const availableWidth = safeContainerWidth - scrollContainerPadding;
1020
- const itemWidth = (availableWidth - gaps) / itemsPerRow;
1021
-
1022
- // Calculate row and approximate scroll position
959
+ // Rough scroll estimate for the justified grid (3 photos/row; variable row heights).
960
+ const itemsPerRow = 3;
961
+ const estimatedRowHeight = 150;
962
+ const rowGap = 4;
1023
963
  const row = Math.floor(photoIndex / itemsPerRow);
1024
- const headerHeight = 250;
1025
- const finalScrollPosition = headerHeight + (row * (itemWidth + 4)) - 150;
964
+ const finalScrollPosition = row * (estimatedRowHeight + rowGap) - 100;
1026
965
 
1027
966
  // Use requestAnimationFrame to ensure DOM is updated before scrolling
1028
967
  requestAnimationFrame(() => {
@@ -1049,45 +988,6 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1049
988
  }
1050
989
  }, [lastSelectedFileId]);
1051
990
 
1052
- const renderPhotoItem = (photo: FileMetadata, index: number) => {
1053
- const downloadUrl = thumbSourceFor(photo);
1054
-
1055
- // Calculate photo item width based on actual container size from bottom sheet
1056
- let itemsPerRow = 3; // Default for mobile
1057
- if (safeContainerWidth > 768) itemsPerRow = 6; // Tablet/Desktop
1058
- else if (safeContainerWidth > 480) itemsPerRow = 4; // Large mobile
1059
-
1060
- // Account for the photoScrollContainer padding (16px on each side = 32px total)
1061
- const scrollContainerPadding = 32; // Total horizontal padding from photoScrollContainer
1062
- const gaps = (itemsPerRow - 1) * 4; // Gap between items
1063
- const availableWidth = safeContainerWidth - scrollContainerPadding;
1064
- const itemWidth = (availableWidth - gaps) / itemsPerRow;
1065
-
1066
- return (
1067
- <TouchableOpacity
1068
- key={photo.id}
1069
- className="rounded-[8px] overflow-hidden"
1070
- style={{ width: itemWidth, height: itemWidth }}
1071
- onPress={() => handleFileOpen(photo)}
1072
- activeOpacity={0.8}
1073
- >
1074
- <View className="w-full h-full relative rounded-[8px] overflow-hidden">
1075
- <ExpoImage
1076
- source={{ uri: downloadUrl }}
1077
- style={screenStyles.photoImage}
1078
- contentFit="cover"
1079
- transition={120}
1080
- cachePolicy="memory-disk"
1081
- onError={() => {
1082
- // Image preview failed to load
1083
- }}
1084
- accessibilityLabel={photo.filename}
1085
- />
1086
- </View>
1087
- </TouchableOpacity>
1088
- );
1089
- };
1090
-
1091
991
  const renderPhotoGrid = useCallback(() => {
1092
992
  const photos = filteredFiles.filter(file => file.contentType.startsWith('image/'));
1093
993
 
@@ -1159,9 +1059,12 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1159
1059
  loadPhotoDimensions={loadPhotoDimensions}
1160
1060
  createJustifiedRows={createJustifiedRows}
1161
1061
  renderJustifiedPhotoItem={renderJustifiedPhotoItem}
1162
- renderSimplePhotoItem={renderPhotoItem}
1163
1062
  textColor={colors.text}
1164
- containerWidth={safeContainerWidth}
1063
+ containerWidth={
1064
+ typeof containerWidth === 'number' && containerWidth > 0
1065
+ ? containerWidth
1066
+ : undefined
1067
+ }
1165
1068
  />
1166
1069
  </ScrollView>
1167
1070
  );
@@ -1179,8 +1082,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1179
1082
  loadPhotoDimensions,
1180
1083
  createJustifiedRows,
1181
1084
  renderJustifiedPhotoItem,
1182
- renderPhotoItem,
1183
- safeContainerWidth
1085
+ containerWidth,
1184
1086
  ]);
1185
1087
 
1186
1088
  // Inline justified grid removed (moved to components/photogrid/JustifiedPhotoGrid.tsx)
@@ -1230,7 +1132,10 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1230
1132
  toValue: 1,
1231
1133
  duration: 2000,
1232
1134
  easing: Easing.linear,
1233
- useNativeDriver: true,
1135
+ // Web has no native animated module; forcing the native
1136
+ // driver there warns and falls back to JS. The shimmer only
1137
+ // drives a transform, so JS-driving it on web is fine.
1138
+ useNativeDriver: Platform.OS !== 'web',
1234
1139
  })
1235
1140
  );
1236
1141
  shimmer.start();
@@ -151,14 +151,61 @@ export interface PhotoPickerViewProps {
151
151
  * Apple Photos pattern: when any cell is selected, *non-selected* siblings
152
152
  * fade to 0.6 opacity to focus attention on the active selection.
153
153
  */
154
- const PhotoPickerCell = React.memo(function PhotoPickerCell(props: {
154
+ /**
155
+ * Shared cell chrome (image, dim, badge). Ring border is injected by the
156
+ * static vs animated wrappers so web never touches Reanimated worklets.
157
+ */
158
+ function PhotoPickerCellContent(props: {
159
+ photo: FileMetadata;
160
+ dim: boolean;
161
+ isSelected: boolean;
162
+ selectionIndex: number;
163
+ primaryColor: string;
164
+ thumbUrl: string | undefined;
165
+ ring: React.ReactNode;
166
+ }) {
167
+ const {
168
+ photo, dim, isSelected, selectionIndex, primaryColor, thumbUrl, ring,
169
+ } = props;
170
+
171
+ return (
172
+ <>
173
+ <View
174
+ className={`flex-1 rounded-radius-8 overflow-hidden bg-[#111111]${dim ? ' opacity-60' : ''}`}
175
+ >
176
+ <ExpoImage
177
+ source={{ uri: thumbUrl }}
178
+ style={{ width: '100%', height: '100%' }}
179
+ contentFit="cover"
180
+ transition={120}
181
+ cachePolicy="memory-disk"
182
+ accessibilityLabel={photo.filename}
183
+ />
184
+ </View>
185
+ {ring}
186
+ {isSelected && (
187
+ <View
188
+ pointerEvents="none"
189
+ className="absolute top-1.5 right-1.5 min-w-[22px] h-[22px] px-1.5 rounded-full items-center justify-center"
190
+ style={{ backgroundColor: primaryColor }}
191
+ >
192
+ <Text className="text-white text-[12px] font-bold leading-[14px]">
193
+ {selectionIndex > 0 ? String(selectionIndex) : ''}
194
+ </Text>
195
+ </View>
196
+ )}
197
+ </>
198
+ );
199
+ }
200
+
201
+ type PhotoPickerCellProps = {
155
202
  photo: FileMetadata;
156
203
  size: number;
157
204
  marginRight: number;
158
205
  marginBottom: number;
159
206
  isSelected: boolean;
160
- selectionIndex: number; // 1-based for badge; 0 if not selected
161
- dim: boolean; // any selection exists and this cell is not selected
207
+ selectionIndex: number;
208
+ dim: boolean;
162
209
  primaryColor: string;
163
210
  thumbUrl: string | undefined;
164
211
  enterIndex: number;
@@ -166,21 +213,64 @@ const PhotoPickerCell = React.memo(function PhotoPickerCell(props: {
166
213
  onPress: () => void;
167
214
  onLongPress: () => void;
168
215
  a11yLabel: string;
169
- }) {
216
+ };
217
+
218
+ const PhotoPickerCellStatic = React.memo(function PhotoPickerCellStatic(props: PhotoPickerCellProps) {
219
+ const {
220
+ photo, size, marginRight, marginBottom, isSelected, selectionIndex,
221
+ dim, primaryColor, thumbUrl, onPress, onLongPress, a11yLabel,
222
+ } = props;
223
+
224
+ const cellWrapperStyle = {
225
+ width: size,
226
+ height: size,
227
+ marginRight,
228
+ marginBottom,
229
+ };
230
+
231
+ const ring = isSelected ? (
232
+ <View
233
+ pointerEvents="none"
234
+ className="absolute inset-0 rounded-radius-8 border-[3px]"
235
+ style={{ borderColor: primaryColor }}
236
+ />
237
+ ) : null;
238
+
239
+ return (
240
+ <TouchableOpacity
241
+ activeOpacity={0.85}
242
+ onPress={onPress}
243
+ onLongPress={onLongPress}
244
+ className="relative"
245
+ style={cellWrapperStyle}
246
+ accessibilityRole="button"
247
+ accessibilityLabel={a11yLabel}
248
+ accessibilityState={{ selected: isSelected }}
249
+ >
250
+ <PhotoPickerCellContent
251
+ photo={photo}
252
+ dim={dim}
253
+ isSelected={isSelected}
254
+ selectionIndex={selectionIndex}
255
+ primaryColor={primaryColor}
256
+ thumbUrl={thumbUrl}
257
+ ring={ring}
258
+ />
259
+ </TouchableOpacity>
260
+ );
261
+ });
262
+
263
+ const PhotoPickerCellAnimated = React.memo(function PhotoPickerCellAnimated(props: PhotoPickerCellProps) {
170
264
  const {
171
265
  photo, size, marginRight, marginBottom, isSelected, selectionIndex,
172
266
  dim, primaryColor, thumbUrl, enterIndex, reduceMotion, onPress,
173
267
  onLongPress, a11yLabel,
174
268
  } = props;
175
269
 
176
- // Cap the cumulative stagger at ~800ms total so the very long grid does
177
- // not keep fading in late tiles. Beyond ~53 tiles the delay maxes out.
178
270
  const STAGGER_PER_CELL_MS = 15;
179
271
  const MAX_TOTAL_STAGGER_MS = 800;
180
272
  const delay = Math.min(enterIndex * STAGGER_PER_CELL_MS, MAX_TOTAL_STAGGER_MS);
181
273
 
182
- // Selection ring pulse animation: 1.0 → 1.05 → 1.0 on transition to
183
- // selected. Plays at most once per selection change; reduce-motion skips.
184
274
  const ringScale = useSharedValue(1);
185
275
  const prevSelected = useRef(isSelected);
186
276
  useEffect(() => {
@@ -204,8 +294,6 @@ const PhotoPickerCell = React.memo(function PhotoPickerCell(props: {
204
294
  transform: [{ scale: ringScale.value }],
205
295
  }));
206
296
 
207
- // Per-tile pixel geometry is derived from the measured grid width, so it
208
- // stays an inline `style` — NativeWind cannot express a runtime pixel size.
209
297
  const cellWrapperStyle = {
210
298
  width: size,
211
299
  height: size,
@@ -213,44 +301,13 @@ const PhotoPickerCell = React.memo(function PhotoPickerCell(props: {
213
301
  marginBottom,
214
302
  };
215
303
 
216
- const inner = (
217
- <>
218
- <View
219
- className={`flex-1 rounded-radius-8 overflow-hidden bg-[#111111]${dim ? ' opacity-60' : ''}`}
220
- >
221
- <ExpoImage
222
- source={{ uri: thumbUrl }}
223
- // expo-image is a third-party component (no NativeWind
224
- // className remap), so fill via inline style.
225
- style={{ width: '100%', height: '100%' }}
226
- contentFit="cover"
227
- transition={120}
228
- cachePolicy="memory-disk"
229
- accessibilityLabel={photo.filename}
230
- />
231
- </View>
232
- {isSelected && (
233
- <Reanimated.View
234
- pointerEvents="none"
235
- className="absolute inset-0 rounded-radius-8 border-[3px]"
236
- // borderColor is the theme primary (dynamic); the animated
237
- // scale transform must ride on `style` too.
238
- style={[{ borderColor: primaryColor }, ringAnimatedStyle]}
239
- />
240
- )}
241
- {isSelected && (
242
- <View
243
- pointerEvents="none"
244
- className="absolute top-1.5 right-1.5 min-w-[22px] h-[22px] px-1.5 rounded-full items-center justify-center"
245
- style={{ backgroundColor: primaryColor }}
246
- >
247
- <Text className="text-white text-[12px] font-bold leading-[14px]">
248
- {selectionIndex > 0 ? String(selectionIndex) : ''}
249
- </Text>
250
- </View>
251
- )}
252
- </>
253
- );
304
+ const ring = isSelected ? (
305
+ <Reanimated.View
306
+ pointerEvents="none"
307
+ className="absolute inset-0 rounded-radius-8 border-[3px]"
308
+ style={[{ borderColor: primaryColor }, ringAnimatedStyle]}
309
+ />
310
+ ) : null;
254
311
 
255
312
  if (reduceMotion) {
256
313
  return (
@@ -264,7 +321,15 @@ const PhotoPickerCell = React.memo(function PhotoPickerCell(props: {
264
321
  accessibilityLabel={a11yLabel}
265
322
  accessibilityState={{ selected: isSelected }}
266
323
  >
267
- {inner}
324
+ <PhotoPickerCellContent
325
+ photo={photo}
326
+ dim={dim}
327
+ isSelected={isSelected}
328
+ selectionIndex={selectionIndex}
329
+ primaryColor={primaryColor}
330
+ thumbUrl={thumbUrl}
331
+ ring={ring}
332
+ />
268
333
  </TouchableOpacity>
269
334
  );
270
335
  }
@@ -284,12 +349,27 @@ const PhotoPickerCell = React.memo(function PhotoPickerCell(props: {
284
349
  accessibilityLabel={a11yLabel}
285
350
  accessibilityState={{ selected: isSelected }}
286
351
  >
287
- {inner}
352
+ <PhotoPickerCellContent
353
+ photo={photo}
354
+ dim={dim}
355
+ isSelected={isSelected}
356
+ selectionIndex={selectionIndex}
357
+ primaryColor={primaryColor}
358
+ thumbUrl={thumbUrl}
359
+ ring={ring}
360
+ />
288
361
  </TouchableOpacity>
289
362
  </Reanimated.View>
290
363
  );
291
364
  });
292
365
 
366
+ const PhotoPickerCell = React.memo(function PhotoPickerCell(props: PhotoPickerCellProps) {
367
+ if (Platform.OS === 'web') {
368
+ return <PhotoPickerCellStatic {...props} />;
369
+ }
370
+ return <PhotoPickerCellAnimated {...props} />;
371
+ });
372
+
293
373
  const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
294
374
  photos,
295
375
  selectedIds,
@@ -547,7 +627,8 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
547
627
  </View>
548
628
  <View pointerEvents="none">
549
629
  <Text
550
- className="text-white text-[17px] font-semibold text-center tracking-[-0.2px]"
630
+ className="text-white text-[17px] font-semibold text-center"
631
+ style={{ letterSpacing: -0.2 }}
551
632
  numberOfLines={1}
552
633
  >
553
634
  {t('fileManagement.choosePhoto')}