@oxyhq/services 22.8.2 → 22.8.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/services",
3
- "version": "22.8.2",
3
+ "version": "22.8.3",
4
4
  "description": "OxyHQ Expo/React Native SDK — UI components, screens, and native features",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -136,7 +136,7 @@
136
136
  "@commitlint/cli": "^17.6.5",
137
137
  "@commitlint/config-conventional": "^17.6.5",
138
138
  "@expo/vector-icons": "^15.0.3",
139
- "@oxyhq/core": "12.9.1",
139
+ "@oxyhq/core": "12.10.0",
140
140
  "@react-native-async-storage/async-storage": "^2.0.0",
141
141
  "@react-native-community/netinfo": "^11.4.1",
142
142
  "@react-native/babel-preset": "^0.86.0",
@@ -176,7 +176,7 @@
176
176
  },
177
177
  "peerDependencies": {
178
178
  "@expo/vector-icons": "^15.0.3",
179
- "@oxyhq/bloom": ">=0.16.0",
179
+ "@oxyhq/bloom": ">=0.43.0",
180
180
  "@oxyhq/core": "^12.9.0",
181
181
  "@react-native-async-storage/async-storage": "^2.0.0",
182
182
  "@react-native-community/netinfo": "^11.4.1",
@@ -374,11 +374,43 @@ describe('patchCachedUserRelationship', () => {
374
374
  'viewer-1',
375
375
  );
376
376
 
377
- patchCachedUserRelationship(qc, 'u1', true, 'viewer-1');
377
+ patchCachedUserRelationship(qc, 'u1', true);
378
378
 
379
379
  expect(readByUsername(qc, 'alice', 'viewer-1')?.relationship).toEqual({
380
380
  isFollowing: true,
381
381
  followsYou: true,
382
382
  });
383
383
  });
384
+
385
+ it('never writes relationship onto the viewer-independent by-id key', () => {
386
+ const qc = makeClient();
387
+ upsertCachedUser(
388
+ qc,
389
+ {
390
+ id: 'u1',
391
+ username: 'alice',
392
+ relationship: { isFollowing: false, followsYou: true },
393
+ },
394
+ 'viewer-1',
395
+ );
396
+
397
+ patchCachedUserRelationship(qc, 'u1', true);
398
+
399
+ expect(readById(qc, 'u1')?.relationship).toBeUndefined();
400
+ });
401
+
402
+ it('updates viewer-scoped detailForViewer keys', () => {
403
+ const qc = makeClient();
404
+ qc.setQueryData(queryKeys.users.detailForViewer('u1', 'viewer-1'), {
405
+ id: 'u1',
406
+ username: 'alice',
407
+ relationship: { isFollowing: false, followsYou: false },
408
+ });
409
+
410
+ patchCachedUserRelationship(qc, 'u1', true);
411
+
412
+ expect(
413
+ qc.getQueryData<CacheableUser>(queryKeys.users.detailForViewer('u1', 'viewer-1'))?.relationship,
414
+ ).toEqual({ isFollowing: true, followsYou: false });
415
+ });
384
416
  });
@@ -16,7 +16,12 @@ export function patchCachedUserRelationship(
16
16
  isFollowing: boolean,
17
17
  ): void {
18
18
  queryClient.setQueriesData<CachedUser>(
19
- { queryKey: queryKeys.users.details() },
19
+ {
20
+ queryKey: queryKeys.users.details(),
21
+ // Viewer-relative `relationship` must never be written on the viewer-
22
+ // independent by-id key — only viewer-scoped detail / by-username keys.
23
+ predicate: (query) => (query.queryKey as readonly unknown[]).includes('viewer'),
24
+ },
20
25
  (existing) => {
21
26
  if (!existing || existing.id !== targetUserId) return existing;
22
27
  return {
@@ -206,6 +206,8 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
206
206
  return sorted;
207
207
  }, [files, searchQuery, viewMode, sortBy, sortOrder]);
208
208
  const [photoDimensions, setPhotoDimensions] = useState<{ [key: string]: { width: number, height: number } }>({});
209
+ const photoDimensionsRef = useRef(photoDimensions);
210
+ photoDimensionsRef.current = photoDimensions;
209
211
  const [loadingDimensions, setLoadingDimensions] = useState(false);
210
212
  // Selection state
211
213
  const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set(initialSelectedIds));
@@ -441,11 +443,10 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
441
443
  if (photos.length === 0) return;
442
444
 
443
445
  setLoadingDimensions(true);
444
- const newDimensions: { [key: string]: { width: number, height: number } } = { ...photoDimensions };
445
- let hasNewDimensions = false;
446
446
 
447
- // Only load dimensions for photos we don't have yet
448
- const photosToLoad = photos.filter(photo => !newDimensions[photo.id]);
447
+ // Snapshot ids missing from the cache at kick-off; concurrent runs may
448
+ // overlap but each merge is applied via a functional updater.
449
+ const photosToLoad = photos.filter((photo) => !photoDimensionsRef.current[photo.id]);
449
450
 
450
451
  if (photosToLoad.length === 0) {
451
452
  setLoadingDimensions(false);
@@ -453,6 +454,8 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
453
454
  }
454
455
 
455
456
  try {
457
+ const measured: { [key: string]: { width: number; height: number } } = {};
458
+
456
459
  await Promise.all(
457
460
  photosToLoad.map(async (photo) => {
458
461
  try {
@@ -470,35 +473,32 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
470
473
  Image.getSize(
471
474
  downloadUrl,
472
475
  (width: number, height: number) => {
473
- newDimensions[photo.id] = { width, height };
474
- hasNewDimensions = true;
476
+ measured[photo.id] = { width, height };
475
477
  resolve();
476
478
  },
477
479
  () => {
478
480
  // Fallback dimensions
479
- newDimensions[photo.id] = { width: 1, height: 1 };
480
- hasNewDimensions = true;
481
+ measured[photo.id] = { width: 1, height: 1 };
481
482
  resolve();
482
483
  }
483
484
  );
484
485
  });
485
486
  } catch (error) {
486
487
  // Fallback dimensions for any errors
487
- newDimensions[photo.id] = { width: 1, height: 1 };
488
- hasNewDimensions = true;
488
+ measured[photo.id] = { width: 1, height: 1 };
489
489
  }
490
490
  })
491
491
  );
492
492
 
493
- if (hasNewDimensions) {
494
- setPhotoDimensions(newDimensions);
493
+ if (Object.keys(measured).length > 0) {
494
+ setPhotoDimensions((prev) => ({ ...prev, ...measured }));
495
495
  }
496
496
  } catch (error) {
497
497
  // Photo dimensions loading failed, continue without dimensions
498
498
  } finally {
499
499
  setLoadingDimensions(false);
500
500
  }
501
- }, [thumbSourceFor, photoDimensions]);
501
+ }, [thumbSourceFor]);
502
502
 
503
503
  // Re-measure photo dimensions when their private-safe URLs resolve. The
504
504
  // justified grid's own trigger fires on the photo SET, not on URL
@@ -571,8 +571,14 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
571
571
 
572
572
  return (
573
573
  // Measured wrapper: `style`-only (no className) so RN-Web fires onLayout.
574
- <View style={{ flex: 1 }} onLayout={onRootLayout}>
575
- <View className="flex-1 bg-black">
574
+ // `minHeight: 0` down the whole flex chain: the sheet is `scrollable=false`
575
+ // (the FlatList owns scrolling) and clamped by its `maxHeight`, but on web a
576
+ // flex child defaults to `min-height: auto` and grows to its content, so the
577
+ // list overflows the clamp and is clipped (renders "half", no scroll). Letting
578
+ // each flex link shrink to the clamped height gives the FlatList a bounded
579
+ // scroll area. Native (Yoga) already defaults min to 0, so this is web-only.
580
+ <View style={{ flex: 1, minHeight: 0 }} onLayout={onRootLayout}>
581
+ <View className="flex-1 bg-black" style={{ minHeight: 0 }}>
576
582
  {/* Photo grid (renders behind translucent header) */}
577
583
  {isEmpty ? (
578
584
  <View className="flex-1 items-center justify-center px-space-32 pt-[88px]">
@@ -616,6 +622,7 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
616
622
  keyExtractor={keyExtractor}
617
623
  numColumns={columns}
618
624
  className="flex-1"
625
+ style={{ minHeight: 0 }}
619
626
  contentContainerClassName="pt-[88px] pb-space-24"
620
627
  showsVerticalScrollIndicator={false}
621
628
  refreshControl={
@@ -630,7 +637,7 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
630
637
  onEndReached={handleEndReached}
631
638
  onEndReachedThreshold={0.4}
632
639
  ListFooterComponent={listFooter}
633
- removeClippedSubviews
640
+ removeClippedSubviews={Platform.OS !== 'web'}
634
641
  initialNumToRender={Math.max(12, columns * 6)}
635
642
  windowSize={9}
636
643
  />