@oxyhq/services 6.9.11 → 6.9.13
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/components/FollowButton.js +135 -118
- package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
- package/lib/commonjs/ui/components/GroupedSection.js.map +1 -1
- package/lib/commonjs/ui/components/fileManagement/FileDetailsModal.js.map +1 -1
- package/lib/commonjs/ui/components/fileManagement/UploadPreview.js.map +1 -1
- package/lib/commonjs/ui/components/internal/GroupedPillButtons.js.map +1 -1
- package/lib/commonjs/ui/components/payment/PaymentReviewStep.js.map +1 -1
- package/lib/commonjs/ui/components/payment/PaymentSummaryStep.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js +6 -2
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/commonjs/ui/hooks/mutations/useServicesMutations.js.map +1 -1
- package/lib/commonjs/ui/hooks/useAuth.js +9 -3
- package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
- package/lib/commonjs/ui/hooks/useFollow.js +134 -74
- package/lib/commonjs/ui/hooks/useFollow.js.map +1 -1
- package/lib/commonjs/ui/hooks/useWebSSO.js.map +1 -1
- package/lib/commonjs/ui/screens/EditProfileFieldScreen.js +30 -11
- package/lib/commonjs/ui/screens/EditProfileFieldScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/FAQScreen.js +1 -0
- package/lib/commonjs/ui/screens/FAQScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/FeedbackScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/FileManagementScreen.js +0 -1
- package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/HistoryViewScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/PremiumSubscriptionScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/karma/KarmaRewardsScreen.js.map +1 -1
- package/lib/commonjs/ui/stores/fileStore.js +6 -6
- package/lib/commonjs/ui/stores/fileStore.js.map +1 -1
- package/lib/commonjs/ui/utils/fileManagement.js +6 -3
- package/lib/commonjs/ui/utils/fileManagement.js.map +1 -1
- package/lib/module/ui/components/FollowButton.js +137 -121
- package/lib/module/ui/components/FollowButton.js.map +1 -1
- package/lib/module/ui/components/GroupedSection.js.map +1 -1
- package/lib/module/ui/components/fileManagement/FileDetailsModal.js.map +1 -1
- package/lib/module/ui/components/fileManagement/UploadPreview.js.map +1 -1
- package/lib/module/ui/components/internal/GroupedPillButtons.js.map +1 -1
- package/lib/module/ui/components/payment/PaymentReviewStep.js.map +1 -1
- package/lib/module/ui/components/payment/PaymentSummaryStep.js.map +1 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js +7 -2
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/module/ui/hooks/mutations/useServicesMutations.js.map +1 -1
- package/lib/module/ui/hooks/useAuth.js +9 -3
- package/lib/module/ui/hooks/useAuth.js.map +1 -1
- package/lib/module/ui/hooks/useFollow.js +132 -72
- package/lib/module/ui/hooks/useFollow.js.map +1 -1
- package/lib/module/ui/hooks/useWebSSO.js.map +1 -1
- package/lib/module/ui/screens/EditProfileFieldScreen.js +30 -11
- package/lib/module/ui/screens/EditProfileFieldScreen.js.map +1 -1
- package/lib/module/ui/screens/FAQScreen.js +1 -0
- package/lib/module/ui/screens/FAQScreen.js.map +1 -1
- package/lib/module/ui/screens/FeedbackScreen.js.map +1 -1
- package/lib/module/ui/screens/FileManagementScreen.js +0 -1
- package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/module/ui/screens/HistoryViewScreen.js.map +1 -1
- package/lib/module/ui/screens/PremiumSubscriptionScreen.js.map +1 -1
- package/lib/module/ui/screens/karma/KarmaRewardsScreen.js.map +1 -1
- package/lib/module/ui/stores/fileStore.js +6 -6
- package/lib/module/ui/stores/fileStore.js.map +1 -1
- package/lib/module/ui/utils/fileManagement.js +6 -3
- package/lib/module/ui/utils/fileManagement.js.map +1 -1
- package/lib/typescript/commonjs/ui/components/FollowButton.d.ts +12 -1
- package/lib/typescript/commonjs/ui/components/FollowButton.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/GroupedSection.d.ts +5 -0
- package/lib/typescript/commonjs/ui/components/GroupedSection.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/feedback/types.d.ts +2 -2
- package/lib/typescript/commonjs/ui/components/feedback/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/internal/GroupedPillButtons.d.ts +7 -1
- package/lib/typescript/commonjs/ui/components/internal/GroupedPillButtons.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useFollow.d.ts +31 -0
- package/lib/typescript/commonjs/ui/hooks/useFollow.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/FAQScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/HistoryViewScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/fileManagement.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/FollowButton.d.ts +12 -1
- package/lib/typescript/module/ui/components/FollowButton.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/GroupedSection.d.ts +5 -0
- package/lib/typescript/module/ui/components/GroupedSection.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/feedback/types.d.ts +2 -2
- package/lib/typescript/module/ui/components/feedback/types.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/internal/GroupedPillButtons.d.ts +7 -1
- package/lib/typescript/module/ui/components/internal/GroupedPillButtons.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useFollow.d.ts +31 -0
- package/lib/typescript/module/ui/hooks/useFollow.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/FAQScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/HistoryViewScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/fileManagement.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/ui/components/FollowButton.tsx +117 -109
- package/src/ui/components/GroupedSection.tsx +5 -0
- package/src/ui/components/feedback/types.ts +2 -2
- package/src/ui/components/fileManagement/FileDetailsModal.tsx +1 -1
- package/src/ui/components/fileManagement/UploadPreview.tsx +1 -1
- package/src/ui/components/internal/GroupedPillButtons.tsx +17 -8
- package/src/ui/components/payment/PaymentReviewStep.tsx +1 -1
- package/src/ui/components/payment/PaymentSummaryStep.tsx +1 -1
- package/src/ui/context/hooks/useAuthOperations.ts +8 -3
- package/src/ui/hooks/mutations/useServicesMutations.ts +3 -3
- package/src/ui/hooks/useAuth.ts +10 -4
- package/src/ui/hooks/useFollow.ts +161 -74
- package/src/ui/hooks/useWebSSO.ts +3 -3
- package/src/ui/screens/EditProfileFieldScreen.tsx +28 -16
- package/src/ui/screens/FAQScreen.tsx +2 -1
- package/src/ui/screens/FeedbackScreen.tsx +7 -7
- package/src/ui/screens/FileManagementScreen.tsx +1 -2
- package/src/ui/screens/HistoryViewScreen.tsx +5 -1
- package/src/ui/screens/PremiumSubscriptionScreen.tsx +1 -1
- package/src/ui/screens/karma/KarmaRewardsScreen.tsx +1 -1
- package/src/ui/stores/fileStore.ts +9 -9
- package/src/ui/utils/fileManagement.ts +16 -10
|
@@ -1,114 +1,150 @@
|
|
|
1
1
|
import { useCallback, useMemo, useEffect } from 'react';
|
|
2
2
|
import { useFollowStore } from '../stores/followStore';
|
|
3
3
|
import { useOxy } from '../context/OxyContext';
|
|
4
|
+
import type { OxyServices } from '@oxyhq/core';
|
|
5
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
4
6
|
|
|
7
|
+
/**
|
|
8
|
+
* useFollow — Hook for follow state management.
|
|
9
|
+
*
|
|
10
|
+
* Performance fixes:
|
|
11
|
+
* 1. Uses granular Zustand selectors instead of subscribing to the entire store.
|
|
12
|
+
* The old `useFollowStore()` caused every component using this hook to re-render
|
|
13
|
+
* on ANY store change (any user's follow status, loading state, error, or count).
|
|
14
|
+
* 2. Callbacks depend on primitive values (userId, isFollowing) not object references,
|
|
15
|
+
* so they remain stable across renders.
|
|
16
|
+
* 3. Store action methods are accessed via getState() in callbacks to avoid
|
|
17
|
+
* subscribing to the action functions themselves (they never change but including
|
|
18
|
+
* them in selectors would cause unnecessary selector recalculations).
|
|
19
|
+
*/
|
|
5
20
|
export const useFollow = (userId?: string | string[]) => {
|
|
6
21
|
const { oxyServices } = useOxy();
|
|
7
22
|
const userIds = useMemo(() => (Array.isArray(userId) ? userId : userId ? [userId] : []), [userId]);
|
|
8
23
|
const isSingleUser = typeof userId === 'string';
|
|
9
24
|
|
|
10
|
-
// Zustand selectors
|
|
11
|
-
const
|
|
25
|
+
// Granular Zustand selectors — only re-render when THIS user's data changes
|
|
26
|
+
const isFollowing = useFollowStore(
|
|
27
|
+
useCallback((s) => (isSingleUser && userId ? s.followingUsers[userId] ?? false : false), [isSingleUser, userId])
|
|
28
|
+
);
|
|
29
|
+
const isLoading = useFollowStore(
|
|
30
|
+
useCallback((s) => (isSingleUser && userId ? s.loadingUsers[userId] ?? false : false), [isSingleUser, userId])
|
|
31
|
+
);
|
|
32
|
+
const error = useFollowStore(
|
|
33
|
+
useCallback((s) => (isSingleUser && userId ? s.errors[userId] ?? null : null), [isSingleUser, userId])
|
|
34
|
+
);
|
|
35
|
+
const followerCount = useFollowStore(
|
|
36
|
+
useCallback((s) => (isSingleUser && userId ? s.followerCounts[userId] ?? null : null), [isSingleUser, userId])
|
|
37
|
+
);
|
|
38
|
+
const followingCount = useFollowStore(
|
|
39
|
+
useCallback((s) => (isSingleUser && userId ? s.followingCounts[userId] ?? null : null), [isSingleUser, userId])
|
|
40
|
+
);
|
|
41
|
+
const isLoadingCounts = useFollowStore(
|
|
42
|
+
useCallback((s) => (isSingleUser && userId ? s.loadingCounts[userId] ?? false : false), [isSingleUser, userId])
|
|
43
|
+
);
|
|
12
44
|
|
|
13
|
-
//
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
45
|
+
// For multi-user mode, use shallow comparison to avoid re-renders when unrelated users change
|
|
46
|
+
const followData = useFollowStore(
|
|
47
|
+
useShallow((s) => {
|
|
48
|
+
if (isSingleUser) return {};
|
|
49
|
+
const data: Record<string, { isFollowing: boolean; isLoading: boolean; error: string | null }> = {};
|
|
50
|
+
for (const uid of userIds) {
|
|
51
|
+
data[uid] = {
|
|
52
|
+
isFollowing: s.followingUsers[uid] ?? false,
|
|
53
|
+
isLoading: s.loadingUsers[uid] ?? false,
|
|
54
|
+
error: s.errors[uid] ?? null,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return data;
|
|
58
|
+
})
|
|
59
|
+
);
|
|
22
60
|
|
|
61
|
+
// Multi-user aggregate selectors
|
|
62
|
+
const multiUserLoadingState = useFollowStore(
|
|
63
|
+
useShallow((s) => {
|
|
64
|
+
if (isSingleUser) return { isAnyLoading: false, hasAnyError: false, allFollowing: true, allNotFollowing: true };
|
|
65
|
+
return {
|
|
66
|
+
isAnyLoading: userIds.some(uid => s.loadingUsers[uid]),
|
|
67
|
+
hasAnyError: userIds.some(uid => !!s.errors[uid]),
|
|
68
|
+
allFollowing: userIds.every(uid => s.followingUsers[uid]),
|
|
69
|
+
allNotFollowing: userIds.every(uid => !s.followingUsers[uid]),
|
|
70
|
+
};
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Stable callbacks that depend on primitives, not object references.
|
|
75
|
+
// Store actions are accessed via getState() to avoid subscribing to them.
|
|
23
76
|
const toggleFollow = useCallback(async () => {
|
|
24
77
|
if (!isSingleUser || !userId) throw new Error('toggleFollow is only available for single user mode');
|
|
25
|
-
|
|
26
|
-
|
|
78
|
+
const currentlyFollowing = useFollowStore.getState().followingUsers[userId] ?? false;
|
|
79
|
+
await useFollowStore.getState().toggleFollowUser(userId, oxyServices, currentlyFollowing);
|
|
80
|
+
}, [isSingleUser, userId, oxyServices]);
|
|
27
81
|
|
|
28
82
|
const setFollowStatus = useCallback((following: boolean) => {
|
|
29
83
|
if (!isSingleUser || !userId) throw new Error('setFollowStatus is only available for single user mode');
|
|
30
|
-
|
|
31
|
-
}, [isSingleUser, userId
|
|
84
|
+
useFollowStore.getState().setFollowingStatus(userId, following);
|
|
85
|
+
}, [isSingleUser, userId]);
|
|
32
86
|
|
|
33
87
|
const fetchStatus = useCallback(async () => {
|
|
34
88
|
if (!isSingleUser || !userId) throw new Error('fetchStatus is only available for single user mode');
|
|
35
|
-
await
|
|
36
|
-
}, [isSingleUser, userId,
|
|
89
|
+
await useFollowStore.getState().fetchFollowStatus(userId, oxyServices);
|
|
90
|
+
}, [isSingleUser, userId, oxyServices]);
|
|
37
91
|
|
|
38
92
|
const clearError = useCallback(() => {
|
|
39
93
|
if (!isSingleUser || !userId) throw new Error('clearError is only available for single user mode');
|
|
40
|
-
|
|
41
|
-
}, [isSingleUser, userId
|
|
94
|
+
useFollowStore.getState().clearFollowError(userId);
|
|
95
|
+
}, [isSingleUser, userId]);
|
|
42
96
|
|
|
43
97
|
const fetchUserCounts = useCallback(async () => {
|
|
44
98
|
if (!isSingleUser || !userId) throw new Error('fetchUserCounts is only available for single user mode');
|
|
45
|
-
await
|
|
46
|
-
}, [isSingleUser, userId,
|
|
99
|
+
await useFollowStore.getState().fetchUserCounts(userId, oxyServices);
|
|
100
|
+
}, [isSingleUser, userId, oxyServices]);
|
|
47
101
|
|
|
48
102
|
const setFollowerCount = useCallback((count: number) => {
|
|
49
103
|
if (!isSingleUser || !userId) throw new Error('setFollowerCount is only available for single user mode');
|
|
50
|
-
|
|
51
|
-
}, [isSingleUser, userId
|
|
104
|
+
useFollowStore.getState().setFollowerCount(userId, count);
|
|
105
|
+
}, [isSingleUser, userId]);
|
|
52
106
|
|
|
53
107
|
const setFollowingCount = useCallback((count: number) => {
|
|
54
108
|
if (!isSingleUser || !userId) throw new Error('setFollowingCount is only available for single user mode');
|
|
55
|
-
|
|
56
|
-
}, [isSingleUser, userId
|
|
109
|
+
useFollowStore.getState().setFollowingCount(userId, count);
|
|
110
|
+
}, [isSingleUser, userId]);
|
|
57
111
|
|
|
58
112
|
// Auto-fetch counts when hook is used for a single user and counts are missing.
|
|
59
113
|
useEffect(() => {
|
|
60
114
|
if (!isSingleUser || !userId) return;
|
|
61
115
|
|
|
62
|
-
// If either count is not set and we're not already loading counts, trigger a fetch.
|
|
63
116
|
if ((followerCount === null || followingCount === null) && !isLoadingCounts) {
|
|
64
117
|
fetchUserCounts().catch((err: unknown) => console.warn('useFollow: fetchUserCounts failed', err));
|
|
65
118
|
}
|
|
66
119
|
}, [isSingleUser, userId, followerCount, followingCount, isLoadingCounts, fetchUserCounts]);
|
|
67
120
|
|
|
68
|
-
//
|
|
69
|
-
const followData = useMemo(() => {
|
|
70
|
-
const data: Record<string, { isFollowing: boolean; isLoading: boolean; error: string | null }> = {};
|
|
71
|
-
userIds.forEach(uid => {
|
|
72
|
-
data[uid] = {
|
|
73
|
-
isFollowing: followState.followingUsers[uid] ?? false,
|
|
74
|
-
isLoading: followState.loadingUsers[uid] ?? false,
|
|
75
|
-
error: followState.errors[uid] ?? null,
|
|
76
|
-
};
|
|
77
|
-
});
|
|
78
|
-
return data;
|
|
79
|
-
}, [userIds, followState.followingUsers, followState.loadingUsers, followState.errors]);
|
|
80
|
-
|
|
121
|
+
// Multi-user callbacks
|
|
81
122
|
const toggleFollowForUser = useCallback(async (targetUserId: string) => {
|
|
82
|
-
const currentState =
|
|
83
|
-
await
|
|
84
|
-
}, [
|
|
123
|
+
const currentState = useFollowStore.getState().followingUsers[targetUserId] ?? false;
|
|
124
|
+
await useFollowStore.getState().toggleFollowUser(targetUserId, oxyServices, currentState);
|
|
125
|
+
}, [oxyServices]);
|
|
85
126
|
|
|
86
127
|
const setFollowStatusForUser = useCallback((targetUserId: string, following: boolean) => {
|
|
87
|
-
|
|
88
|
-
}, [
|
|
128
|
+
useFollowStore.getState().setFollowingStatus(targetUserId, following);
|
|
129
|
+
}, []);
|
|
89
130
|
|
|
90
131
|
const fetchStatusForUser = useCallback(async (targetUserId: string) => {
|
|
91
|
-
await
|
|
92
|
-
}, [
|
|
132
|
+
await useFollowStore.getState().fetchFollowStatus(targetUserId, oxyServices);
|
|
133
|
+
}, [oxyServices]);
|
|
93
134
|
|
|
94
135
|
const fetchAllStatuses = useCallback(async () => {
|
|
95
|
-
|
|
96
|
-
|
|
136
|
+
const store = useFollowStore.getState();
|
|
137
|
+
await Promise.all(userIds.map(uid => store.fetchFollowStatus(uid, oxyServices)));
|
|
138
|
+
}, [userIds, oxyServices]);
|
|
97
139
|
|
|
98
140
|
const clearErrorForUser = useCallback((targetUserId: string) => {
|
|
99
|
-
|
|
100
|
-
}, [
|
|
141
|
+
useFollowStore.getState().clearFollowError(targetUserId);
|
|
142
|
+
}, []);
|
|
101
143
|
|
|
102
144
|
const updateCountsFromFollowAction = useCallback((targetUserId: string, action: 'follow' | 'unfollow', counts: { followers: number; following: number }) => {
|
|
103
145
|
const currentUserId = oxyServices.getCurrentUserId() || undefined;
|
|
104
|
-
|
|
105
|
-
}, [
|
|
106
|
-
|
|
107
|
-
// Aggregate helpers for multiple users
|
|
108
|
-
const isAnyLoading = userIds.some(uid => followState.loadingUsers[uid]);
|
|
109
|
-
const hasAnyError = userIds.some(uid => !!followState.errors[uid]);
|
|
110
|
-
const allFollowing = userIds.every(uid => followState.followingUsers[uid]);
|
|
111
|
-
const allNotFollowing = userIds.every(uid => !followState.followingUsers[uid]);
|
|
146
|
+
useFollowStore.getState().updateCountsFromFollowAction(targetUserId, action, counts, currentUserId);
|
|
147
|
+
}, [oxyServices]);
|
|
112
148
|
|
|
113
149
|
if (isSingleUser && userId) {
|
|
114
150
|
return {
|
|
@@ -119,7 +155,6 @@ export const useFollow = (userId?: string | string[]) => {
|
|
|
119
155
|
setFollowStatus,
|
|
120
156
|
fetchStatus,
|
|
121
157
|
clearError,
|
|
122
|
-
// Follower count methods
|
|
123
158
|
followerCount,
|
|
124
159
|
followingCount,
|
|
125
160
|
isLoadingCounts,
|
|
@@ -136,33 +171,85 @@ export const useFollow = (userId?: string | string[]) => {
|
|
|
136
171
|
fetchStatusForUser,
|
|
137
172
|
fetchAllStatuses,
|
|
138
173
|
clearErrorForUser,
|
|
139
|
-
isAnyLoading,
|
|
140
|
-
hasAnyError,
|
|
141
|
-
allFollowing,
|
|
142
|
-
allNotFollowing,
|
|
174
|
+
isAnyLoading: multiUserLoadingState.isAnyLoading,
|
|
175
|
+
hasAnyError: multiUserLoadingState.hasAnyError,
|
|
176
|
+
allFollowing: multiUserLoadingState.allFollowing,
|
|
177
|
+
allNotFollowing: multiUserLoadingState.allNotFollowing,
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* useFollowForButton — Lightweight follow hook for FollowButton in list contexts.
|
|
183
|
+
*
|
|
184
|
+
* Unlike useFollow, this hook:
|
|
185
|
+
* - Accepts oxyServices directly (no useOxy() context subscription)
|
|
186
|
+
* - Only subscribes to the specific user's follow and loading state
|
|
187
|
+
* - Returns only what FollowButton needs (no counts, no multi-user)
|
|
188
|
+
*/
|
|
189
|
+
export const useFollowForButton = (userId: string, oxyServices: OxyServices) => {
|
|
190
|
+
const isFollowing = useFollowStore(
|
|
191
|
+
useCallback((s) => s.followingUsers[userId] ?? false, [userId])
|
|
192
|
+
);
|
|
193
|
+
const isLoading = useFollowStore(
|
|
194
|
+
useCallback((s) => s.loadingUsers[userId] ?? false, [userId])
|
|
195
|
+
);
|
|
196
|
+
const error = useFollowStore(
|
|
197
|
+
useCallback((s) => s.errors[userId] ?? null, [userId])
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const toggleFollow = useCallback(async () => {
|
|
201
|
+
const currentlyFollowing = useFollowStore.getState().followingUsers[userId] ?? false;
|
|
202
|
+
await useFollowStore.getState().toggleFollowUser(userId, oxyServices, currentlyFollowing);
|
|
203
|
+
}, [userId, oxyServices]);
|
|
204
|
+
|
|
205
|
+
const fetchStatus = useCallback(async () => {
|
|
206
|
+
await useFollowStore.getState().fetchFollowStatus(userId, oxyServices);
|
|
207
|
+
}, [userId, oxyServices]);
|
|
208
|
+
|
|
209
|
+
const setFollowStatus = useCallback((following: boolean) => {
|
|
210
|
+
useFollowStore.getState().setFollowingStatus(userId, following);
|
|
211
|
+
}, [userId]);
|
|
212
|
+
|
|
213
|
+
const clearError = useCallback(() => {
|
|
214
|
+
useFollowStore.getState().clearFollowError(userId);
|
|
215
|
+
}, [userId]);
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
isFollowing,
|
|
219
|
+
isLoading,
|
|
220
|
+
error,
|
|
221
|
+
toggleFollow,
|
|
222
|
+
fetchStatus,
|
|
223
|
+
setFollowStatus,
|
|
224
|
+
clearError,
|
|
143
225
|
};
|
|
144
226
|
};
|
|
145
227
|
|
|
146
228
|
// Convenience hook for just follower counts
|
|
147
229
|
export const useFollowerCounts = (userId: string) => {
|
|
148
230
|
const { oxyServices } = useOxy();
|
|
149
|
-
const followState = useFollowStore();
|
|
150
231
|
|
|
151
|
-
const followerCount =
|
|
152
|
-
|
|
153
|
-
|
|
232
|
+
const followerCount = useFollowStore(
|
|
233
|
+
useCallback((s) => s.followerCounts[userId] ?? null, [userId])
|
|
234
|
+
);
|
|
235
|
+
const followingCount = useFollowStore(
|
|
236
|
+
useCallback((s) => s.followingCounts[userId] ?? null, [userId])
|
|
237
|
+
);
|
|
238
|
+
const isLoadingCounts = useFollowStore(
|
|
239
|
+
useCallback((s) => s.loadingCounts[userId] ?? false, [userId])
|
|
240
|
+
);
|
|
154
241
|
|
|
155
242
|
const fetchUserCounts = useCallback(async () => {
|
|
156
|
-
await
|
|
157
|
-
}, [userId,
|
|
243
|
+
await useFollowStore.getState().fetchUserCounts(userId, oxyServices);
|
|
244
|
+
}, [userId, oxyServices]);
|
|
158
245
|
|
|
159
246
|
const setFollowerCount = useCallback((count: number) => {
|
|
160
|
-
|
|
161
|
-
}, [userId
|
|
247
|
+
useFollowStore.getState().setFollowerCount(userId, count);
|
|
248
|
+
}, [userId]);
|
|
162
249
|
|
|
163
250
|
const setFollowingCount = useCallback((count: number) => {
|
|
164
|
-
|
|
165
|
-
}, [userId
|
|
251
|
+
useFollowStore.getState().setFollowingCount(userId, count);
|
|
252
|
+
}, [userId]);
|
|
166
253
|
|
|
167
254
|
return {
|
|
168
255
|
followerCount,
|
|
@@ -172,4 +259,4 @@ export const useFollowerCounts = (userId: string) => {
|
|
|
172
259
|
setFollowerCount,
|
|
173
260
|
setFollowingCount,
|
|
174
261
|
};
|
|
175
|
-
};
|
|
262
|
+
};
|
|
@@ -84,7 +84,7 @@ export function useWebSSO({
|
|
|
84
84
|
const hasCheckedRef = useRef(false);
|
|
85
85
|
|
|
86
86
|
// Check FedCM support once
|
|
87
|
-
const fedCMSupported = isWebBrowser() &&
|
|
87
|
+
const fedCMSupported = isWebBrowser() && oxyServices.isFedCMSupported?.();
|
|
88
88
|
|
|
89
89
|
const checkSSO = useCallback(async (): Promise<SessionLoginResponse | null> => {
|
|
90
90
|
if (!isWebBrowser() || isCheckingRef.current) {
|
|
@@ -106,7 +106,7 @@ export function useWebSSO({
|
|
|
106
106
|
isCheckingRef.current = true;
|
|
107
107
|
|
|
108
108
|
try {
|
|
109
|
-
const session = await
|
|
109
|
+
const session = await oxyServices.silentSignInWithFedCM?.();
|
|
110
110
|
|
|
111
111
|
if (session) {
|
|
112
112
|
await onSessionFound(session);
|
|
@@ -142,7 +142,7 @@ export function useWebSSO({
|
|
|
142
142
|
isCheckingRef.current = true;
|
|
143
143
|
|
|
144
144
|
try {
|
|
145
|
-
const session = await
|
|
145
|
+
const session = await oxyServices.signInWithFedCM?.();
|
|
146
146
|
|
|
147
147
|
if (session) {
|
|
148
148
|
await onSessionFound(session);
|
|
@@ -255,31 +255,30 @@ const EditProfileFieldScreen: React.FC<EditProfileFieldScreenProps> = ({
|
|
|
255
255
|
useEffect(() => {
|
|
256
256
|
if (!user) return;
|
|
257
257
|
|
|
258
|
-
|
|
259
|
-
const userData = user as any;
|
|
258
|
+
const userData = user;
|
|
260
259
|
|
|
261
260
|
if (fieldConfig.isList) {
|
|
262
261
|
if (fieldType === 'locations') {
|
|
263
|
-
const locations = Array.isArray(userData.locations) ? userData.locations : [];
|
|
264
|
-
setListItems(locations.map((loc
|
|
265
|
-
id: loc.id || `location-${i}
|
|
266
|
-
name: loc.name || '',
|
|
262
|
+
const locations = Array.isArray(userData.locations) ? userData.locations as Array<Record<string, unknown>> : [];
|
|
263
|
+
setListItems(locations.map((loc, i) => ({
|
|
264
|
+
id: String(loc.id || `location-${i}`),
|
|
265
|
+
name: String(loc.name || ''),
|
|
267
266
|
...loc,
|
|
268
267
|
})));
|
|
269
268
|
} else if (fieldType === 'links') {
|
|
270
|
-
const linksMetadata = Array.isArray(userData.linksMetadata) ? userData.linksMetadata : [];
|
|
269
|
+
const linksMetadata = Array.isArray(userData.linksMetadata) ? userData.linksMetadata as Array<Record<string, unknown>> : [];
|
|
271
270
|
const links = Array.isArray(userData.links) ? userData.links : [];
|
|
272
271
|
// Use linksMetadata if available, otherwise convert links array
|
|
273
272
|
if (linksMetadata.length > 0) {
|
|
274
|
-
setListItems(linksMetadata.map((link
|
|
275
|
-
id: link.id || `link-${i}
|
|
276
|
-
url: link.url || link.link || '',
|
|
277
|
-
title: link.title || '',
|
|
273
|
+
setListItems(linksMetadata.map((link, i) => ({
|
|
274
|
+
id: String(link.id || `link-${i}`),
|
|
275
|
+
url: String(link.url || link.link || ''),
|
|
276
|
+
title: String(link.title || ''),
|
|
278
277
|
...link,
|
|
279
278
|
})));
|
|
280
279
|
} else {
|
|
281
|
-
setListItems(links.map((item
|
|
282
|
-
const url = typeof item === 'string' ? item : (item.link ||
|
|
280
|
+
setListItems(links.map((item, i) => {
|
|
281
|
+
const url = typeof item === 'string' ? item : (item.link || '');
|
|
283
282
|
return {
|
|
284
283
|
id: `link-${i}`,
|
|
285
284
|
url,
|
|
@@ -365,11 +364,24 @@ const EditProfileFieldScreen: React.FC<EditProfileFieldScreenProps> = ({
|
|
|
365
364
|
if (fieldConfig.isList) {
|
|
366
365
|
let success = false;
|
|
367
366
|
if (fieldType === 'locations') {
|
|
368
|
-
success = await saveProfile({
|
|
367
|
+
success = await saveProfile({
|
|
368
|
+
locations: listItems.map(item => ({
|
|
369
|
+
id: item.id,
|
|
370
|
+
name: String(item.name || ''),
|
|
371
|
+
...(item.label !== undefined && { label: String(item.label) }),
|
|
372
|
+
...(item.coordinates !== undefined && { coordinates: item.coordinates as { lat: number; lon: number } }),
|
|
373
|
+
})),
|
|
374
|
+
});
|
|
369
375
|
} else if (fieldType === 'links') {
|
|
370
376
|
success = await saveProfile({
|
|
371
|
-
linksMetadata: listItems
|
|
372
|
-
|
|
377
|
+
linksMetadata: listItems.map(item => ({
|
|
378
|
+
id: item.id,
|
|
379
|
+
url: String(item.url || ''),
|
|
380
|
+
...(item.title !== undefined && { title: String(item.title) }),
|
|
381
|
+
...(item.description !== undefined && { description: String(item.description) }),
|
|
382
|
+
...(item.image !== undefined && { image: String(item.image) }),
|
|
383
|
+
})),
|
|
384
|
+
links: listItems.map(item => String(item.url || '')),
|
|
373
385
|
});
|
|
374
386
|
}
|
|
375
387
|
if (success) {
|
|
@@ -263,7 +263,8 @@ const createStyles = (themeStyles: any) => StyleSheet.create({
|
|
|
263
263
|
flex: 1,
|
|
264
264
|
fontSize: 16,
|
|
265
265
|
...Platform.select({
|
|
266
|
-
|
|
266
|
+
// outlineStyle: 'none' is a valid web CSS property not in React Native's TextStyle definition
|
|
267
|
+
web: { outlineStyle: 'none' as unknown as import('react-native').TextStyle['outlineStyle'] },
|
|
267
268
|
}),
|
|
268
269
|
},
|
|
269
270
|
categoriesContainer: {
|
|
@@ -50,7 +50,7 @@ const FeedbackScreen: React.FC<BaseScreenProps> = ({
|
|
|
50
50
|
const fadeAnim = useRef(new Animated.Value(1)).current;
|
|
51
51
|
const slideAnim = useRef(new Animated.Value(0)).current;
|
|
52
52
|
|
|
53
|
-
const styles = useMemo(() => createFeedbackStyles(colors
|
|
53
|
+
const styles = useMemo(() => createFeedbackStyles(colors), [colors]);
|
|
54
54
|
|
|
55
55
|
const animateTransition = useCallback((nextStep: number) => {
|
|
56
56
|
Animated.timing(fadeAnim, {
|
|
@@ -149,7 +149,7 @@ const FeedbackScreen: React.FC<BaseScreenProps> = ({
|
|
|
149
149
|
iconColor: type.color,
|
|
150
150
|
title: type.label,
|
|
151
151
|
subtitle: type.description,
|
|
152
|
-
onPress: () => { updateField('type', type.id
|
|
152
|
+
onPress: () => { updateField('type', type.id); updateField('category', ''); },
|
|
153
153
|
selected: feedbackData.type === type.id,
|
|
154
154
|
showChevron: false,
|
|
155
155
|
multiRow: true,
|
|
@@ -172,7 +172,7 @@ const FeedbackScreen: React.FC<BaseScreenProps> = ({
|
|
|
172
172
|
icon: p.icon,
|
|
173
173
|
iconColor: p.color,
|
|
174
174
|
title: p.label,
|
|
175
|
-
onPress: () => updateField('priority', p.id
|
|
175
|
+
onPress: () => updateField('priority', p.id),
|
|
176
176
|
selected: feedbackData.priority === p.id,
|
|
177
177
|
showChevron: false,
|
|
178
178
|
dense: true,
|
|
@@ -246,7 +246,7 @@ const FeedbackScreen: React.FC<BaseScreenProps> = ({
|
|
|
246
246
|
onChangeText={(text) => { updateField('title', text); setErrorMessage(''); }}
|
|
247
247
|
placeholder={t('feedback.fields.title.placeholder') || 'Brief summary of your feedback'}
|
|
248
248
|
testID="feedback-title-input"
|
|
249
|
-
colors={colors
|
|
249
|
+
colors={colors}
|
|
250
250
|
styles={styles}
|
|
251
251
|
accessibilityLabel="Feedback title"
|
|
252
252
|
accessibilityHint="Enter a brief summary of your feedback"
|
|
@@ -261,7 +261,7 @@ const FeedbackScreen: React.FC<BaseScreenProps> = ({
|
|
|
261
261
|
multiline={true}
|
|
262
262
|
numberOfLines={6}
|
|
263
263
|
testID="feedback-description-input"
|
|
264
|
-
colors={colors
|
|
264
|
+
colors={colors}
|
|
265
265
|
styles={styles}
|
|
266
266
|
accessibilityLabel="Feedback description"
|
|
267
267
|
accessibilityHint="Provide detailed information about your feedback"
|
|
@@ -319,7 +319,7 @@ const FeedbackScreen: React.FC<BaseScreenProps> = ({
|
|
|
319
319
|
onChangeText={(text) => { updateField('contactEmail', text); setErrorMessage(''); }}
|
|
320
320
|
placeholder={user?.email || (t('feedback.fields.email.placeholder') || 'Enter your email address')}
|
|
321
321
|
testID="feedback-email-input"
|
|
322
|
-
colors={colors
|
|
322
|
+
colors={colors}
|
|
323
323
|
styles={styles}
|
|
324
324
|
accessibilityLabel="Email address"
|
|
325
325
|
accessibilityHint="Enter your email so we can respond"
|
|
@@ -494,7 +494,7 @@ const FeedbackScreen: React.FC<BaseScreenProps> = ({
|
|
|
494
494
|
keyboardShouldPersistTaps="handled"
|
|
495
495
|
>
|
|
496
496
|
{feedbackState.status !== 'success' && (
|
|
497
|
-
<ProgressIndicator currentStep={currentStep} totalSteps={4} colors={colors
|
|
497
|
+
<ProgressIndicator currentStep={currentStep} totalSteps={4} colors={colors} styles={styles} />
|
|
498
498
|
)}
|
|
499
499
|
{renderCurrentStep()}
|
|
500
500
|
</ScrollView>
|
|
@@ -1506,8 +1506,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
1506
1506
|
{file.metadata.description}
|
|
1507
1507
|
</Text>
|
|
1508
1508
|
) : undefined,
|
|
1509
|
-
|
|
1510
|
-
} as any;
|
|
1509
|
+
};
|
|
1511
1510
|
});
|
|
1512
1511
|
}, [filteredFiles, theme, themeStyles, deleting, handleFileDownload, handleFileDelete, handleFileOpen, getSafeDownloadUrlCallback, selectMode, selectedIds]);
|
|
1513
1512
|
|
|
@@ -41,7 +41,11 @@ const HistoryViewScreen: React.FC<BaseScreenProps> = ({
|
|
|
41
41
|
if (isReactNative) {
|
|
42
42
|
try {
|
|
43
43
|
const asyncStorageModule = await import('@react-native-async-storage/async-storage');
|
|
44
|
-
const storage =
|
|
44
|
+
const storage = asyncStorageModule.default as unknown as {
|
|
45
|
+
getItem: (key: string) => Promise<string | null>;
|
|
46
|
+
setItem: (key: string, value: string) => Promise<void>;
|
|
47
|
+
removeItem: (key: string) => Promise<void>;
|
|
48
|
+
};
|
|
45
49
|
return {
|
|
46
50
|
getItem: storage.getItem.bind(storage),
|
|
47
51
|
setItem: storage.setItem.bind(storage),
|
|
@@ -915,7 +915,7 @@ const PremiumSubscriptionScreen: React.FC<BaseScreenProps> = ({
|
|
|
915
915
|
<View style={styles.featureHeader}>
|
|
916
916
|
<View style={styles.featureIconContainer}>
|
|
917
917
|
<Ionicons
|
|
918
|
-
name={getCategoryIcon(feature.category) as
|
|
918
|
+
name={getCategoryIcon(feature.category) as React.ComponentProps<typeof Ionicons>['name']}
|
|
919
919
|
size={24}
|
|
920
920
|
color={getCategoryColor(feature.category)}
|
|
921
921
|
/>
|
|
@@ -292,7 +292,7 @@ const KarmaRewardsScreen: React.FC<BaseScreenProps> = ({ goBack, theme }) => {
|
|
|
292
292
|
{isLocked ? (
|
|
293
293
|
<Ionicons name="lock-closed" size={40} color="#8E8E93" />
|
|
294
294
|
) : (
|
|
295
|
-
<Ionicons name={achievement.icon as
|
|
295
|
+
<Ionicons name={achievement.icon as React.ComponentProps<typeof Ionicons>['name']} size={40} color="#FFFFFF" />
|
|
296
296
|
)}
|
|
297
297
|
</View>
|
|
298
298
|
|
|
@@ -5,12 +5,12 @@ import type { FileMetadata } from '@oxyhq/core';
|
|
|
5
5
|
function shallowEqualFile(a: FileMetadata, b: FileMetadata): boolean {
|
|
6
6
|
if (a === b) return true;
|
|
7
7
|
if (!a || !b) return false;
|
|
8
|
-
const aKeys = Object.keys(a)
|
|
9
|
-
const bKeys = Object.keys(b)
|
|
8
|
+
const aKeys = Object.keys(a);
|
|
9
|
+
const bKeys = Object.keys(b);
|
|
10
10
|
if (aKeys.length !== bKeys.length) return false;
|
|
11
11
|
for (const k of aKeys) {
|
|
12
12
|
// treat metadata/variants shallowly by reference
|
|
13
|
-
if (
|
|
13
|
+
if (a[k as keyof FileMetadata] !== b[k as keyof FileMetadata]) return false;
|
|
14
14
|
}
|
|
15
15
|
return true;
|
|
16
16
|
}
|
|
@@ -60,7 +60,7 @@ export const useFileStore = create<FileState>((set, get) => ({
|
|
|
60
60
|
if (sameOrder) {
|
|
61
61
|
sameFiles = order.every(id => state.files[id] && shallowEqualFile(state.files[id], map[id] as FileMetadata));
|
|
62
62
|
}
|
|
63
|
-
if (sameOrder && sameFiles) return
|
|
63
|
+
if (sameOrder && sameFiles) return state;
|
|
64
64
|
return { files: map, order };
|
|
65
65
|
}
|
|
66
66
|
const newFiles = { ...state.files };
|
|
@@ -72,13 +72,13 @@ export const useFileStore = create<FileState>((set, get) => ({
|
|
|
72
72
|
if (!prev || !shallowEqualFile(prev, merged)) { newFiles[f.id] = merged; changed = true; }
|
|
73
73
|
if (!newOrder.includes(f.id)) { newOrder.unshift(f.id); changed = true; }
|
|
74
74
|
});
|
|
75
|
-
if (!changed) return
|
|
75
|
+
if (!changed) return state;
|
|
76
76
|
return { files: newFiles, order: newOrder };
|
|
77
77
|
}),
|
|
78
78
|
addFile: (file, opts) => set(state => {
|
|
79
79
|
const prepend = opts?.prepend !== false; // default true
|
|
80
80
|
if (state.files[file.id]) {
|
|
81
|
-
if (shallowEqualFile(state.files[file.id], file)) return
|
|
81
|
+
if (shallowEqualFile(state.files[file.id], file)) return state;
|
|
82
82
|
return { files: { ...state.files, [file.id]: file } };
|
|
83
83
|
}
|
|
84
84
|
return {
|
|
@@ -88,13 +88,13 @@ export const useFileStore = create<FileState>((set, get) => ({
|
|
|
88
88
|
}),
|
|
89
89
|
updateFile: (id, patch) => set(state => {
|
|
90
90
|
const existing = state.files[id];
|
|
91
|
-
if (!existing) return
|
|
91
|
+
if (!existing) return state;
|
|
92
92
|
const updated = { ...existing, ...patch } as FileMetadata;
|
|
93
|
-
if (shallowEqualFile(existing, updated)) return
|
|
93
|
+
if (shallowEqualFile(existing, updated)) return state;
|
|
94
94
|
return { files: { ...state.files, [id]: updated } };
|
|
95
95
|
}),
|
|
96
96
|
removeFile: (id) => set(state => {
|
|
97
|
-
if (!state.files[id]) return
|
|
97
|
+
if (!state.files[id]) return state;
|
|
98
98
|
const { [id]: _removed, ...rest } = state.files;
|
|
99
99
|
const newOrder = state.order.filter(fid => fid !== id);
|
|
100
100
|
return { files: rest, order: newOrder };
|