@oxyhq/auth 1.0.0

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 (119) hide show
  1. package/README.md +56 -0
  2. package/dist/cjs/WebOxyProvider.js +287 -0
  3. package/dist/cjs/hooks/mutations/index.js +23 -0
  4. package/dist/cjs/hooks/mutations/mutationFactory.js +126 -0
  5. package/dist/cjs/hooks/mutations/useAccountMutations.js +275 -0
  6. package/dist/cjs/hooks/mutations/useServicesMutations.js +149 -0
  7. package/dist/cjs/hooks/queries/index.js +35 -0
  8. package/dist/cjs/hooks/queries/queryKeys.js +82 -0
  9. package/dist/cjs/hooks/queries/useAccountQueries.js +141 -0
  10. package/dist/cjs/hooks/queries/useSecurityQueries.js +45 -0
  11. package/dist/cjs/hooks/queries/useServicesQueries.js +113 -0
  12. package/dist/cjs/hooks/queryClient.js +110 -0
  13. package/dist/cjs/hooks/useAssets.js +225 -0
  14. package/dist/cjs/hooks/useFileDownloadUrl.js +91 -0
  15. package/dist/cjs/hooks/useFileFiltering.js +81 -0
  16. package/dist/cjs/hooks/useFollow.js +159 -0
  17. package/dist/cjs/hooks/useFollow.types.js +4 -0
  18. package/dist/cjs/hooks/useQueryClient.js +16 -0
  19. package/dist/cjs/hooks/useSessionSocket.js +215 -0
  20. package/dist/cjs/hooks/useWebSSO.js +146 -0
  21. package/dist/cjs/index.js +115 -0
  22. package/dist/cjs/stores/accountStore.js +226 -0
  23. package/dist/cjs/stores/assetStore.js +192 -0
  24. package/dist/cjs/stores/authStore.js +47 -0
  25. package/dist/cjs/stores/followStore.js +154 -0
  26. package/dist/cjs/utils/authHelpers.js +154 -0
  27. package/dist/cjs/utils/avatarUtils.js +77 -0
  28. package/dist/cjs/utils/errorHandlers.js +128 -0
  29. package/dist/cjs/utils/sessionHelpers.js +90 -0
  30. package/dist/cjs/utils/storageHelpers.js +147 -0
  31. package/dist/esm/WebOxyProvider.js +282 -0
  32. package/dist/esm/hooks/mutations/index.js +10 -0
  33. package/dist/esm/hooks/mutations/mutationFactory.js +122 -0
  34. package/dist/esm/hooks/mutations/useAccountMutations.js +267 -0
  35. package/dist/esm/hooks/mutations/useServicesMutations.js +141 -0
  36. package/dist/esm/hooks/queries/index.js +14 -0
  37. package/dist/esm/hooks/queries/queryKeys.js +76 -0
  38. package/dist/esm/hooks/queries/useAccountQueries.js +131 -0
  39. package/dist/esm/hooks/queries/useSecurityQueries.js +40 -0
  40. package/dist/esm/hooks/queries/useServicesQueries.js +105 -0
  41. package/dist/esm/hooks/queryClient.js +104 -0
  42. package/dist/esm/hooks/useAssets.js +220 -0
  43. package/dist/esm/hooks/useFileDownloadUrl.js +86 -0
  44. package/dist/esm/hooks/useFileFiltering.js +78 -0
  45. package/dist/esm/hooks/useFollow.js +154 -0
  46. package/dist/esm/hooks/useFollow.types.js +3 -0
  47. package/dist/esm/hooks/useQueryClient.js +12 -0
  48. package/dist/esm/hooks/useSessionSocket.js +209 -0
  49. package/dist/esm/hooks/useWebSSO.js +143 -0
  50. package/dist/esm/index.js +48 -0
  51. package/dist/esm/stores/accountStore.js +219 -0
  52. package/dist/esm/stores/assetStore.js +180 -0
  53. package/dist/esm/stores/authStore.js +44 -0
  54. package/dist/esm/stores/followStore.js +151 -0
  55. package/dist/esm/utils/authHelpers.js +145 -0
  56. package/dist/esm/utils/avatarUtils.js +72 -0
  57. package/dist/esm/utils/errorHandlers.js +121 -0
  58. package/dist/esm/utils/sessionHelpers.js +84 -0
  59. package/dist/esm/utils/storageHelpers.js +108 -0
  60. package/dist/types/WebOxyProvider.d.ts +97 -0
  61. package/dist/types/hooks/mutations/index.d.ts +8 -0
  62. package/dist/types/hooks/mutations/mutationFactory.d.ts +75 -0
  63. package/dist/types/hooks/mutations/useAccountMutations.d.ts +68 -0
  64. package/dist/types/hooks/mutations/useServicesMutations.d.ts +22 -0
  65. package/dist/types/hooks/queries/index.d.ts +10 -0
  66. package/dist/types/hooks/queries/queryKeys.d.ts +64 -0
  67. package/dist/types/hooks/queries/useAccountQueries.d.ts +42 -0
  68. package/dist/types/hooks/queries/useSecurityQueries.d.ts +14 -0
  69. package/dist/types/hooks/queries/useServicesQueries.d.ts +31 -0
  70. package/dist/types/hooks/queryClient.d.ts +18 -0
  71. package/dist/types/hooks/useAssets.d.ts +34 -0
  72. package/dist/types/hooks/useFileDownloadUrl.d.ts +18 -0
  73. package/dist/types/hooks/useFileFiltering.d.ts +28 -0
  74. package/dist/types/hooks/useFollow.d.ts +61 -0
  75. package/dist/types/hooks/useFollow.types.d.ts +32 -0
  76. package/dist/types/hooks/useQueryClient.d.ts +6 -0
  77. package/dist/types/hooks/useSessionSocket.d.ts +13 -0
  78. package/dist/types/hooks/useWebSSO.d.ts +57 -0
  79. package/dist/types/index.d.ts +46 -0
  80. package/dist/types/stores/accountStore.d.ts +33 -0
  81. package/dist/types/stores/assetStore.d.ts +53 -0
  82. package/dist/types/stores/authStore.d.ts +16 -0
  83. package/dist/types/stores/followStore.d.ts +24 -0
  84. package/dist/types/utils/authHelpers.d.ts +98 -0
  85. package/dist/types/utils/avatarUtils.d.ts +33 -0
  86. package/dist/types/utils/errorHandlers.d.ts +34 -0
  87. package/dist/types/utils/sessionHelpers.d.ts +63 -0
  88. package/dist/types/utils/storageHelpers.d.ts +27 -0
  89. package/package.json +71 -0
  90. package/src/WebOxyProvider.tsx +372 -0
  91. package/src/global.d.ts +1 -0
  92. package/src/hooks/mutations/index.ts +25 -0
  93. package/src/hooks/mutations/mutationFactory.ts +215 -0
  94. package/src/hooks/mutations/useAccountMutations.ts +344 -0
  95. package/src/hooks/mutations/useServicesMutations.ts +164 -0
  96. package/src/hooks/queries/index.ts +36 -0
  97. package/src/hooks/queries/queryKeys.ts +88 -0
  98. package/src/hooks/queries/useAccountQueries.ts +152 -0
  99. package/src/hooks/queries/useSecurityQueries.ts +64 -0
  100. package/src/hooks/queries/useServicesQueries.ts +126 -0
  101. package/src/hooks/queryClient.ts +112 -0
  102. package/src/hooks/useAssets.ts +291 -0
  103. package/src/hooks/useFileDownloadUrl.ts +118 -0
  104. package/src/hooks/useFileFiltering.ts +115 -0
  105. package/src/hooks/useFollow.ts +175 -0
  106. package/src/hooks/useFollow.types.ts +33 -0
  107. package/src/hooks/useQueryClient.ts +17 -0
  108. package/src/hooks/useSessionSocket.ts +233 -0
  109. package/src/hooks/useWebSSO.ts +187 -0
  110. package/src/index.ts +144 -0
  111. package/src/stores/accountStore.ts +296 -0
  112. package/src/stores/assetStore.ts +281 -0
  113. package/src/stores/authStore.ts +63 -0
  114. package/src/stores/followStore.ts +181 -0
  115. package/src/utils/authHelpers.ts +183 -0
  116. package/src/utils/avatarUtils.ts +103 -0
  117. package/src/utils/errorHandlers.ts +194 -0
  118. package/src/utils/sessionHelpers.ts +151 -0
  119. package/src/utils/storageHelpers.ts +130 -0
@@ -0,0 +1,219 @@
1
+ import { create } from 'zustand';
2
+ const initialState = {
3
+ accounts: {},
4
+ accountOrder: [],
5
+ accountsArray: [],
6
+ loading: false,
7
+ loadingSessionIds: new Set(),
8
+ error: null,
9
+ };
10
+ // Helper: Build accounts array from accounts map and order
11
+ const buildAccountsArray = (accounts, order) => {
12
+ const result = [];
13
+ for (const id of order) {
14
+ const account = accounts[id];
15
+ if (account)
16
+ result.push(account);
17
+ }
18
+ return result;
19
+ };
20
+ // Helper: Create QuickAccount from user data
21
+ const createQuickAccount = (sessionId, userData, existingAccount, oxyServices) => {
22
+ const displayName = userData.name?.full || userData.name?.first || userData.username || 'Account';
23
+ const userId = userData.id || userData._id?.toString();
24
+ // Preserve existing avatarUrl if avatar hasn't changed (prevents image reload)
25
+ let avatarUrl;
26
+ if (existingAccount && existingAccount.avatar === userData.avatar && existingAccount.avatarUrl) {
27
+ avatarUrl = existingAccount.avatarUrl; // Reuse existing URL
28
+ }
29
+ else if (userData.avatar && oxyServices) {
30
+ avatarUrl = oxyServices.getFileDownloadUrl(userData.avatar, 'thumb');
31
+ }
32
+ return {
33
+ sessionId,
34
+ userId,
35
+ username: userData.username || '',
36
+ displayName,
37
+ avatar: userData.avatar,
38
+ avatarUrl,
39
+ };
40
+ };
41
+ export const useAccountStore = create((set, get) => ({
42
+ ...initialState,
43
+ setAccounts: (accounts) => set((state) => {
44
+ const accountMap = {};
45
+ const order = [];
46
+ const seenSessionIds = new Set();
47
+ for (const account of accounts) {
48
+ if (seenSessionIds.has(account.sessionId))
49
+ continue;
50
+ seenSessionIds.add(account.sessionId);
51
+ accountMap[account.sessionId] = account;
52
+ order.push(account.sessionId);
53
+ }
54
+ const accountsArray = buildAccountsArray(accountMap, order);
55
+ const sameOrder = order.length === state.accountOrder.length &&
56
+ order.every((id, i) => id === state.accountOrder[i]);
57
+ const sameAccounts = sameOrder &&
58
+ order.every(id => {
59
+ const existing = state.accounts[id];
60
+ const newAccount = accountMap[id];
61
+ return existing &&
62
+ existing.sessionId === newAccount.sessionId &&
63
+ existing.userId === newAccount.userId &&
64
+ existing.avatar === newAccount.avatar &&
65
+ existing.avatarUrl === newAccount.avatarUrl;
66
+ });
67
+ if (sameAccounts)
68
+ return {};
69
+ return { accounts: accountMap, accountOrder: order, accountsArray };
70
+ }),
71
+ addAccount: (account) => set((state) => {
72
+ // Check if account with same sessionId exists
73
+ if (state.accounts[account.sessionId]) {
74
+ // Update existing
75
+ const existing = state.accounts[account.sessionId];
76
+ if (existing.avatar === account.avatar && existing.avatarUrl === account.avatarUrl) {
77
+ return {}; // No change
78
+ }
79
+ const newAccounts = { ...state.accounts, [account.sessionId]: account };
80
+ return {
81
+ accounts: newAccounts,
82
+ accountsArray: buildAccountsArray(newAccounts, state.accountOrder),
83
+ };
84
+ }
85
+ const newAccounts = { ...state.accounts, [account.sessionId]: account };
86
+ const newOrder = [account.sessionId, ...state.accountOrder];
87
+ return {
88
+ accounts: newAccounts,
89
+ accountOrder: newOrder,
90
+ accountsArray: buildAccountsArray(newAccounts, newOrder),
91
+ };
92
+ }),
93
+ updateAccount: (sessionId, updates) => set((state) => {
94
+ const existing = state.accounts[sessionId];
95
+ if (!existing)
96
+ return {};
97
+ const updated = { ...existing, ...updates };
98
+ if (existing.avatar === updated.avatar && existing.avatarUrl === updated.avatarUrl) {
99
+ return {}; // No change
100
+ }
101
+ const newAccounts = { ...state.accounts, [sessionId]: updated };
102
+ return {
103
+ accounts: newAccounts,
104
+ accountsArray: buildAccountsArray(newAccounts, state.accountOrder),
105
+ };
106
+ }),
107
+ removeAccount: (sessionId) => set((state) => {
108
+ if (!state.accounts[sessionId])
109
+ return {};
110
+ const { [sessionId]: _removed, ...rest } = state.accounts;
111
+ const newOrder = state.accountOrder.filter(id => id !== sessionId);
112
+ return {
113
+ accounts: rest,
114
+ accountOrder: newOrder,
115
+ accountsArray: buildAccountsArray(rest, newOrder),
116
+ };
117
+ }),
118
+ moveAccountToTop: (sessionId) => set((state) => {
119
+ if (!state.accounts[sessionId])
120
+ return {};
121
+ const filtered = state.accountOrder.filter(id => id !== sessionId);
122
+ const newOrder = [sessionId, ...filtered];
123
+ return {
124
+ accountOrder: newOrder,
125
+ accountsArray: buildAccountsArray(state.accounts, newOrder),
126
+ };
127
+ }),
128
+ setLoading: (loading) => set({ loading }),
129
+ setLoadingSession: (sessionId, loading) => set((state) => {
130
+ const newSet = new Set(state.loadingSessionIds);
131
+ if (loading) {
132
+ newSet.add(sessionId);
133
+ }
134
+ else {
135
+ newSet.delete(sessionId);
136
+ }
137
+ return { loadingSessionIds: newSet };
138
+ }),
139
+ setError: (error) => set({ error }),
140
+ loadAccounts: async (sessionIds, oxyServices, existingAccounts = [], preserveOrder = true) => {
141
+ const state = get();
142
+ const uniqueSessionIds = Array.from(new Set(sessionIds));
143
+ if (uniqueSessionIds.length === 0) {
144
+ get().setAccounts([]);
145
+ return;
146
+ }
147
+ // Try to get data from TanStack Query cache first
148
+ try {
149
+ // This will be called from a component, so we need to access queryClient differently
150
+ // For now, we'll keep the API call but optimize it
151
+ const existingMap = new Map(existingAccounts.map(a => [a.sessionId, a]));
152
+ for (const account of Object.values(state.accounts)) {
153
+ existingMap.set(account.sessionId, account);
154
+ }
155
+ const missingSessionIds = uniqueSessionIds.filter(id => !existingMap.has(id));
156
+ if (missingSessionIds.length === 0) {
157
+ const ordered = uniqueSessionIds
158
+ .map(id => existingMap.get(id))
159
+ .filter((acc) => acc !== undefined);
160
+ get().setAccounts(ordered);
161
+ return;
162
+ }
163
+ if (state.loading) {
164
+ return;
165
+ }
166
+ set({ loading: true, error: null });
167
+ try {
168
+ const batchResults = await oxyServices.getUsersBySessions(missingSessionIds);
169
+ const accountMap = new Map();
170
+ for (const { sessionId, user: userData } of batchResults) {
171
+ if (userData && !accountMap.has(sessionId)) {
172
+ const existing = existingMap.get(sessionId);
173
+ accountMap.set(sessionId, createQuickAccount(sessionId, userData, existing, oxyServices));
174
+ }
175
+ }
176
+ for (const [sessionId, account] of accountMap) {
177
+ existingMap.set(sessionId, account);
178
+ }
179
+ const orderToUse = preserveOrder ? uniqueSessionIds : [...uniqueSessionIds, ...state.accountOrder];
180
+ const seen = new Set();
181
+ const ordered = [];
182
+ for (const sessionId of orderToUse) {
183
+ if (seen.has(sessionId))
184
+ continue;
185
+ seen.add(sessionId);
186
+ const account = existingMap.get(sessionId);
187
+ if (account)
188
+ ordered.push(account);
189
+ }
190
+ get().setAccounts(ordered);
191
+ }
192
+ catch (error) {
193
+ const errorMessage = error instanceof Error ? error.message : 'Failed to load accounts';
194
+ if (__DEV__) {
195
+ console.error('AccountStore: Failed to load accounts:', error);
196
+ }
197
+ set({ error: errorMessage });
198
+ }
199
+ finally {
200
+ set({ loading: false });
201
+ }
202
+ }
203
+ catch (error) {
204
+ const errorMessage = error instanceof Error ? error.message : 'Failed to load accounts';
205
+ if (__DEV__) {
206
+ console.error('AccountStore: Failed to load accounts:', error);
207
+ }
208
+ set({ error: errorMessage, loading: false });
209
+ }
210
+ },
211
+ reset: () => set(initialState),
212
+ }));
213
+ // Selectors for performance - return cached array to prevent infinite loops
214
+ export const useAccounts = () => {
215
+ return useAccountStore(state => state.accountsArray);
216
+ };
217
+ export const useAccountLoading = () => useAccountStore(s => s.loading);
218
+ export const useAccountError = () => useAccountStore(s => s.error);
219
+ export const useAccountLoadingSession = (sessionId) => useAccountStore(s => s.loadingSessionIds.has(sessionId));
@@ -0,0 +1,180 @@
1
+ import { create } from 'zustand';
2
+ const initialState = {
3
+ assets: {},
4
+ uploadProgress: {},
5
+ loading: {
6
+ uploading: false,
7
+ linking: false,
8
+ deleting: false,
9
+ },
10
+ errors: {},
11
+ };
12
+ export const useAssetStore = create((set, get) => ({
13
+ ...initialState,
14
+ // Asset management
15
+ setAsset: (asset) => {
16
+ set((state) => ({
17
+ assets: {
18
+ ...state.assets,
19
+ [asset.id]: asset,
20
+ },
21
+ }));
22
+ },
23
+ setAssets: (assets) => {
24
+ set((state) => {
25
+ const assetMap = assets.reduce((acc, asset) => {
26
+ acc[asset.id] = asset;
27
+ return acc;
28
+ }, {});
29
+ return {
30
+ assets: {
31
+ ...state.assets,
32
+ ...assetMap,
33
+ },
34
+ };
35
+ });
36
+ },
37
+ removeAsset: (assetId) => {
38
+ set((state) => {
39
+ const { [assetId]: removed, ...rest } = state.assets;
40
+ return { assets: rest };
41
+ });
42
+ },
43
+ // Upload progress
44
+ setUploadProgress: (fileId, progress) => {
45
+ set((state) => ({
46
+ uploadProgress: {
47
+ ...state.uploadProgress,
48
+ [fileId]: progress,
49
+ },
50
+ }));
51
+ },
52
+ removeUploadProgress: (fileId) => {
53
+ set((state) => {
54
+ const { [fileId]: removed, ...rest } = state.uploadProgress;
55
+ return { uploadProgress: rest };
56
+ });
57
+ },
58
+ // Link management
59
+ addLink: (assetId, link) => {
60
+ set((state) => {
61
+ const asset = state.assets[assetId];
62
+ if (!asset)
63
+ return state;
64
+ // Check if link already exists
65
+ const existingLink = asset.links.find((l) => l.app === link.app &&
66
+ l.entityType === link.entityType &&
67
+ l.entityId === link.entityId);
68
+ if (existingLink)
69
+ return state;
70
+ const updatedAsset = {
71
+ ...asset,
72
+ links: [...asset.links, link],
73
+ usageCount: asset.links.length + 1,
74
+ };
75
+ return {
76
+ assets: {
77
+ ...state.assets,
78
+ [assetId]: updatedAsset,
79
+ },
80
+ };
81
+ });
82
+ },
83
+ removeLink: (assetId, app, entityType, entityId) => {
84
+ set((state) => {
85
+ const asset = state.assets[assetId];
86
+ if (!asset)
87
+ return state;
88
+ const filteredLinks = asset.links.filter((link) => !(link.app === app &&
89
+ link.entityType === entityType &&
90
+ link.entityId === entityId));
91
+ const updatedAsset = {
92
+ ...asset,
93
+ links: filteredLinks,
94
+ usageCount: filteredLinks.length,
95
+ status: filteredLinks.length === 0 ? 'trash' : asset.status,
96
+ };
97
+ return {
98
+ assets: {
99
+ ...state.assets,
100
+ [assetId]: updatedAsset,
101
+ },
102
+ };
103
+ });
104
+ },
105
+ // Loading states
106
+ setUploading: (uploading) => {
107
+ set((state) => ({
108
+ loading: { ...state.loading, uploading },
109
+ }));
110
+ },
111
+ setLinking: (linking) => {
112
+ set((state) => ({
113
+ loading: { ...state.loading, linking },
114
+ }));
115
+ },
116
+ setDeleting: (deleting) => {
117
+ set((state) => ({
118
+ loading: { ...state.loading, deleting },
119
+ }));
120
+ },
121
+ // Error management
122
+ setUploadError: (error) => {
123
+ set((state) => ({
124
+ errors: { ...state.errors, upload: error },
125
+ }));
126
+ },
127
+ setLinkError: (error) => {
128
+ set((state) => ({
129
+ errors: { ...state.errors, link: error },
130
+ }));
131
+ },
132
+ setDeleteError: (error) => {
133
+ set((state) => ({
134
+ errors: { ...state.errors, delete: error },
135
+ }));
136
+ },
137
+ clearErrors: () => {
138
+ set({ errors: {} });
139
+ },
140
+ // Utility methods
141
+ getAssetsByApp: (app) => {
142
+ const { assets } = get();
143
+ return Object.values(assets).filter((asset) => asset.links.some((link) => link.app === app));
144
+ },
145
+ getAssetsByEntity: (app, entityType, entityId) => {
146
+ const { assets } = get();
147
+ return Object.values(assets).filter((asset) => asset.links.some((link) => link.app === app &&
148
+ link.entityType === entityType &&
149
+ link.entityId === entityId));
150
+ },
151
+ getAssetUsageCount: (assetId) => {
152
+ const { assets } = get();
153
+ const asset = assets[assetId];
154
+ return asset ? asset.usageCount : 0;
155
+ },
156
+ isAssetLinked: (assetId, app, entityType, entityId) => {
157
+ const { assets } = get();
158
+ const asset = assets[assetId];
159
+ if (!asset)
160
+ return false;
161
+ return asset.links.some((link) => link.app === app &&
162
+ link.entityType === entityType &&
163
+ link.entityId === entityId);
164
+ },
165
+ // Reset store
166
+ reset: () => {
167
+ set(initialState);
168
+ },
169
+ }));
170
+ // Selector hooks for convenience
171
+ export const useAssets = () => useAssetStore((state) => Object.values(state.assets));
172
+ export const useAsset = (assetId) => useAssetStore((state) => state.assets[assetId]);
173
+ export const useUploadProgress = () => useAssetStore((state) => state.uploadProgress);
174
+ export const useAssetLoading = () => useAssetStore((state) => state.loading);
175
+ export const useAssetErrors = () => useAssetStore((state) => state.errors);
176
+ // Typed selectors for specific use cases
177
+ export const useAssetsByApp = (app) => useAssetStore((state) => state.getAssetsByApp(app));
178
+ export const useAssetsByEntity = (app, entityType, entityId) => useAssetStore((state) => state.getAssetsByEntity(app, entityType, entityId));
179
+ export const useAssetUsageCount = (assetId) => useAssetStore((state) => state.getAssetUsageCount(assetId));
180
+ export const useIsAssetLinked = (assetId, app, entityType, entityId) => useAssetStore((state) => state.isAssetLinked(assetId, app, entityType, entityId));
@@ -0,0 +1,44 @@
1
+ import { create } from 'zustand';
2
+ import { createDebugLogger } from '@oxyhq/core';
3
+ const debug = createDebugLogger('AuthStore');
4
+ export const useAuthStore = create((set, get) => ({
5
+ user: null,
6
+ isAuthenticated: false,
7
+ isLoading: false,
8
+ error: null,
9
+ lastUserFetch: null,
10
+ loginSuccess: (user) => set({
11
+ isLoading: false,
12
+ isAuthenticated: true,
13
+ user,
14
+ lastUserFetch: Date.now(),
15
+ }),
16
+ loginFailure: (error) => set({ isLoading: false, error }),
17
+ logout: () => set({
18
+ user: null,
19
+ isAuthenticated: false,
20
+ lastUserFetch: null,
21
+ }),
22
+ setUser: (user) => set({ user, lastUserFetch: Date.now() }),
23
+ fetchUser: async (oxyServices, forceRefresh = false) => {
24
+ const state = get();
25
+ const now = Date.now();
26
+ const cacheAge = state.lastUserFetch ? now - state.lastUserFetch : Number.POSITIVE_INFINITY;
27
+ const cacheValid = cacheAge < 5 * 60 * 1000; // 5 minutes cache
28
+ // Use cached data if available and not forcing refresh
29
+ if (!forceRefresh && state.user && cacheValid) {
30
+ debug.log('Using cached user data (age:', cacheAge, 'ms)');
31
+ return;
32
+ }
33
+ set({ isLoading: true, error: null });
34
+ try {
35
+ const user = await oxyServices.getCurrentUser();
36
+ set({ user, isLoading: false, isAuthenticated: true, lastUserFetch: now });
37
+ }
38
+ catch (error) {
39
+ const errorMessage = error instanceof Error ? error.message : 'Failed to fetch user';
40
+ debug.error('Error fetching user:', error);
41
+ set({ error: errorMessage, isLoading: false });
42
+ }
43
+ },
44
+ }));
@@ -0,0 +1,151 @@
1
+ import { create } from 'zustand';
2
+ export const useFollowStore = create((set, get) => ({
3
+ followingUsers: {},
4
+ loadingUsers: {},
5
+ fetchingUsers: {},
6
+ errors: {},
7
+ followerCounts: {},
8
+ followingCounts: {},
9
+ loadingCounts: {},
10
+ setFollowingStatus: (userId, isFollowing) => set((state) => ({
11
+ followingUsers: { ...state.followingUsers, [userId]: isFollowing },
12
+ errors: { ...state.errors, [userId]: null },
13
+ })),
14
+ clearFollowError: (userId) => set((state) => ({
15
+ errors: { ...state.errors, [userId]: null },
16
+ })),
17
+ resetFollowState: () => set({
18
+ followingUsers: {},
19
+ loadingUsers: {},
20
+ fetchingUsers: {},
21
+ errors: {},
22
+ followerCounts: {},
23
+ followingCounts: {},
24
+ loadingCounts: {},
25
+ }),
26
+ fetchFollowStatus: async (userId, oxyServices) => {
27
+ set((state) => ({
28
+ fetchingUsers: { ...state.fetchingUsers, [userId]: true },
29
+ errors: { ...state.errors, [userId]: null },
30
+ }));
31
+ try {
32
+ const response = await oxyServices.getFollowStatus(userId);
33
+ set((state) => ({
34
+ followingUsers: { ...state.followingUsers, [userId]: response.isFollowing },
35
+ fetchingUsers: { ...state.fetchingUsers, [userId]: false },
36
+ errors: { ...state.errors, [userId]: null },
37
+ }));
38
+ }
39
+ catch (error) {
40
+ set((state) => ({
41
+ fetchingUsers: { ...state.fetchingUsers, [userId]: false },
42
+ errors: { ...state.errors, [userId]: error?.message || 'Failed to fetch follow status' },
43
+ }));
44
+ }
45
+ },
46
+ toggleFollowUser: async (userId, oxyServices, isCurrentlyFollowing) => {
47
+ set((state) => ({
48
+ loadingUsers: { ...state.loadingUsers, [userId]: true },
49
+ errors: { ...state.errors, [userId]: null },
50
+ }));
51
+ try {
52
+ let response;
53
+ let newFollowState;
54
+ if (isCurrentlyFollowing) {
55
+ response = await oxyServices.unfollowUser(userId);
56
+ newFollowState = false;
57
+ }
58
+ else {
59
+ response = await oxyServices.followUser(userId);
60
+ newFollowState = true;
61
+ }
62
+ // Update follow status
63
+ set((state) => ({
64
+ followingUsers: { ...state.followingUsers, [userId]: newFollowState },
65
+ loadingUsers: { ...state.loadingUsers, [userId]: false },
66
+ errors: { ...state.errors, [userId]: null },
67
+ }));
68
+ // Update counts if the response includes them
69
+ // The API returns counts for both users:
70
+ // - followers: target user's follower count (the user being followed)
71
+ // - following: current user's following count (the user doing the following)
72
+ if (response && response.counts) {
73
+ const { counts } = response;
74
+ // Get current user ID from oxyServices
75
+ const currentUserId = oxyServices.getCurrentUserId();
76
+ set((state) => {
77
+ const updates = {};
78
+ // Update target user's follower count (the user being followed)
79
+ updates.followerCounts = {
80
+ ...state.followerCounts,
81
+ [userId]: counts.followers
82
+ };
83
+ // Update current user's following count (the user doing the following)
84
+ if (currentUserId) {
85
+ updates.followingCounts = {
86
+ ...state.followingCounts,
87
+ [currentUserId]: counts.following
88
+ };
89
+ }
90
+ return updates;
91
+ });
92
+ }
93
+ }
94
+ catch (error) {
95
+ set((state) => ({
96
+ loadingUsers: { ...state.loadingUsers, [userId]: false },
97
+ errors: { ...state.errors, [userId]: error?.message || 'Failed to update follow status' },
98
+ }));
99
+ }
100
+ },
101
+ setFollowerCount: (userId, count) => set((state) => ({
102
+ followerCounts: { ...state.followerCounts, [userId]: count },
103
+ })),
104
+ setFollowingCount: (userId, count) => set((state) => ({
105
+ followingCounts: { ...state.followingCounts, [userId]: count },
106
+ })),
107
+ updateCountsFromFollowAction: (targetUserId, action, counts, currentUserId) => {
108
+ set((state) => {
109
+ const updates = {};
110
+ // Update target user's follower count (the user being followed)
111
+ updates.followerCounts = {
112
+ ...state.followerCounts,
113
+ [targetUserId]: counts.followers
114
+ };
115
+ // Update current user's following count (the user doing the following)
116
+ if (currentUserId) {
117
+ updates.followingCounts = {
118
+ ...state.followingCounts,
119
+ [currentUserId]: counts.following
120
+ };
121
+ }
122
+ return updates;
123
+ });
124
+ },
125
+ fetchUserCounts: async (userId, oxyServices) => {
126
+ set((state) => ({
127
+ loadingCounts: { ...state.loadingCounts, [userId]: true },
128
+ }));
129
+ try {
130
+ const user = await oxyServices.getUserById(userId);
131
+ if (user && user._count) {
132
+ set((state) => ({
133
+ followerCounts: {
134
+ ...state.followerCounts,
135
+ [userId]: user._count?.followers || 0
136
+ },
137
+ followingCounts: {
138
+ ...state.followingCounts,
139
+ [userId]: user._count?.following || 0
140
+ },
141
+ loadingCounts: { ...state.loadingCounts, [userId]: false },
142
+ }));
143
+ }
144
+ }
145
+ catch (error) {
146
+ set((state) => ({
147
+ loadingCounts: { ...state.loadingCounts, [userId]: false },
148
+ }));
149
+ }
150
+ },
151
+ }));