@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.
- package/lib/commonjs/ui/hooks/queries/userCacheRelationship.js +28 -0
- package/lib/commonjs/ui/hooks/queries/userCacheRelationship.js.map +1 -0
- package/lib/commonjs/ui/hooks/useFollow.js +6 -2
- package/lib/commonjs/ui/hooks/useFollow.js.map +1 -1
- package/lib/commonjs/ui/screens/ProfileScreen.js +1 -1
- package/lib/commonjs/ui/screens/ProfileScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js +118 -80
- package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
- package/lib/module/ui/hooks/queries/userCacheRelationship.js +24 -0
- package/lib/module/ui/hooks/queries/userCacheRelationship.js.map +1 -0
- package/lib/module/ui/hooks/useFollow.js +7 -3
- package/lib/module/ui/hooks/useFollow.js.map +1 -1
- package/lib/module/ui/screens/ProfileScreen.js +1 -1
- package/lib/module/ui/screens/ProfileScreen.js.map +1 -1
- package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js +119 -81
- package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/queries/userCacheRelationship.d.ts +9 -0
- package/lib/typescript/commonjs/ui/hooks/queries/userCacheRelationship.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/hooks/useFollow.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/queries/userCacheRelationship.d.ts +9 -0
- package/lib/typescript/module/ui/hooks/queries/userCacheRelationship.d.ts.map +1 -0
- package/lib/typescript/module/ui/hooks/useFollow.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/ui/hooks/queries/__tests__/userCache.test.ts +23 -0
- package/src/ui/hooks/queries/userCacheRelationship.ts +28 -0
- package/src/ui/hooks/useFollow.ts +7 -3
- package/src/ui/screens/ProfileScreen.tsx +1 -1
- 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;
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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.
|
|
315
|
+
ringScale.setValue(1);
|
|
281
316
|
return;
|
|
282
317
|
}
|
|
283
|
-
|
|
284
|
-
ringScale
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
<
|
|
339
|
+
<Animated.View
|
|
306
340
|
pointerEvents="none"
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
</
|
|
389
|
+
</Animated.View>
|
|
363
390
|
);
|
|
364
391
|
});
|
|
365
392
|
|
|
366
393
|
const PhotoPickerCell = React.memo(function PhotoPickerCell(props: PhotoPickerCellProps) {
|
|
367
|
-
|
|
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} />;
|