@oxyhq/services 5.4.4 → 5.4.5
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/core/index.js +22 -3
- package/lib/commonjs/core/index.js.map +1 -1
- package/lib/commonjs/index.js +50 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/FollowButton.js +79 -31
- package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
- package/lib/commonjs/ui/components/OxySignInButton.js +2 -2
- package/lib/commonjs/ui/components/OxySignInButton.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +11 -1
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/hooks/index.js +13 -0
- package/lib/commonjs/ui/hooks/index.js.map +1 -0
- package/lib/commonjs/ui/hooks/useFollow.js +184 -0
- package/lib/commonjs/ui/hooks/useFollow.js.map +1 -0
- package/lib/commonjs/ui/index.js +25 -1
- package/lib/commonjs/ui/index.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountCenterScreen.js +4 -3
- package/lib/commonjs/ui/screens/AccountCenterScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountOverviewScreen.js +7 -6
- package/lib/commonjs/ui/screens/AccountOverviewScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js +3 -2
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountSwitcherScreen.js +3 -2
- package/lib/commonjs/ui/screens/AccountSwitcherScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/AppInfoScreen.js +6 -8
- package/lib/commonjs/ui/screens/AppInfoScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/SignInScreen.js +1 -1
- package/lib/commonjs/ui/screens/SignInScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/karma/KarmaCenterScreen.js +5 -4
- package/lib/commonjs/ui/screens/karma/KarmaCenterScreen.js.map +1 -1
- package/lib/commonjs/ui/store/index.js +219 -4
- package/lib/commonjs/ui/store/index.js.map +1 -1
- package/lib/module/core/index.js +22 -3
- package/lib/module/core/index.js.map +1 -1
- package/lib/module/index.js +6 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/FollowButton.js +80 -32
- package/lib/module/ui/components/FollowButton.js.map +1 -1
- package/lib/module/ui/components/OxySignInButton.js +2 -2
- package/lib/module/ui/components/OxySignInButton.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +11 -1
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/hooks/index.js +4 -0
- package/lib/module/ui/hooks/index.js.map +1 -0
- package/lib/module/ui/hooks/useFollow.js +180 -0
- package/lib/module/ui/hooks/useFollow.js.map +1 -0
- package/lib/module/ui/index.js +9 -0
- package/lib/module/ui/index.js.map +1 -1
- package/lib/module/ui/screens/AccountCenterScreen.js +4 -3
- package/lib/module/ui/screens/AccountCenterScreen.js.map +1 -1
- package/lib/module/ui/screens/AccountOverviewScreen.js +7 -6
- package/lib/module/ui/screens/AccountOverviewScreen.js.map +1 -1
- package/lib/module/ui/screens/AccountSettingsScreen.js +3 -2
- package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/module/ui/screens/AccountSwitcherScreen.js +3 -2
- package/lib/module/ui/screens/AccountSwitcherScreen.js.map +1 -1
- package/lib/module/ui/screens/AppInfoScreen.js +6 -8
- package/lib/module/ui/screens/AppInfoScreen.js.map +1 -1
- package/lib/module/ui/screens/SignInScreen.js +1 -1
- package/lib/module/ui/screens/SignInScreen.js.map +1 -1
- package/lib/module/ui/screens/karma/KarmaCenterScreen.js +5 -4
- package/lib/module/ui/screens/karma/KarmaCenterScreen.js.map +1 -1
- package/lib/module/ui/store/index.js +215 -4
- package/lib/module/ui/store/index.js.map +1 -1
- package/lib/typescript/core/index.d.ts +16 -3
- package/lib/typescript/core/index.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +4 -2
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/ui/components/FollowButton.d.ts +1 -0
- package/lib/typescript/ui/components/FollowButton.d.ts.map +1 -1
- package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/index.d.ts +2 -0
- package/lib/typescript/ui/hooks/index.d.ts.map +1 -0
- package/lib/typescript/ui/hooks/useFollow.d.ts +43 -0
- package/lib/typescript/ui/hooks/useFollow.d.ts.map +1 -0
- package/lib/typescript/ui/index.d.ts +3 -0
- package/lib/typescript/ui/index.d.ts.map +1 -1
- package/lib/typescript/ui/screens/AccountCenterScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/AccountSwitcherScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/AppInfoScreen.d.ts.map +1 -1
- package/lib/typescript/ui/store/index.d.ts +47 -0
- package/lib/typescript/ui/store/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/index.ts +88 -3
- package/src/index.ts +19 -3
- package/src/ui/components/FollowButton.tsx +114 -56
- package/src/ui/components/OxySignInButton.tsx +2 -2
- package/src/ui/context/OxyContext.tsx +12 -2
- package/src/ui/hooks/index.ts +1 -0
- package/src/ui/hooks/useFollow.ts +173 -0
- package/src/ui/index.ts +9 -0
- package/src/ui/screens/AccountCenterScreen.tsx +17 -15
- package/src/ui/screens/AccountOverviewScreen.tsx +25 -25
- package/src/ui/screens/AccountSettingsScreen.tsx +30 -30
- package/src/ui/screens/AccountSwitcherScreen.tsx +34 -33
- package/src/ui/screens/AppInfoScreen.tsx +153 -155
- package/src/ui/screens/SignInScreen.tsx +2 -2
- package/src/ui/screens/karma/KarmaCenterScreen.tsx +4 -4
- package/src/ui/store/index.ts +197 -3
|
@@ -1,25 +1,33 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import {
|
|
3
|
-
TouchableOpacity,
|
|
4
|
-
Text,
|
|
5
|
-
StyleSheet,
|
|
6
|
-
ViewStyle,
|
|
7
|
-
TextStyle,
|
|
8
|
-
StyleProp,
|
|
1
|
+
import React, { useEffect, useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
TouchableOpacity,
|
|
4
|
+
Text,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
ViewStyle,
|
|
7
|
+
TextStyle,
|
|
8
|
+
StyleProp,
|
|
9
9
|
Platform,
|
|
10
10
|
ActivityIndicator
|
|
11
11
|
} from 'react-native';
|
|
12
|
-
import Animated, {
|
|
13
|
-
useSharedValue,
|
|
14
|
-
useAnimatedStyle,
|
|
15
|
-
withSpring,
|
|
12
|
+
import Animated, {
|
|
13
|
+
useSharedValue,
|
|
14
|
+
useAnimatedStyle,
|
|
15
|
+
withSpring,
|
|
16
16
|
interpolateColor,
|
|
17
|
-
Easing,
|
|
18
|
-
withTiming
|
|
17
|
+
Easing,
|
|
18
|
+
withTiming
|
|
19
19
|
} from 'react-native-reanimated';
|
|
20
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
20
21
|
import { useOxy } from '../context/OxyContext';
|
|
21
22
|
import { fontFamilies } from '../styles/fonts';
|
|
22
23
|
import { toast } from '../../lib/sonner';
|
|
24
|
+
import {
|
|
25
|
+
toggleFollowUser,
|
|
26
|
+
setFollowingStatus,
|
|
27
|
+
clearFollowError,
|
|
28
|
+
fetchFollowStatus
|
|
29
|
+
} from '../store';
|
|
30
|
+
import type { RootState, AppDispatch } from '../store';
|
|
23
31
|
|
|
24
32
|
export interface FollowButtonProps {
|
|
25
33
|
/**
|
|
@@ -59,7 +67,7 @@ export interface FollowButtonProps {
|
|
|
59
67
|
* @default false
|
|
60
68
|
*/
|
|
61
69
|
disabled?: boolean;
|
|
62
|
-
|
|
70
|
+
|
|
63
71
|
/**
|
|
64
72
|
* Whether to show loading indicator during API calls
|
|
65
73
|
* @default true
|
|
@@ -82,6 +90,7 @@ export interface FollowButtonProps {
|
|
|
82
90
|
|
|
83
91
|
/**
|
|
84
92
|
* An animated follow button with interactive state changes and preventDefault support
|
|
93
|
+
* Uses Redux for state management to ensure all buttons with the same user ID stay synchronized
|
|
85
94
|
*
|
|
86
95
|
* @example
|
|
87
96
|
* ```tsx
|
|
@@ -130,36 +139,74 @@ const FollowButton: React.FC<FollowButtonProps> = ({
|
|
|
130
139
|
preventParentActions = true,
|
|
131
140
|
onPress,
|
|
132
141
|
}) => {
|
|
142
|
+
const dispatch = useDispatch();
|
|
133
143
|
const { oxyServices, isAuthenticated } = useOxy();
|
|
134
|
-
|
|
135
|
-
|
|
144
|
+
|
|
145
|
+
// Optimized single selector to prevent multiple re-renders
|
|
146
|
+
const followState = useSelector((state: RootState) => ({
|
|
147
|
+
isFollowing: state.follow.followingUsers[userId] ?? initiallyFollowing ?? false,
|
|
148
|
+
isLoading: state.follow.loadingUsers[userId] ?? false,
|
|
149
|
+
error: state.follow.errors[userId]
|
|
150
|
+
}));
|
|
151
|
+
|
|
152
|
+
// Whether the follow status has been loaded from the store
|
|
153
|
+
const isStatusKnown = useSelector((state: RootState) =>
|
|
154
|
+
Object.prototype.hasOwnProperty.call(state.follow.followingUsers, userId)
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const { isFollowing, isLoading, error } = followState;
|
|
136
158
|
|
|
137
159
|
// Animation values
|
|
138
|
-
const animationProgress = useSharedValue(
|
|
160
|
+
const animationProgress = useSharedValue(isFollowing ? 1 : 0);
|
|
139
161
|
const scale = useSharedValue(1);
|
|
140
|
-
|
|
162
|
+
|
|
163
|
+
// Initialize Redux state with initial value if not already set
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
if (userId && !isStatusKnown) {
|
|
166
|
+
// Set the initial state regardless of whether initiallyFollowing is defined
|
|
167
|
+
const initialState = initiallyFollowing ?? false;
|
|
168
|
+
dispatch(setFollowingStatus({ userId, isFollowing: initialState }));
|
|
169
|
+
}
|
|
170
|
+
}, [userId, initiallyFollowing, isStatusKnown, dispatch]);
|
|
171
|
+
|
|
172
|
+
// Fetch latest follow status from backend on mount if authenticated
|
|
173
|
+
// This runs separately and will overwrite the initial state with actual data
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
if (userId && isAuthenticated) {
|
|
176
|
+
dispatch(fetchFollowStatus({ userId, oxyServices }));
|
|
177
|
+
}
|
|
178
|
+
}, [userId, oxyServices, isAuthenticated, dispatch]);
|
|
179
|
+
|
|
141
180
|
// Update the animation value when isFollowing changes
|
|
142
181
|
useEffect(() => {
|
|
143
182
|
animationProgress.value = withTiming(isFollowing ? 1 : 0, {
|
|
144
183
|
duration: 300,
|
|
145
184
|
easing: Easing.bezier(0.25, 0.1, 0.25, 1),
|
|
146
185
|
});
|
|
147
|
-
}, [isFollowing
|
|
186
|
+
}, [isFollowing]); // Removed animationProgress from dependencies as it's stable
|
|
187
|
+
|
|
188
|
+
// Show error toast when error occurs
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (error) {
|
|
191
|
+
toast.error(error);
|
|
192
|
+
dispatch(clearFollowError(userId));
|
|
193
|
+
}
|
|
194
|
+
}, [error]); // Removed userId and dispatch to prevent unnecessary runs
|
|
148
195
|
|
|
149
|
-
// The button press handler with preventDefault support
|
|
150
|
-
const handlePress = async (event?: any) => {
|
|
196
|
+
// The button press handler with preventDefault support - memoized to prevent recreation
|
|
197
|
+
const handlePress = useCallback(async (event?: any) => {
|
|
151
198
|
// Prevent parent actions if enabled (e.g., if inside a link or pressable container)
|
|
152
199
|
if (preventParentActions && event) {
|
|
153
200
|
// For React Native Web compatibility
|
|
154
201
|
if (Platform.OS === 'web' && event.preventDefault) {
|
|
155
202
|
event.preventDefault();
|
|
156
203
|
}
|
|
157
|
-
|
|
204
|
+
|
|
158
205
|
// Stop event propagation to prevent parent TouchableOpacity/Pressable actions
|
|
159
206
|
if (event.stopPropagation) {
|
|
160
207
|
event.stopPropagation();
|
|
161
208
|
}
|
|
162
|
-
|
|
209
|
+
|
|
163
210
|
// For React Native, prevent gesture bubbling
|
|
164
211
|
if (event.nativeEvent && event.nativeEvent.stopPropagation) {
|
|
165
212
|
event.nativeEvent.stopPropagation();
|
|
@@ -172,47 +219,58 @@ const FollowButton: React.FC<FollowButtonProps> = ({
|
|
|
172
219
|
return;
|
|
173
220
|
}
|
|
174
221
|
|
|
175
|
-
if (disabled || isLoading
|
|
176
|
-
|
|
222
|
+
if (disabled || followState.isLoading) return;
|
|
223
|
+
|
|
224
|
+
// Check if user is authenticated - show toast instead of disabling
|
|
225
|
+
if (!isAuthenticated) {
|
|
226
|
+
toast.error('Please sign in to follow users');
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
177
230
|
// Touch feedback animation
|
|
178
231
|
scale.value = withSpring(0.95, { damping: 10 }, () => {
|
|
179
232
|
scale.value = withSpring(1);
|
|
180
233
|
});
|
|
181
234
|
|
|
182
|
-
setIsLoading(true);
|
|
183
|
-
|
|
184
235
|
try {
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
} else {
|
|
192
|
-
// Follow API call would go here
|
|
193
|
-
// await oxyServices.user.followUser(userId);
|
|
194
|
-
console.log(`Following user: ${userId}`);
|
|
195
|
-
await new Promise(resolve => setTimeout(resolve, 500)); // Simulating API call
|
|
196
|
-
}
|
|
236
|
+
// Dispatch the async action to follow/unfollow
|
|
237
|
+
const result = await dispatch(toggleFollowUser({
|
|
238
|
+
userId,
|
|
239
|
+
oxyServices,
|
|
240
|
+
isCurrentlyFollowing: followState.isFollowing
|
|
241
|
+
})).unwrap();
|
|
197
242
|
|
|
198
|
-
// Toggle following state with animation
|
|
199
|
-
const newFollowingState = !isFollowing;
|
|
200
|
-
setIsFollowing(newFollowingState);
|
|
201
|
-
|
|
202
243
|
// Call the callback if provided
|
|
203
244
|
if (onFollowChange) {
|
|
204
|
-
onFollowChange(
|
|
245
|
+
onFollowChange(result.isFollowing);
|
|
205
246
|
}
|
|
206
247
|
|
|
207
248
|
// Show success toast
|
|
208
|
-
toast.success(
|
|
209
|
-
} catch (error) {
|
|
249
|
+
toast.success(result.isFollowing ? 'Following user!' : 'Unfollowed user');
|
|
250
|
+
} catch (error: any) {
|
|
210
251
|
console.error('Follow action failed:', error);
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
252
|
+
|
|
253
|
+
// Show user-friendly error messages for state mismatches
|
|
254
|
+
const errorMessage = error?.toString() || 'Unknown error';
|
|
255
|
+
if (errorMessage.includes('State synced with backend')) {
|
|
256
|
+
toast.info('Status updated. Please try again.');
|
|
257
|
+
} else {
|
|
258
|
+
toast.error(`Failed to ${followState.isFollowing ? 'unfollow' : 'follow'} user. Please try again.`);
|
|
259
|
+
}
|
|
214
260
|
}
|
|
215
|
-
}
|
|
261
|
+
}, [
|
|
262
|
+
preventParentActions,
|
|
263
|
+
onPress,
|
|
264
|
+
disabled,
|
|
265
|
+
followState.isLoading,
|
|
266
|
+
followState.isFollowing,
|
|
267
|
+
isAuthenticated,
|
|
268
|
+
scale,
|
|
269
|
+
dispatch,
|
|
270
|
+
userId,
|
|
271
|
+
oxyServices,
|
|
272
|
+
onFollowChange
|
|
273
|
+
]);
|
|
216
274
|
|
|
217
275
|
// Animated styles for the button
|
|
218
276
|
const animatedButtonStyle = useAnimatedStyle(() => {
|
|
@@ -221,7 +279,7 @@ const FollowButton: React.FC<FollowButtonProps> = ({
|
|
|
221
279
|
[0, 1],
|
|
222
280
|
['#d169e5', '#FFFFFF']
|
|
223
281
|
);
|
|
224
|
-
|
|
282
|
+
|
|
225
283
|
const borderColor = interpolateColor(
|
|
226
284
|
animationProgress.value,
|
|
227
285
|
[0, 1],
|
|
@@ -296,7 +354,7 @@ const FollowButton: React.FC<FollowButtonProps> = ({
|
|
|
296
354
|
<TouchableOpacity
|
|
297
355
|
activeOpacity={0.8}
|
|
298
356
|
onPress={handlePress}
|
|
299
|
-
disabled={disabled || isLoading
|
|
357
|
+
disabled={disabled || isLoading}
|
|
300
358
|
>
|
|
301
359
|
<Animated.View
|
|
302
360
|
style={[
|
|
@@ -307,9 +365,9 @@ const FollowButton: React.FC<FollowButtonProps> = ({
|
|
|
307
365
|
]}
|
|
308
366
|
>
|
|
309
367
|
{isLoading && showLoadingState ? (
|
|
310
|
-
<ActivityIndicator
|
|
311
|
-
size="small"
|
|
312
|
-
color={isFollowing ? '#d169e5' : '#FFFFFF'}
|
|
368
|
+
<ActivityIndicator
|
|
369
|
+
size="small"
|
|
370
|
+
color={isFollowing ? '#d169e5' : '#FFFFFF'}
|
|
313
371
|
/>
|
|
314
372
|
) : (
|
|
315
373
|
<Animated.Text
|
|
@@ -88,10 +88,10 @@ export const OxySignInButton: React.FC<OxySignInButtonProps> = ({
|
|
|
88
88
|
screen = 'SignIn',
|
|
89
89
|
}) => {
|
|
90
90
|
// Get all needed values from context in a single call
|
|
91
|
-
const {
|
|
91
|
+
const { isAuthenticated, showBottomSheet } = useOxy();
|
|
92
92
|
|
|
93
93
|
// Don't show the button if already authenticated (unless explicitly overridden)
|
|
94
|
-
if (
|
|
94
|
+
if (isAuthenticated && !showWhenAuthenticated) return null;
|
|
95
95
|
|
|
96
96
|
// Default handler that uses the context methods
|
|
97
97
|
const handlePress = () => {
|
|
@@ -11,7 +11,7 @@ export interface OxyContextState {
|
|
|
11
11
|
minimalUser: MinimalUserData | null; // Minimal user data for UI
|
|
12
12
|
sessions: SecureClientSession[]; // All active sessions
|
|
13
13
|
activeSessionId: string | null;
|
|
14
|
-
isAuthenticated: boolean;
|
|
14
|
+
isAuthenticated: boolean; // Single source of truth for authentication - use this instead of service methods
|
|
15
15
|
isLoading: boolean;
|
|
16
16
|
error: string | null;
|
|
17
17
|
|
|
@@ -640,13 +640,23 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
640
640
|
}
|
|
641
641
|
}, [bottomSheetRef]);
|
|
642
642
|
|
|
643
|
+
// Compute comprehensive authentication status
|
|
644
|
+
// This is the single source of truth for authentication across the entire app
|
|
645
|
+
const isAuthenticated = useMemo(() => {
|
|
646
|
+
// User is authenticated if:
|
|
647
|
+
// 1. We have a full user object loaded, OR
|
|
648
|
+
// 2. We have an active session with a valid token
|
|
649
|
+
// This covers both the loaded state and the loading-but-authenticated state
|
|
650
|
+
return !!user || (!!activeSessionId && !!oxyServices?.getCurrentUserId());
|
|
651
|
+
}, [user, activeSessionId, oxyServices]);
|
|
652
|
+
|
|
643
653
|
// Context value
|
|
644
654
|
const contextValue: OxyContextState = {
|
|
645
655
|
user,
|
|
646
656
|
minimalUser,
|
|
647
657
|
sessions,
|
|
648
658
|
activeSessionId,
|
|
649
|
-
isAuthenticated
|
|
659
|
+
isAuthenticated,
|
|
650
660
|
isLoading,
|
|
651
661
|
error,
|
|
652
662
|
login,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useFollow } from './useFollow';
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
2
|
+
import { useCallback, useMemo } from 'react';
|
|
3
|
+
import { toggleFollowUser, setFollowingStatus, clearFollowError, fetchFollowStatus } from '../store';
|
|
4
|
+
import type { RootState } from '../store';
|
|
5
|
+
import { useOxy } from '../context/OxyContext';
|
|
6
|
+
|
|
7
|
+
// Memoized selector to prevent unnecessary re-renders
|
|
8
|
+
const createFollowSelector = (userId: string) => (state: RootState) => ({
|
|
9
|
+
isFollowing: state.follow.followingUsers[userId] ?? false,
|
|
10
|
+
isLoading: state.follow.loadingUsers[userId] ?? false,
|
|
11
|
+
error: state.follow.errors[userId] ?? null,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Memoized selector for multiple users
|
|
15
|
+
const createMultipleFollowSelector = (userIds: string[]) => (state: RootState) => {
|
|
16
|
+
const followData: Record<string, { isFollowing: boolean; isLoading: boolean; error: string | null }> = {};
|
|
17
|
+
const followState = state.follow;
|
|
18
|
+
|
|
19
|
+
for (const userId of userIds) {
|
|
20
|
+
followData[userId] = {
|
|
21
|
+
isFollowing: followState.followingUsers[userId] ?? false,
|
|
22
|
+
isLoading: followState.loadingUsers[userId] ?? false,
|
|
23
|
+
error: followState.errors[userId] ?? null,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
followData,
|
|
29
|
+
isAnyLoading: userIds.some(uid => followState.loadingUsers[uid]),
|
|
30
|
+
hasAnyError: userIds.some(uid => followState.errors[uid]),
|
|
31
|
+
allFollowing: userIds.every(uid => followState.followingUsers[uid]),
|
|
32
|
+
allNotFollowing: userIds.every(uid => !followState.followingUsers[uid]),
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Custom hook for managing follow/unfollow functionality
|
|
38
|
+
* Optimized to prevent unnecessary re-renders
|
|
39
|
+
* Can handle both single user and multiple users
|
|
40
|
+
*/
|
|
41
|
+
export const useFollow = (userId?: string | string[]) => {
|
|
42
|
+
const dispatch = useDispatch();
|
|
43
|
+
const { oxyServices } = useOxy();
|
|
44
|
+
|
|
45
|
+
// Memoize user IDs to prevent recreation on every render
|
|
46
|
+
const userIds = useMemo(() => {
|
|
47
|
+
return Array.isArray(userId) ? userId : userId ? [userId] : [];
|
|
48
|
+
}, [userId]);
|
|
49
|
+
|
|
50
|
+
const isSingleUser = typeof userId === 'string';
|
|
51
|
+
|
|
52
|
+
// Memoize selectors to prevent recreation
|
|
53
|
+
const singleUserSelector = useMemo(() => {
|
|
54
|
+
return isSingleUser && userId ? createFollowSelector(userId) : null;
|
|
55
|
+
}, [isSingleUser, userId]);
|
|
56
|
+
|
|
57
|
+
const multipleUserSelector = useMemo(() => {
|
|
58
|
+
return !isSingleUser ? createMultipleFollowSelector(userIds) : null;
|
|
59
|
+
}, [isSingleUser, userIds]);
|
|
60
|
+
|
|
61
|
+
// Use appropriate selector based on mode
|
|
62
|
+
const singleUserData = useSelector(singleUserSelector || (() => ({ isFollowing: false, isLoading: false, error: null })));
|
|
63
|
+
const multipleUserData = useSelector(multipleUserSelector || (() => ({
|
|
64
|
+
followData: {},
|
|
65
|
+
isAnyLoading: false,
|
|
66
|
+
hasAnyError: false,
|
|
67
|
+
allFollowing: false,
|
|
68
|
+
allNotFollowing: true
|
|
69
|
+
})));
|
|
70
|
+
|
|
71
|
+
// Memoized callbacks to prevent recreation on every render
|
|
72
|
+
const toggleFollow = useCallback(async () => {
|
|
73
|
+
if (!isSingleUser || !userId) throw new Error('toggleFollow is only available for single user mode');
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const result = await dispatch(toggleFollowUser({
|
|
77
|
+
userId,
|
|
78
|
+
oxyServices,
|
|
79
|
+
isCurrentlyFollowing: singleUserData.isFollowing
|
|
80
|
+
})).unwrap();
|
|
81
|
+
return result;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}, [dispatch, userId, oxyServices, singleUserData.isFollowing, isSingleUser]);
|
|
86
|
+
|
|
87
|
+
const setFollowStatus = useCallback((following: boolean) => {
|
|
88
|
+
if (!isSingleUser || !userId) throw new Error('setFollowStatus is only available for single user mode');
|
|
89
|
+
dispatch(setFollowingStatus({ userId, isFollowing: following }));
|
|
90
|
+
}, [dispatch, userId, isSingleUser]);
|
|
91
|
+
|
|
92
|
+
const fetchStatus = useCallback(async () => {
|
|
93
|
+
if (!isSingleUser || !userId) throw new Error('fetchStatus is only available for single user mode');
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
await dispatch(fetchFollowStatus({ userId, oxyServices })).unwrap();
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.warn(`Failed to fetch follow status for user ${userId}:`, error);
|
|
99
|
+
}
|
|
100
|
+
}, [dispatch, userId, oxyServices, isSingleUser]);
|
|
101
|
+
|
|
102
|
+
const clearError = useCallback(() => {
|
|
103
|
+
if (!isSingleUser || !userId) throw new Error('clearError is only available for single user mode');
|
|
104
|
+
dispatch(clearFollowError(userId));
|
|
105
|
+
}, [dispatch, userId, isSingleUser]);
|
|
106
|
+
|
|
107
|
+
// Multiple user callbacks
|
|
108
|
+
const toggleFollowForUser = useCallback(async (targetUserId: string) => {
|
|
109
|
+
const currentState = multipleUserData.followData[targetUserId]?.isFollowing ?? false;
|
|
110
|
+
try {
|
|
111
|
+
const result = await dispatch(toggleFollowUser({
|
|
112
|
+
userId: targetUserId,
|
|
113
|
+
oxyServices,
|
|
114
|
+
isCurrentlyFollowing: currentState
|
|
115
|
+
})).unwrap();
|
|
116
|
+
return result;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}, [dispatch, oxyServices, multipleUserData.followData]);
|
|
121
|
+
|
|
122
|
+
const setFollowStatusForUser = useCallback((targetUserId: string, following: boolean) => {
|
|
123
|
+
dispatch(setFollowingStatus({ userId: targetUserId, isFollowing: following }));
|
|
124
|
+
}, [dispatch]);
|
|
125
|
+
|
|
126
|
+
const fetchStatusForUser = useCallback(async (targetUserId: string) => {
|
|
127
|
+
try {
|
|
128
|
+
await dispatch(fetchFollowStatus({ userId: targetUserId, oxyServices })).unwrap();
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.warn(`Failed to fetch follow status for user ${targetUserId}:`, error);
|
|
131
|
+
}
|
|
132
|
+
}, [dispatch, oxyServices]);
|
|
133
|
+
|
|
134
|
+
const fetchAllStatuses = useCallback(async () => {
|
|
135
|
+
const promises = userIds.map(uid =>
|
|
136
|
+
dispatch(fetchFollowStatus({ userId: uid, oxyServices })).unwrap().catch((error: any) => {
|
|
137
|
+
console.warn(`Failed to fetch follow status for user ${uid}:`, error);
|
|
138
|
+
})
|
|
139
|
+
);
|
|
140
|
+
await Promise.all(promises);
|
|
141
|
+
}, [dispatch, userIds, oxyServices]);
|
|
142
|
+
|
|
143
|
+
const clearErrorForUser = useCallback((targetUserId: string) => {
|
|
144
|
+
dispatch(clearFollowError(targetUserId));
|
|
145
|
+
}, [dispatch]);
|
|
146
|
+
|
|
147
|
+
// Return appropriate interface based on mode
|
|
148
|
+
if (isSingleUser && userId) {
|
|
149
|
+
return {
|
|
150
|
+
isFollowing: singleUserData.isFollowing,
|
|
151
|
+
isLoading: singleUserData.isLoading,
|
|
152
|
+
error: singleUserData.error,
|
|
153
|
+
toggleFollow,
|
|
154
|
+
setFollowStatus,
|
|
155
|
+
fetchStatus,
|
|
156
|
+
clearError,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
followData: multipleUserData.followData,
|
|
162
|
+
toggleFollowForUser,
|
|
163
|
+
setFollowStatusForUser,
|
|
164
|
+
fetchStatusForUser,
|
|
165
|
+
fetchAllStatuses,
|
|
166
|
+
clearErrorForUser,
|
|
167
|
+
// Helper methods
|
|
168
|
+
isAnyLoading: multipleUserData.isAnyLoading,
|
|
169
|
+
hasAnyError: multipleUserData.hasAnyError,
|
|
170
|
+
allFollowing: multipleUserData.allFollowing,
|
|
171
|
+
allNotFollowing: multipleUserData.allNotFollowing,
|
|
172
|
+
};
|
|
173
|
+
};
|
package/src/ui/index.ts
CHANGED
|
@@ -30,3 +30,12 @@ export { fontFamilies, fontStyles } from './styles/fonts';
|
|
|
30
30
|
|
|
31
31
|
// Export types for navigation (internal use)
|
|
32
32
|
export * from './navigation/types';
|
|
33
|
+
|
|
34
|
+
// Hooks
|
|
35
|
+
export { useFollow } from './hooks';
|
|
36
|
+
|
|
37
|
+
// Screens
|
|
38
|
+
export { default as ProfileScreen } from './screens/ProfileScreen';
|
|
39
|
+
|
|
40
|
+
// Navigation
|
|
41
|
+
export { default as OxyRouter } from './navigation/OxyRouter';
|
|
@@ -15,12 +15,12 @@ import { packageInfo } from '../../constants/version';
|
|
|
15
15
|
import { toast } from '../../lib/sonner';
|
|
16
16
|
import { Ionicons } from '@expo/vector-icons';
|
|
17
17
|
import { fontFamilies } from '../styles/fonts';
|
|
18
|
-
import {
|
|
19
|
-
ProfileCard,
|
|
20
|
-
Section,
|
|
21
|
-
QuickActions,
|
|
22
|
-
GroupedSection,
|
|
23
|
-
GroupedItem
|
|
18
|
+
import {
|
|
19
|
+
ProfileCard,
|
|
20
|
+
Section,
|
|
21
|
+
QuickActions,
|
|
22
|
+
GroupedSection,
|
|
23
|
+
GroupedItem
|
|
24
24
|
} from '../components';
|
|
25
25
|
|
|
26
26
|
const AccountCenterScreen: React.FC<BaseScreenProps> = ({
|
|
@@ -28,7 +28,7 @@ const AccountCenterScreen: React.FC<BaseScreenProps> = ({
|
|
|
28
28
|
theme,
|
|
29
29
|
navigate,
|
|
30
30
|
}) => {
|
|
31
|
-
const { user, logout, isLoading, sessions } = useOxy();
|
|
31
|
+
const { user, logout, isLoading, sessions, isAuthenticated } = useOxy();
|
|
32
32
|
|
|
33
33
|
const isDarkTheme = theme === 'dark';
|
|
34
34
|
const textColor = isDarkTheme ? '#FFFFFF' : '#000000';
|
|
@@ -69,7 +69,7 @@ const AccountCenterScreen: React.FC<BaseScreenProps> = ({
|
|
|
69
69
|
);
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
-
if (!
|
|
72
|
+
if (!isAuthenticated) {
|
|
73
73
|
return (
|
|
74
74
|
<View style={[styles.container, { backgroundColor }]}>
|
|
75
75
|
<Text style={[styles.message, { color: textColor }]}>Not signed in</Text>
|
|
@@ -88,13 +88,15 @@ const AccountCenterScreen: React.FC<BaseScreenProps> = ({
|
|
|
88
88
|
return (
|
|
89
89
|
<View style={[styles.container, { backgroundColor }]}>
|
|
90
90
|
{/* Header with user profile */}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
91
|
+
{user && (
|
|
92
|
+
<ProfileCard
|
|
93
|
+
user={user}
|
|
94
|
+
theme={theme}
|
|
95
|
+
onEditPress={() => navigate('AccountSettings', { activeTab: 'profile' })}
|
|
96
|
+
onClosePress={onClose}
|
|
97
|
+
showCloseButton={!!onClose}
|
|
98
|
+
/>
|
|
99
|
+
)}
|
|
98
100
|
|
|
99
101
|
<ScrollView style={styles.scrollView} contentContainerStyle={styles.scrollContainer} showsVerticalScrollIndicator={false}>
|
|
100
102
|
{/* Quick Actions */}
|