@oxyhq/services 22.8.0 → 22.8.2

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 (30) hide show
  1. package/lib/commonjs/ui/hooks/queries/userCacheRelationship.js +28 -0
  2. package/lib/commonjs/ui/hooks/queries/userCacheRelationship.js.map +1 -0
  3. package/lib/commonjs/ui/hooks/useFollow.js +6 -2
  4. package/lib/commonjs/ui/hooks/useFollow.js.map +1 -1
  5. package/lib/commonjs/ui/screens/ProfileScreen.js +1 -1
  6. package/lib/commonjs/ui/screens/ProfileScreen.js.map +1 -1
  7. package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js +118 -80
  8. package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
  9. package/lib/module/ui/hooks/queries/userCacheRelationship.js +24 -0
  10. package/lib/module/ui/hooks/queries/userCacheRelationship.js.map +1 -0
  11. package/lib/module/ui/hooks/useFollow.js +7 -3
  12. package/lib/module/ui/hooks/useFollow.js.map +1 -1
  13. package/lib/module/ui/screens/ProfileScreen.js +1 -1
  14. package/lib/module/ui/screens/ProfileScreen.js.map +1 -1
  15. package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js +119 -81
  16. package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
  17. package/lib/typescript/commonjs/ui/hooks/queries/userCacheRelationship.d.ts +9 -0
  18. package/lib/typescript/commonjs/ui/hooks/queries/userCacheRelationship.d.ts.map +1 -0
  19. package/lib/typescript/commonjs/ui/hooks/useFollow.d.ts.map +1 -1
  20. package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
  21. package/lib/typescript/module/ui/hooks/queries/userCacheRelationship.d.ts +9 -0
  22. package/lib/typescript/module/ui/hooks/queries/userCacheRelationship.d.ts.map +1 -0
  23. package/lib/typescript/module/ui/hooks/useFollow.d.ts.map +1 -1
  24. package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
  25. package/package.json +2 -2
  26. package/src/ui/hooks/queries/__tests__/userCache.test.ts +23 -0
  27. package/src/ui/hooks/queries/userCacheRelationship.ts +28 -0
  28. package/src/ui/hooks/useFollow.ts +7 -3
  29. package/src/ui/screens/ProfileScreen.tsx +1 -1
  30. package/src/ui/screens/fileManagement/PhotoPickerSection.tsx +100 -70
@@ -1 +1 @@
1
- {"version":3,"file":"PhotoPickerSection.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/fileManagement/PhotoPickerSection.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAsBjF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA6FhD;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACjC,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACzB,WAAW,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,OAAO,CAAC;IACrB,UAAU,EAAE,OAAO,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,cAAc,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC1D,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;IACrB,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,MAAM,GAAG,SAAS,CAAC;IACxD,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IAC7C,cAAc,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IAC9C,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,KAAK,MAAM,CAAC;CACtE;AAoOD,QAAA,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CAwUnD,CAAC;AAEF,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"PhotoPickerSection.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/fileManagement/PhotoPickerSection.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAejF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA6FhD;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACjC,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACzB,WAAW,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,OAAO,CAAC;IACrB,UAAU,EAAE,OAAO,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,cAAc,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC1D,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;IACrB,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,MAAM,GAAG,SAAS,CAAC;IACxD,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IAC7C,cAAc,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IAC9C,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,KAAK,MAAM,CAAC;CACtE;AAyQD,QAAA,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CAwUnD,CAAC;AAEF,eAAe,eAAe,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/services",
3
- "version": "22.8.0",
3
+ "version": "22.8.2",
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.0",
139
+ "@oxyhq/core": "12.9.1",
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",
@@ -14,6 +14,7 @@
14
14
 
15
15
  import { QueryClient } from '@tanstack/react-query';
16
16
  import { upsertCachedUser, upsertCachedUsers } from '../userCache';
17
+ import { patchCachedUserRelationship } from '../userCacheRelationship';
17
18
  import type { CacheableUser } from '../userCache';
18
19
  import { queryKeys } from '../queryKeys';
19
20
  import { useAuthStore } from '../../../stores/authStore';
@@ -359,3 +360,25 @@ describe('upsertCachedUsers — batch', () => {
359
360
  expect(qc.getQueryCache().getAll()).toHaveLength(0);
360
361
  });
361
362
  });
363
+
364
+ describe('patchCachedUserRelationship', () => {
365
+ it('updates relationship.isFollowing on the viewer-scoped by-username entry', () => {
366
+ const qc = makeClient();
367
+ upsertCachedUser(
368
+ qc,
369
+ {
370
+ id: 'u1',
371
+ username: 'alice',
372
+ relationship: { isFollowing: false, followsYou: true },
373
+ },
374
+ 'viewer-1',
375
+ );
376
+
377
+ patchCachedUserRelationship(qc, 'u1', true, 'viewer-1');
378
+
379
+ expect(readByUsername(qc, 'alice', 'viewer-1')?.relationship).toEqual({
380
+ isFollowing: true,
381
+ followsYou: true,
382
+ });
383
+ });
384
+ });
@@ -0,0 +1,28 @@
1
+ import type { QueryClient } from '@tanstack/react-query';
2
+ import { queryKeys } from './queryKeys';
3
+ import type { CacheableUser } from './userCache';
4
+
5
+ type CachedUser = CacheableUser & { id: string };
6
+
7
+ /**
8
+ * Patch `relationship.isFollowing` on every cached single-profile entry for
9
+ * `targetUserId` (by-id and viewer-scoped by-username keys) after a follow
10
+ * toggle. Lives outside `userCache.ts` so hooks like `useFollow` can import it
11
+ * without pulling the auth store (which `userCache` uses for viewer defaults).
12
+ */
13
+ export function patchCachedUserRelationship(
14
+ queryClient: QueryClient,
15
+ targetUserId: string,
16
+ isFollowing: boolean,
17
+ ): void {
18
+ queryClient.setQueriesData<CachedUser>(
19
+ { queryKey: queryKeys.users.details() },
20
+ (existing) => {
21
+ if (!existing || existing.id !== targetUserId) return existing;
22
+ return {
23
+ ...existing,
24
+ relationship: { ...existing.relationship, isFollowing },
25
+ };
26
+ },
27
+ );
28
+ }
@@ -1,10 +1,11 @@
1
1
  import { useCallback, useMemo, useState } from 'react';
2
- import { useQuery } from '@tanstack/react-query';
2
+ import { useQuery, useQueryClient } from '@tanstack/react-query';
3
3
  import { useFollowStore } from '../stores/followStore';
4
4
  import { useOxy } from '../context/OxyContext';
5
5
  import type { OxyServices, BulkFollowResult, BulkUnfollowResult } from '@oxyhq/core';
6
6
  import { useShallow } from 'zustand/react/shallow';
7
7
  import { queryKeys } from './queries/queryKeys';
8
+ import { patchCachedUserRelationship } from './queries/userCacheRelationship';
8
9
 
9
10
  /**
10
11
  * useFollow — Hook for follow state management.
@@ -20,6 +21,7 @@ import { queryKeys } from './queries/queryKeys';
20
21
  * them in selectors would cause unnecessary selector recalculations).
21
22
  */
22
23
  export const useFollow = (userId?: string | string[]) => {
24
+ const queryClient = useQueryClient();
23
25
  const { oxyServices, canUsePrivateApi } = useOxy();
24
26
  const userIds = useMemo(() => (Array.isArray(userId) ? userId : userId ? [userId] : []), [userId]);
25
27
  const isSingleUser = typeof userId === 'string';
@@ -100,7 +102,8 @@ export const useFollow = (userId?: string | string[]) => {
100
102
  if (!canUsePrivateApi) throw new Error('Authentication is required to follow users');
101
103
  const currentlyFollowing = useFollowStore.getState().followingUsers[userId] ?? false;
102
104
  await useFollowStore.getState().toggleFollowUser(userId, oxyServices, currentlyFollowing);
103
- }, [isSingleUser, userId, canUsePrivateApi, oxyServices]);
105
+ patchCachedUserRelationship(queryClient, userId, !currentlyFollowing);
106
+ }, [isSingleUser, userId, canUsePrivateApi, oxyServices, queryClient]);
104
107
 
105
108
  const setFollowStatus = useCallback((following: boolean) => {
106
109
  if (!isSingleUser || !userId) throw new Error('setFollowStatus is only available for single user mode');
@@ -162,7 +165,8 @@ export const useFollow = (userId?: string | string[]) => {
162
165
  if (!canUsePrivateApi) throw new Error('Authentication is required to follow users');
163
166
  const currentState = useFollowStore.getState().followingUsers[targetUserId] ?? false;
164
167
  await useFollowStore.getState().toggleFollowUser(targetUserId, oxyServices, currentState);
165
- }, [canUsePrivateApi, oxyServices]);
168
+ patchCachedUserRelationship(queryClient, targetUserId, !currentState);
169
+ }, [canUsePrivateApi, oxyServices, queryClient]);
166
170
 
167
171
  const setFollowStatusForUser = useCallback((targetUserId: string, following: boolean) => {
168
172
  useFollowStore.getState().setFollowingStatus(targetUserId, following);
@@ -138,7 +138,7 @@ const ProfileScreen: React.FC<ProfileScreenProps> = ({ userId, username, theme,
138
138
  setError(errorMessage);
139
139
  })
140
140
  .finally(() => setIsLoading(false));
141
- }, [userId]);
141
+ }, [userId, currentUser?.id, oxyServices]);
142
142
 
143
143
  if (isLoading) {
144
144
  return (
@@ -7,18 +7,11 @@ import {
7
7
  RefreshControl,
8
8
  FlatList,
9
9
  Platform,
10
+ Animated,
10
11
  useWindowDimensions,
11
12
  type LayoutChangeEvent,
12
13
  } from 'react-native';
13
14
  import { Image as ExpoImage } from 'expo-image';
14
- import Reanimated, {
15
- FadeIn,
16
- useAnimatedStyle,
17
- useSharedValue,
18
- withSequence,
19
- withSpring,
20
- withTiming,
21
- } from 'react-native-reanimated';
22
15
  import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
23
16
  import type { FileMetadata } from '@oxyhq/core';
24
17
  import { computePhotoGridLayout } from './photoGridLayout';
@@ -144,6 +137,33 @@ export interface PhotoPickerViewProps {
144
137
  t: (key: string, vars?: Record<string, string | number>) => string;
145
138
  }
146
139
 
140
+ /**
141
+ * Photo-cell animation tuning. The animated cell uses RN's own `Animated`
142
+ * (NOT Reanimated): its entrance is a staggered opacity fade — replacing the
143
+ * Reanimated `entering={FadeIn}` LAYOUT animation, which cannot load on the
144
+ * RN-Web build (no `react-native-worklets` babel plugin) and only warns +
145
+ * no-ops there. RN `Animated` runs cross-platform: native-driven on device,
146
+ * JS-driven on web (`useNativeDriver: false`), so the fade actually plays on
147
+ * web instead of being skipped.
148
+ */
149
+ const STAGGER_PER_CELL_MS = 15;
150
+ const MAX_TOTAL_STAGGER_MS = 800;
151
+ const ENTRANCE_DURATION_MS = 200;
152
+ /** Selection-ring pulse: a quick scale bump that springs back to rest. */
153
+ const RING_PULSE_PEAK = 1.05;
154
+ const RING_PULSE_UP_MS = 110;
155
+ const RING_SPRING_FRICTION = 9;
156
+ const RING_SPRING_TENSION = 120;
157
+ /**
158
+ * Inline mirror of the `rounded-radius-8 border-[3px]` classes the static ring
159
+ * carries. The animated ring MUST use inline `style` (its animated transform
160
+ * has to be inline, and NativeWind `className` interop is not guaranteed on an
161
+ * `Animated.View` under the RN-Web build), so these keep it pixel-identical to
162
+ * the static ring.
163
+ */
164
+ const CELL_CORNER_RADIUS = 8; // `rounded-radius-8`
165
+ const SELECTION_RING_WIDTH = 3; // `border-[3px]`
166
+
147
167
  /**
148
168
  * A single photo cell. Memoized so re-renders during selection only touch
149
169
  * affected cells — selection of one photo must not redraw the whole grid.
@@ -153,7 +173,7 @@ export interface PhotoPickerViewProps {
153
173
  */
154
174
  /**
155
175
  * Shared cell chrome (image, dim, badge). Ring border is injected by the
156
- * static vs animated wrappers so web never touches Reanimated worklets.
176
+ * static vs animated wrappers.
157
177
  */
158
178
  function PhotoPickerCellContent(props: {
159
179
  photo: FileMetadata;
@@ -263,82 +283,89 @@ const PhotoPickerCellStatic = React.memo(function PhotoPickerCellStatic(props: P
263
283
  const PhotoPickerCellAnimated = React.memo(function PhotoPickerCellAnimated(props: PhotoPickerCellProps) {
264
284
  const {
265
285
  photo, size, marginRight, marginBottom, isSelected, selectionIndex,
266
- dim, primaryColor, thumbUrl, enterIndex, reduceMotion, onPress,
267
- onLongPress, a11yLabel,
286
+ dim, primaryColor, thumbUrl, enterIndex, onPress, onLongPress, a11yLabel,
268
287
  } = props;
269
288
 
270
- const STAGGER_PER_CELL_MS = 15;
271
- const MAX_TOTAL_STAGGER_MS = 800;
272
289
  const delay = Math.min(enterIndex * STAGGER_PER_CELL_MS, MAX_TOTAL_STAGGER_MS);
273
290
 
274
- const ringScale = useSharedValue(1);
291
+ // Entrance: a per-cell opacity fade, staggered by the cell's grid index.
292
+ // `useNativeDriver` is only available on native — on web the animation is
293
+ // JS-driven, which still plays the fade (unlike Reanimated layout entering,
294
+ // which no-ops on the plugin-less RN-Web build).
295
+ const opacity = useRef(new Animated.Value(0)).current;
296
+ useEffect(() => {
297
+ const entrance = Animated.timing(opacity, {
298
+ toValue: 1,
299
+ duration: ENTRANCE_DURATION_MS,
300
+ delay,
301
+ useNativeDriver: Platform.OS !== 'web',
302
+ });
303
+ entrance.start();
304
+ return () => entrance.stop();
305
+ }, [opacity, delay]);
306
+
307
+ // Ring pulse: a quick scale bump each time a cell transitions INTO the
308
+ // selected state, springing back to rest.
309
+ const ringScale = useRef(new Animated.Value(1)).current;
275
310
  const prevSelected = useRef(isSelected);
276
311
  useEffect(() => {
277
312
  if (prevSelected.current === isSelected) return;
278
313
  prevSelected.current = isSelected;
279
314
  if (!isSelected) {
280
- ringScale.value = 1;
315
+ ringScale.setValue(1);
281
316
  return;
282
317
  }
283
- if (reduceMotion) {
284
- ringScale.value = 1;
285
- return;
286
- }
287
- ringScale.value = withSequence(
288
- withTiming(1.05, { duration: 110 }),
289
- withSpring(1, { damping: 14, stiffness: 200 }),
290
- );
291
- }, [isSelected, reduceMotion, ringScale]);
292
-
293
- const ringAnimatedStyle = useAnimatedStyle(() => ({
294
- transform: [{ scale: ringScale.value }],
295
- }));
296
-
297
- const cellWrapperStyle = {
298
- width: size,
299
- height: size,
300
- marginRight,
301
- marginBottom,
302
- };
303
-
318
+ const pulse = Animated.sequence([
319
+ Animated.timing(ringScale, {
320
+ toValue: RING_PULSE_PEAK,
321
+ duration: RING_PULSE_UP_MS,
322
+ useNativeDriver: Platform.OS !== 'web',
323
+ }),
324
+ Animated.spring(ringScale, {
325
+ toValue: 1,
326
+ friction: RING_SPRING_FRICTION,
327
+ tension: RING_SPRING_TENSION,
328
+ useNativeDriver: Platform.OS !== 'web',
329
+ }),
330
+ ]);
331
+ pulse.start();
332
+ return () => pulse.stop();
333
+ }, [isSelected, ringScale]);
334
+
335
+ // The animated ring reads `ringScale`; its border/radius/position are inline
336
+ // `style` because NativeWind `className` interop is not guaranteed on an
337
+ // `Animated.View` under the RN-Web build.
304
338
  const ring = isSelected ? (
305
- <Reanimated.View
339
+ <Animated.View
306
340
  pointerEvents="none"
307
- className="absolute inset-0 rounded-radius-8 border-[3px]"
308
- style={[{ borderColor: primaryColor }, ringAnimatedStyle]}
341
+ style={{
342
+ position: 'absolute',
343
+ top: 0,
344
+ right: 0,
345
+ bottom: 0,
346
+ left: 0,
347
+ borderRadius: CELL_CORNER_RADIUS,
348
+ borderWidth: SELECTION_RING_WIDTH,
349
+ borderColor: primaryColor,
350
+ transform: [{ scale: ringScale }],
351
+ }}
309
352
  />
310
353
  ) : null;
311
354
 
312
- if (reduceMotion) {
313
- return (
314
- <TouchableOpacity
315
- activeOpacity={0.85}
316
- onPress={onPress}
317
- onLongPress={onLongPress}
318
- className="relative"
319
- style={cellWrapperStyle}
320
- accessibilityRole="button"
321
- accessibilityLabel={a11yLabel}
322
- accessibilityState={{ selected: isSelected }}
323
- >
324
- <PhotoPickerCellContent
325
- photo={photo}
326
- dim={dim}
327
- isSelected={isSelected}
328
- selectionIndex={selectionIndex}
329
- primaryColor={primaryColor}
330
- thumbUrl={thumbUrl}
331
- ring={ring}
332
- />
333
- </TouchableOpacity>
334
- );
335
- }
336
-
337
355
  return (
338
- <Reanimated.View
339
- entering={FadeIn.delay(delay).duration(200)}
340
- className="relative"
341
- style={cellWrapperStyle}
356
+ // Animated wrapper carries the entrance opacity + layout box as inline
357
+ // `style` (animated values must be inline, and className interop on
358
+ // Animated.View is not guaranteed on RN-Web). `className` stays on the
359
+ // non-animated TouchableOpacity + content below.
360
+ <Animated.View
361
+ style={{
362
+ width: size,
363
+ height: size,
364
+ marginRight,
365
+ marginBottom,
366
+ position: 'relative',
367
+ opacity,
368
+ }}
342
369
  >
343
370
  <TouchableOpacity
344
371
  activeOpacity={0.85}
@@ -359,12 +386,15 @@ const PhotoPickerCellAnimated = React.memo(function PhotoPickerCellAnimated(prop
359
386
  ring={ring}
360
387
  />
361
388
  </TouchableOpacity>
362
- </Reanimated.View>
389
+ </Animated.View>
363
390
  );
364
391
  });
365
392
 
366
393
  const PhotoPickerCell = React.memo(function PhotoPickerCell(props: PhotoPickerCellProps) {
367
- if (Platform.OS === 'web') {
394
+ // reduceMotion the static (unanimated) cell on every platform. Otherwise
395
+ // the RN-`Animated` cell, which now plays on web too (no Reanimated layout
396
+ // entering, so no plugin-less RN-Web warning / no-op).
397
+ if (props.reduceMotion) {
368
398
  return <PhotoPickerCellStatic {...props} />;
369
399
  }
370
400
  return <PhotoPickerCellAnimated {...props} />;