@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.
- package/README.md +56 -0
- package/dist/cjs/WebOxyProvider.js +287 -0
- package/dist/cjs/hooks/mutations/index.js +23 -0
- package/dist/cjs/hooks/mutations/mutationFactory.js +126 -0
- package/dist/cjs/hooks/mutations/useAccountMutations.js +275 -0
- package/dist/cjs/hooks/mutations/useServicesMutations.js +149 -0
- package/dist/cjs/hooks/queries/index.js +35 -0
- package/dist/cjs/hooks/queries/queryKeys.js +82 -0
- package/dist/cjs/hooks/queries/useAccountQueries.js +141 -0
- package/dist/cjs/hooks/queries/useSecurityQueries.js +45 -0
- package/dist/cjs/hooks/queries/useServicesQueries.js +113 -0
- package/dist/cjs/hooks/queryClient.js +110 -0
- package/dist/cjs/hooks/useAssets.js +225 -0
- package/dist/cjs/hooks/useFileDownloadUrl.js +91 -0
- package/dist/cjs/hooks/useFileFiltering.js +81 -0
- package/dist/cjs/hooks/useFollow.js +159 -0
- package/dist/cjs/hooks/useFollow.types.js +4 -0
- package/dist/cjs/hooks/useQueryClient.js +16 -0
- package/dist/cjs/hooks/useSessionSocket.js +215 -0
- package/dist/cjs/hooks/useWebSSO.js +146 -0
- package/dist/cjs/index.js +115 -0
- package/dist/cjs/stores/accountStore.js +226 -0
- package/dist/cjs/stores/assetStore.js +192 -0
- package/dist/cjs/stores/authStore.js +47 -0
- package/dist/cjs/stores/followStore.js +154 -0
- package/dist/cjs/utils/authHelpers.js +154 -0
- package/dist/cjs/utils/avatarUtils.js +77 -0
- package/dist/cjs/utils/errorHandlers.js +128 -0
- package/dist/cjs/utils/sessionHelpers.js +90 -0
- package/dist/cjs/utils/storageHelpers.js +147 -0
- package/dist/esm/WebOxyProvider.js +282 -0
- package/dist/esm/hooks/mutations/index.js +10 -0
- package/dist/esm/hooks/mutations/mutationFactory.js +122 -0
- package/dist/esm/hooks/mutations/useAccountMutations.js +267 -0
- package/dist/esm/hooks/mutations/useServicesMutations.js +141 -0
- package/dist/esm/hooks/queries/index.js +14 -0
- package/dist/esm/hooks/queries/queryKeys.js +76 -0
- package/dist/esm/hooks/queries/useAccountQueries.js +131 -0
- package/dist/esm/hooks/queries/useSecurityQueries.js +40 -0
- package/dist/esm/hooks/queries/useServicesQueries.js +105 -0
- package/dist/esm/hooks/queryClient.js +104 -0
- package/dist/esm/hooks/useAssets.js +220 -0
- package/dist/esm/hooks/useFileDownloadUrl.js +86 -0
- package/dist/esm/hooks/useFileFiltering.js +78 -0
- package/dist/esm/hooks/useFollow.js +154 -0
- package/dist/esm/hooks/useFollow.types.js +3 -0
- package/dist/esm/hooks/useQueryClient.js +12 -0
- package/dist/esm/hooks/useSessionSocket.js +209 -0
- package/dist/esm/hooks/useWebSSO.js +143 -0
- package/dist/esm/index.js +48 -0
- package/dist/esm/stores/accountStore.js +219 -0
- package/dist/esm/stores/assetStore.js +180 -0
- package/dist/esm/stores/authStore.js +44 -0
- package/dist/esm/stores/followStore.js +151 -0
- package/dist/esm/utils/authHelpers.js +145 -0
- package/dist/esm/utils/avatarUtils.js +72 -0
- package/dist/esm/utils/errorHandlers.js +121 -0
- package/dist/esm/utils/sessionHelpers.js +84 -0
- package/dist/esm/utils/storageHelpers.js +108 -0
- package/dist/types/WebOxyProvider.d.ts +97 -0
- package/dist/types/hooks/mutations/index.d.ts +8 -0
- package/dist/types/hooks/mutations/mutationFactory.d.ts +75 -0
- package/dist/types/hooks/mutations/useAccountMutations.d.ts +68 -0
- package/dist/types/hooks/mutations/useServicesMutations.d.ts +22 -0
- package/dist/types/hooks/queries/index.d.ts +10 -0
- package/dist/types/hooks/queries/queryKeys.d.ts +64 -0
- package/dist/types/hooks/queries/useAccountQueries.d.ts +42 -0
- package/dist/types/hooks/queries/useSecurityQueries.d.ts +14 -0
- package/dist/types/hooks/queries/useServicesQueries.d.ts +31 -0
- package/dist/types/hooks/queryClient.d.ts +18 -0
- package/dist/types/hooks/useAssets.d.ts +34 -0
- package/dist/types/hooks/useFileDownloadUrl.d.ts +18 -0
- package/dist/types/hooks/useFileFiltering.d.ts +28 -0
- package/dist/types/hooks/useFollow.d.ts +61 -0
- package/dist/types/hooks/useFollow.types.d.ts +32 -0
- package/dist/types/hooks/useQueryClient.d.ts +6 -0
- package/dist/types/hooks/useSessionSocket.d.ts +13 -0
- package/dist/types/hooks/useWebSSO.d.ts +57 -0
- package/dist/types/index.d.ts +46 -0
- package/dist/types/stores/accountStore.d.ts +33 -0
- package/dist/types/stores/assetStore.d.ts +53 -0
- package/dist/types/stores/authStore.d.ts +16 -0
- package/dist/types/stores/followStore.d.ts +24 -0
- package/dist/types/utils/authHelpers.d.ts +98 -0
- package/dist/types/utils/avatarUtils.d.ts +33 -0
- package/dist/types/utils/errorHandlers.d.ts +34 -0
- package/dist/types/utils/sessionHelpers.d.ts +63 -0
- package/dist/types/utils/storageHelpers.d.ts +27 -0
- package/package.json +71 -0
- package/src/WebOxyProvider.tsx +372 -0
- package/src/global.d.ts +1 -0
- package/src/hooks/mutations/index.ts +25 -0
- package/src/hooks/mutations/mutationFactory.ts +215 -0
- package/src/hooks/mutations/useAccountMutations.ts +344 -0
- package/src/hooks/mutations/useServicesMutations.ts +164 -0
- package/src/hooks/queries/index.ts +36 -0
- package/src/hooks/queries/queryKeys.ts +88 -0
- package/src/hooks/queries/useAccountQueries.ts +152 -0
- package/src/hooks/queries/useSecurityQueries.ts +64 -0
- package/src/hooks/queries/useServicesQueries.ts +126 -0
- package/src/hooks/queryClient.ts +112 -0
- package/src/hooks/useAssets.ts +291 -0
- package/src/hooks/useFileDownloadUrl.ts +118 -0
- package/src/hooks/useFileFiltering.ts +115 -0
- package/src/hooks/useFollow.ts +175 -0
- package/src/hooks/useFollow.types.ts +33 -0
- package/src/hooks/useQueryClient.ts +17 -0
- package/src/hooks/useSessionSocket.ts +233 -0
- package/src/hooks/useWebSSO.ts +187 -0
- package/src/index.ts +144 -0
- package/src/stores/accountStore.ts +296 -0
- package/src/stores/assetStore.ts +281 -0
- package/src/stores/authStore.ts +63 -0
- package/src/stores/followStore.ts +181 -0
- package/src/utils/authHelpers.ts +183 -0
- package/src/utils/avatarUtils.ts +103 -0
- package/src/utils/errorHandlers.ts +194 -0
- package/src/utils/sessionHelpers.ts +151 -0
- package/src/utils/storageHelpers.ts +130 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { useAssetStore } from '../stores/assetStore';
|
|
3
|
+
import { OxyServices } from '@oxyhq/core';
|
|
4
|
+
import {
|
|
5
|
+
Asset,
|
|
6
|
+
AssetLinkRequest,
|
|
7
|
+
AssetUnlinkRequest,
|
|
8
|
+
AssetUploadProgress
|
|
9
|
+
} from '@oxyhq/core';
|
|
10
|
+
|
|
11
|
+
// Create a singleton instance for the hook
|
|
12
|
+
let oxyInstance: OxyServices | null = null;
|
|
13
|
+
|
|
14
|
+
export const setOxyAssetInstance = (instance: OxyServices) => {
|
|
15
|
+
oxyInstance = instance;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Hook for managing assets with Zustand store integration
|
|
20
|
+
*/
|
|
21
|
+
export const useAssets = () => {
|
|
22
|
+
const {
|
|
23
|
+
assets,
|
|
24
|
+
uploadProgress,
|
|
25
|
+
loading,
|
|
26
|
+
errors,
|
|
27
|
+
setAsset,
|
|
28
|
+
setAssets,
|
|
29
|
+
removeAsset,
|
|
30
|
+
setUploadProgress,
|
|
31
|
+
removeUploadProgress,
|
|
32
|
+
addLink,
|
|
33
|
+
removeLink,
|
|
34
|
+
setUploading,
|
|
35
|
+
setLinking,
|
|
36
|
+
setDeleting,
|
|
37
|
+
setUploadError,
|
|
38
|
+
setLinkError,
|
|
39
|
+
setDeleteError,
|
|
40
|
+
clearErrors,
|
|
41
|
+
getAssetsByApp,
|
|
42
|
+
getAssetsByEntity,
|
|
43
|
+
getAssetUsageCount,
|
|
44
|
+
isAssetLinked,
|
|
45
|
+
reset
|
|
46
|
+
} = useAssetStore();
|
|
47
|
+
|
|
48
|
+
// Upload asset with progress tracking
|
|
49
|
+
const upload = useCallback(async (
|
|
50
|
+
file: File,
|
|
51
|
+
metadata?: Record<string, any>
|
|
52
|
+
): Promise<Asset | null> => {
|
|
53
|
+
if (!oxyInstance) {
|
|
54
|
+
throw new Error('OxyServices instance not configured. Call setOxyAssetInstance first.');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
clearErrors();
|
|
59
|
+
setUploading(true);
|
|
60
|
+
|
|
61
|
+
// Upload file (progress tracking simplified for now)
|
|
62
|
+
const result = await oxyInstance.assetUpload(file as any, undefined, metadata);
|
|
63
|
+
|
|
64
|
+
// Update progress with final status
|
|
65
|
+
if (result?.file) {
|
|
66
|
+
const fileId = result.file.id;
|
|
67
|
+
setUploadProgress(fileId, {
|
|
68
|
+
fileId,
|
|
69
|
+
uploaded: file.size,
|
|
70
|
+
total: file.size,
|
|
71
|
+
percentage: 100,
|
|
72
|
+
status: 'complete'
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Remove progress after a short delay
|
|
76
|
+
setTimeout(() => {
|
|
77
|
+
removeUploadProgress(fileId);
|
|
78
|
+
}, 2000);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Add asset to store
|
|
82
|
+
if (result.file) {
|
|
83
|
+
setAsset(result.file);
|
|
84
|
+
return result.file;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return null;
|
|
88
|
+
} catch (error: any) {
|
|
89
|
+
setUploadError(error.message || 'Upload failed');
|
|
90
|
+
throw error;
|
|
91
|
+
} finally {
|
|
92
|
+
setUploading(false);
|
|
93
|
+
}
|
|
94
|
+
}, [
|
|
95
|
+
clearErrors,
|
|
96
|
+
setUploading,
|
|
97
|
+
setUploadProgress,
|
|
98
|
+
removeUploadProgress,
|
|
99
|
+
setAsset,
|
|
100
|
+
setUploadError
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
// Link asset to entity
|
|
104
|
+
const link = useCallback(async (
|
|
105
|
+
assetId: string,
|
|
106
|
+
app: string,
|
|
107
|
+
entityType: string,
|
|
108
|
+
entityId: string
|
|
109
|
+
): Promise<void> => {
|
|
110
|
+
if (!oxyInstance) {
|
|
111
|
+
throw new Error('OxyServices instance not configured. Call setOxyAssetInstance first.');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
clearErrors();
|
|
116
|
+
setLinking(true);
|
|
117
|
+
|
|
118
|
+
// Auto-detect visibility for avatars and profile banners
|
|
119
|
+
const visibility = (entityType === 'avatar' || entityType === 'profile-banner')
|
|
120
|
+
? 'public' as const
|
|
121
|
+
: undefined;
|
|
122
|
+
|
|
123
|
+
const result = await oxyInstance.assetLink(assetId, app, entityType, entityId, visibility);
|
|
124
|
+
|
|
125
|
+
if (result.file) {
|
|
126
|
+
setAsset(result.file);
|
|
127
|
+
} else {
|
|
128
|
+
// If API doesn't return full file, update store optimistically
|
|
129
|
+
addLink(assetId, {
|
|
130
|
+
app,
|
|
131
|
+
entityType,
|
|
132
|
+
entityId,
|
|
133
|
+
createdBy: '', // Will be filled by server
|
|
134
|
+
createdAt: new Date().toISOString()
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
} catch (error: any) {
|
|
138
|
+
setLinkError(error.message || 'Link failed');
|
|
139
|
+
throw error;
|
|
140
|
+
} finally {
|
|
141
|
+
setLinking(false);
|
|
142
|
+
}
|
|
143
|
+
}, [clearErrors, setLinking, setAsset, addLink, setLinkError]);
|
|
144
|
+
|
|
145
|
+
// Unlink asset from entity
|
|
146
|
+
const unlink = useCallback(async (
|
|
147
|
+
assetId: string,
|
|
148
|
+
app: string,
|
|
149
|
+
entityType: string,
|
|
150
|
+
entityId: string
|
|
151
|
+
): Promise<void> => {
|
|
152
|
+
if (!oxyInstance) {
|
|
153
|
+
throw new Error('OxyServices instance not configured. Call setOxyAssetInstance first.');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
clearErrors();
|
|
158
|
+
setLinking(true);
|
|
159
|
+
|
|
160
|
+
const result = await oxyInstance.assetUnlink(assetId, app, entityType, entityId);
|
|
161
|
+
|
|
162
|
+
if (result.file) {
|
|
163
|
+
setAsset(result.file);
|
|
164
|
+
} else {
|
|
165
|
+
// Update store optimistically
|
|
166
|
+
removeLink(assetId, app, entityType, entityId);
|
|
167
|
+
}
|
|
168
|
+
} catch (error: any) {
|
|
169
|
+
setLinkError(error.message || 'Unlink failed');
|
|
170
|
+
throw error;
|
|
171
|
+
} finally {
|
|
172
|
+
setLinking(false);
|
|
173
|
+
}
|
|
174
|
+
}, [clearErrors, setLinking, setAsset, removeLink, setLinkError]);
|
|
175
|
+
|
|
176
|
+
// Get asset URL
|
|
177
|
+
const getUrl = useCallback(async (
|
|
178
|
+
assetId: string,
|
|
179
|
+
variant?: string,
|
|
180
|
+
expiresIn?: number
|
|
181
|
+
): Promise<string> => {
|
|
182
|
+
if (!oxyInstance) {
|
|
183
|
+
throw new Error('OxyServices instance not configured. Call setOxyAssetInstance first.');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const result = await oxyInstance.assetGetUrl(assetId, variant, expiresIn);
|
|
188
|
+
return result.url;
|
|
189
|
+
} catch (error: any) {
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
}, []);
|
|
193
|
+
|
|
194
|
+
// Get asset metadata
|
|
195
|
+
const getAsset = useCallback(async (assetId: string): Promise<Asset> => {
|
|
196
|
+
if (!oxyInstance) {
|
|
197
|
+
throw new Error('OxyServices instance not configured. Call setOxyAssetInstance first.');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const result = await oxyInstance.assetGet(assetId);
|
|
202
|
+
if (result.file) {
|
|
203
|
+
setAsset(result.file);
|
|
204
|
+
return result.file;
|
|
205
|
+
}
|
|
206
|
+
throw new Error('Asset not found');
|
|
207
|
+
} catch (error: any) {
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
}, [setAsset]);
|
|
211
|
+
|
|
212
|
+
// Delete asset
|
|
213
|
+
const deleteAsset = useCallback(async (
|
|
214
|
+
assetId: string,
|
|
215
|
+
force: boolean = false
|
|
216
|
+
): Promise<void> => {
|
|
217
|
+
if (!oxyInstance) {
|
|
218
|
+
throw new Error('OxyServices instance not configured. Call setOxyAssetInstance first.');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
clearErrors();
|
|
223
|
+
setDeleting(true);
|
|
224
|
+
|
|
225
|
+
await oxyInstance.assetDelete(assetId, force);
|
|
226
|
+
removeAsset(assetId);
|
|
227
|
+
} catch (error: any) {
|
|
228
|
+
setDeleteError(error.message || 'Delete failed');
|
|
229
|
+
throw error;
|
|
230
|
+
} finally {
|
|
231
|
+
setDeleting(false);
|
|
232
|
+
}
|
|
233
|
+
}, [clearErrors, setDeleting, removeAsset, setDeleteError]);
|
|
234
|
+
|
|
235
|
+
// Restore asset from trash
|
|
236
|
+
const restore = useCallback(async (assetId: string): Promise<void> => {
|
|
237
|
+
if (!oxyInstance) {
|
|
238
|
+
throw new Error('OxyServices instance not configured. Call setOxyAssetInstance first.');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const result = await oxyInstance.assetRestore(assetId);
|
|
243
|
+
if (result.file) {
|
|
244
|
+
setAsset(result.file);
|
|
245
|
+
}
|
|
246
|
+
} catch (error: any) {
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
}, [setAsset]);
|
|
250
|
+
|
|
251
|
+
// Get variants
|
|
252
|
+
const getVariants = useCallback(async (assetId: string) => {
|
|
253
|
+
if (!oxyInstance) {
|
|
254
|
+
throw new Error('OxyServices instance not configured. Call setOxyAssetInstance first.');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
return await oxyInstance.assetGetVariants(assetId);
|
|
259
|
+
} catch (error: any) {
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
}, []);
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
// State
|
|
266
|
+
assets: Object.values(assets),
|
|
267
|
+
uploadProgress,
|
|
268
|
+
loading,
|
|
269
|
+
errors,
|
|
270
|
+
|
|
271
|
+
// Actions
|
|
272
|
+
upload,
|
|
273
|
+
link,
|
|
274
|
+
unlink,
|
|
275
|
+
getUrl,
|
|
276
|
+
getAsset,
|
|
277
|
+
deleteAsset,
|
|
278
|
+
restore,
|
|
279
|
+
getVariants,
|
|
280
|
+
|
|
281
|
+
// Utility methods
|
|
282
|
+
getAssetsByApp,
|
|
283
|
+
getAssetsByEntity,
|
|
284
|
+
getAssetUsageCount,
|
|
285
|
+
isAssetLinked,
|
|
286
|
+
|
|
287
|
+
// Store management
|
|
288
|
+
clearErrors,
|
|
289
|
+
reset
|
|
290
|
+
};
|
|
291
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { OxyServices } from '@oxyhq/core';
|
|
3
|
+
|
|
4
|
+
let oxyInstance: OxyServices | null = null;
|
|
5
|
+
|
|
6
|
+
export const setOxyFileUrlInstance = (instance: OxyServices) => {
|
|
7
|
+
oxyInstance = instance;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export interface UseFileDownloadUrlOptions {
|
|
11
|
+
variant?: string;
|
|
12
|
+
expiresIn?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UseFileDownloadUrlResult {
|
|
16
|
+
url: string | null;
|
|
17
|
+
loading: boolean;
|
|
18
|
+
error: Error | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Hook to resolve a file's download URL asynchronously.
|
|
23
|
+
*
|
|
24
|
+
* Prefers `getFileDownloadUrlAsync` and falls back to the synchronous
|
|
25
|
+
* `getFileDownloadUrl` helper if the async call fails.
|
|
26
|
+
*/
|
|
27
|
+
export const useFileDownloadUrl = (
|
|
28
|
+
fileId?: string | null,
|
|
29
|
+
options?: UseFileDownloadUrlOptions
|
|
30
|
+
): UseFileDownloadUrlResult => {
|
|
31
|
+
const [url, setUrl] = useState<string | null>(null);
|
|
32
|
+
const [loading, setLoading] = useState(false);
|
|
33
|
+
const [error, setError] = useState<Error | null>(null);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!fileId) {
|
|
37
|
+
setUrl(null);
|
|
38
|
+
setLoading(false);
|
|
39
|
+
setError(null);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!oxyInstance) {
|
|
44
|
+
// Fail silently but don't crash the UI – caller can decide what to do with null URL.
|
|
45
|
+
setUrl(null);
|
|
46
|
+
setLoading(false);
|
|
47
|
+
setError(new Error('OxyServices instance not configured for useFileDownloadUrl'));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let cancelled = false;
|
|
52
|
+
|
|
53
|
+
const load = async () => {
|
|
54
|
+
setLoading(true);
|
|
55
|
+
setError(null);
|
|
56
|
+
|
|
57
|
+
// Store instance in local variable for TypeScript null checking
|
|
58
|
+
const instance = oxyInstance;
|
|
59
|
+
if (!instance) {
|
|
60
|
+
setLoading(false);
|
|
61
|
+
setError(new Error('OxyServices instance not configured for useFileDownloadUrl'));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const { variant, expiresIn } = options || {};
|
|
67
|
+
let resolvedUrl: string | null = null;
|
|
68
|
+
|
|
69
|
+
if (typeof instance.getFileDownloadUrlAsync === 'function') {
|
|
70
|
+
resolvedUrl = await instance.getFileDownloadUrlAsync(fileId, variant, expiresIn);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!resolvedUrl && typeof instance.getFileDownloadUrl === 'function') {
|
|
74
|
+
resolvedUrl = instance.getFileDownloadUrl(fileId, variant, expiresIn);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!cancelled) {
|
|
78
|
+
setUrl(resolvedUrl || null);
|
|
79
|
+
}
|
|
80
|
+
} catch (err: any) {
|
|
81
|
+
// Fallback to sync URL on error where possible
|
|
82
|
+
try {
|
|
83
|
+
if (typeof instance.getFileDownloadUrl === 'function') {
|
|
84
|
+
const { variant, expiresIn } = options || {};
|
|
85
|
+
const fallbackUrl = instance.getFileDownloadUrl(fileId, variant, expiresIn);
|
|
86
|
+
if (!cancelled) {
|
|
87
|
+
setUrl(fallbackUrl || null);
|
|
88
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
// ignore secondary failure, we'll surface the original error below
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!cancelled) {
|
|
97
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
98
|
+
}
|
|
99
|
+
} finally {
|
|
100
|
+
if (!cancelled) {
|
|
101
|
+
setLoading(false);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
load();
|
|
107
|
+
|
|
108
|
+
return () => {
|
|
109
|
+
cancelled = true;
|
|
110
|
+
};
|
|
111
|
+
}, [fileId, options?.variant, options?.expiresIn]);
|
|
112
|
+
|
|
113
|
+
return { url, loading, error };
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { useMemo, useState, useCallback } from 'react';
|
|
2
|
+
import type { FileMetadata } from '@oxyhq/core';
|
|
3
|
+
|
|
4
|
+
export type ViewMode = 'all' | 'photos' | 'videos' | 'documents' | 'audio';
|
|
5
|
+
export type SortBy = 'date' | 'size' | 'name' | 'type';
|
|
6
|
+
export type SortOrder = 'asc' | 'desc';
|
|
7
|
+
|
|
8
|
+
interface UseFileFilteringOptions {
|
|
9
|
+
files: FileMetadata[];
|
|
10
|
+
initialViewMode?: ViewMode;
|
|
11
|
+
initialSortBy?: SortBy;
|
|
12
|
+
initialSortOrder?: SortOrder;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface UseFileFilteringReturn {
|
|
16
|
+
filteredFiles: FileMetadata[];
|
|
17
|
+
viewMode: ViewMode;
|
|
18
|
+
setViewMode: (mode: ViewMode) => void;
|
|
19
|
+
searchQuery: string;
|
|
20
|
+
setSearchQuery: (query: string) => void;
|
|
21
|
+
sortBy: SortBy;
|
|
22
|
+
setSortBy: (sort: SortBy) => void;
|
|
23
|
+
sortOrder: SortOrder;
|
|
24
|
+
setSortOrder: (order: SortOrder) => void;
|
|
25
|
+
toggleSortOrder: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Hook for file filtering, sorting, and search functionality
|
|
30
|
+
* Extracts common file management logic for reuse across components
|
|
31
|
+
*/
|
|
32
|
+
export function useFileFiltering({
|
|
33
|
+
files,
|
|
34
|
+
initialViewMode = 'all',
|
|
35
|
+
initialSortBy = 'date',
|
|
36
|
+
initialSortOrder = 'desc',
|
|
37
|
+
}: UseFileFilteringOptions): UseFileFilteringReturn {
|
|
38
|
+
const [viewMode, setViewMode] = useState<ViewMode>(initialViewMode);
|
|
39
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
40
|
+
const [sortBy, setSortBy] = useState<SortBy>(initialSortBy);
|
|
41
|
+
const [sortOrder, setSortOrder] = useState<SortOrder>(initialSortOrder);
|
|
42
|
+
|
|
43
|
+
const toggleSortOrder = useCallback(() => {
|
|
44
|
+
setSortOrder((prev) => (prev === 'asc' ? 'desc' : 'asc'));
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
const filteredFiles = useMemo(() => {
|
|
48
|
+
// Filter by view mode
|
|
49
|
+
let filteredByMode = files;
|
|
50
|
+
if (viewMode === 'photos') {
|
|
51
|
+
filteredByMode = files.filter((file) => file.contentType.startsWith('image/'));
|
|
52
|
+
} else if (viewMode === 'videos') {
|
|
53
|
+
filteredByMode = files.filter((file) => file.contentType.startsWith('video/'));
|
|
54
|
+
} else if (viewMode === 'documents') {
|
|
55
|
+
filteredByMode = files.filter(
|
|
56
|
+
(file) =>
|
|
57
|
+
file.contentType.includes('pdf') ||
|
|
58
|
+
file.contentType.includes('document') ||
|
|
59
|
+
file.contentType.includes('text') ||
|
|
60
|
+
file.contentType.includes('msword') ||
|
|
61
|
+
file.contentType.includes('excel') ||
|
|
62
|
+
file.contentType.includes('spreadsheet') ||
|
|
63
|
+
file.contentType.includes('presentation') ||
|
|
64
|
+
file.contentType.includes('powerpoint')
|
|
65
|
+
);
|
|
66
|
+
} else if (viewMode === 'audio') {
|
|
67
|
+
filteredByMode = files.filter((file) => file.contentType.startsWith('audio/'));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Filter by search query
|
|
71
|
+
let filtered = filteredByMode;
|
|
72
|
+
if (searchQuery.trim()) {
|
|
73
|
+
const query = searchQuery.toLowerCase();
|
|
74
|
+
filtered = filteredByMode.filter(
|
|
75
|
+
(file) =>
|
|
76
|
+
file.filename.toLowerCase().includes(query) ||
|
|
77
|
+
file.contentType.toLowerCase().includes(query) ||
|
|
78
|
+
(file.metadata?.description &&
|
|
79
|
+
file.metadata.description.toLowerCase().includes(query))
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Sort files
|
|
84
|
+
const sorted = [...filtered].sort((a, b) => {
|
|
85
|
+
let comparison = 0;
|
|
86
|
+
if (sortBy === 'date') {
|
|
87
|
+
const dateA = new Date(a.uploadDate || 0).getTime();
|
|
88
|
+
const dateB = new Date(b.uploadDate || 0).getTime();
|
|
89
|
+
comparison = dateA - dateB;
|
|
90
|
+
} else if (sortBy === 'size') {
|
|
91
|
+
comparison = (a.length || 0) - (b.length || 0);
|
|
92
|
+
} else if (sortBy === 'name') {
|
|
93
|
+
comparison = (a.filename || '').localeCompare(b.filename || '');
|
|
94
|
+
} else if (sortBy === 'type') {
|
|
95
|
+
comparison = (a.contentType || '').localeCompare(b.contentType || '');
|
|
96
|
+
}
|
|
97
|
+
return sortOrder === 'asc' ? comparison : -comparison;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return sorted;
|
|
101
|
+
}, [files, searchQuery, viewMode, sortBy, sortOrder]);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
filteredFiles,
|
|
105
|
+
viewMode,
|
|
106
|
+
setViewMode,
|
|
107
|
+
searchQuery,
|
|
108
|
+
setSearchQuery,
|
|
109
|
+
sortBy,
|
|
110
|
+
setSortBy,
|
|
111
|
+
sortOrder,
|
|
112
|
+
setSortOrder,
|
|
113
|
+
toggleSortOrder,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { useCallback, useMemo, useEffect } from 'react';
|
|
2
|
+
import { useFollowStore } from '../stores/followStore';
|
|
3
|
+
import { useWebOxy } from '../WebOxyProvider';
|
|
4
|
+
|
|
5
|
+
export const useFollow = (userId?: string | string[]) => {
|
|
6
|
+
const { oxyServices } = useWebOxy();
|
|
7
|
+
const userIds = useMemo(() => (Array.isArray(userId) ? userId : userId ? [userId] : []), [userId]);
|
|
8
|
+
const isSingleUser = typeof userId === 'string';
|
|
9
|
+
|
|
10
|
+
// Zustand selectors
|
|
11
|
+
const followState = useFollowStore();
|
|
12
|
+
|
|
13
|
+
// Single user helpers
|
|
14
|
+
const isFollowing = isSingleUser && userId ? followState.followingUsers[userId] ?? false : false;
|
|
15
|
+
const isLoading = isSingleUser && userId ? followState.loadingUsers[userId] ?? false : false;
|
|
16
|
+
const error = isSingleUser && userId ? followState.errors[userId] ?? null : null;
|
|
17
|
+
|
|
18
|
+
// Follower count helpers
|
|
19
|
+
const followerCount = isSingleUser && userId ? followState.followerCounts[userId] ?? null : null;
|
|
20
|
+
const followingCount = isSingleUser && userId ? followState.followingCounts[userId] ?? null : null;
|
|
21
|
+
const isLoadingCounts = isSingleUser && userId ? followState.loadingCounts[userId] ?? false : false;
|
|
22
|
+
|
|
23
|
+
const toggleFollow = useCallback(async () => {
|
|
24
|
+
if (!isSingleUser || !userId) throw new Error('toggleFollow is only available for single user mode');
|
|
25
|
+
await followState.toggleFollowUser(userId, oxyServices, isFollowing);
|
|
26
|
+
}, [isSingleUser, userId, followState, oxyServices, isFollowing]);
|
|
27
|
+
|
|
28
|
+
const setFollowStatus = useCallback((following: boolean) => {
|
|
29
|
+
if (!isSingleUser || !userId) throw new Error('setFollowStatus is only available for single user mode');
|
|
30
|
+
followState.setFollowingStatus(userId, following);
|
|
31
|
+
}, [isSingleUser, userId, followState]);
|
|
32
|
+
|
|
33
|
+
const fetchStatus = useCallback(async () => {
|
|
34
|
+
if (!isSingleUser || !userId) throw new Error('fetchStatus is only available for single user mode');
|
|
35
|
+
await followState.fetchFollowStatus(userId, oxyServices);
|
|
36
|
+
}, [isSingleUser, userId, followState, oxyServices]);
|
|
37
|
+
|
|
38
|
+
const clearError = useCallback(() => {
|
|
39
|
+
if (!isSingleUser || !userId) throw new Error('clearError is only available for single user mode');
|
|
40
|
+
followState.clearFollowError(userId);
|
|
41
|
+
}, [isSingleUser, userId, followState]);
|
|
42
|
+
|
|
43
|
+
const fetchUserCounts = useCallback(async () => {
|
|
44
|
+
if (!isSingleUser || !userId) throw new Error('fetchUserCounts is only available for single user mode');
|
|
45
|
+
await followState.fetchUserCounts(userId, oxyServices);
|
|
46
|
+
}, [isSingleUser, userId, followState, oxyServices]);
|
|
47
|
+
|
|
48
|
+
const setFollowerCount = useCallback((count: number) => {
|
|
49
|
+
if (!isSingleUser || !userId) throw new Error('setFollowerCount is only available for single user mode');
|
|
50
|
+
followState.setFollowerCount(userId, count);
|
|
51
|
+
}, [isSingleUser, userId, followState]);
|
|
52
|
+
|
|
53
|
+
const setFollowingCount = useCallback((count: number) => {
|
|
54
|
+
if (!isSingleUser || !userId) throw new Error('setFollowingCount is only available for single user mode');
|
|
55
|
+
followState.setFollowingCount(userId, count);
|
|
56
|
+
}, [isSingleUser, userId, followState]);
|
|
57
|
+
|
|
58
|
+
// Auto-fetch counts when hook is used for a single user and counts are missing.
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!isSingleUser || !userId) return;
|
|
61
|
+
|
|
62
|
+
// If either count is not set and we're not already loading counts, trigger a fetch.
|
|
63
|
+
if ((followerCount === null || followingCount === null) && !isLoadingCounts) {
|
|
64
|
+
fetchUserCounts().catch((err: any) => console.warn('useFollow: fetchUserCounts failed', err));
|
|
65
|
+
}
|
|
66
|
+
}, [isSingleUser, userId, followerCount, followingCount, isLoadingCounts, fetchUserCounts]);
|
|
67
|
+
|
|
68
|
+
// Multiple user helpers
|
|
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
|
+
|
|
81
|
+
const toggleFollowForUser = useCallback(async (targetUserId: string) => {
|
|
82
|
+
const currentState = followState.followingUsers[targetUserId] ?? false;
|
|
83
|
+
await followState.toggleFollowUser(targetUserId, oxyServices, currentState);
|
|
84
|
+
}, [followState, oxyServices]);
|
|
85
|
+
|
|
86
|
+
const setFollowStatusForUser = useCallback((targetUserId: string, following: boolean) => {
|
|
87
|
+
followState.setFollowingStatus(targetUserId, following);
|
|
88
|
+
}, [followState]);
|
|
89
|
+
|
|
90
|
+
const fetchStatusForUser = useCallback(async (targetUserId: string) => {
|
|
91
|
+
await followState.fetchFollowStatus(targetUserId, oxyServices);
|
|
92
|
+
}, [followState, oxyServices]);
|
|
93
|
+
|
|
94
|
+
const fetchAllStatuses = useCallback(async () => {
|
|
95
|
+
await Promise.all(userIds.map(uid => followState.fetchFollowStatus(uid, oxyServices)));
|
|
96
|
+
}, [userIds, followState, oxyServices]);
|
|
97
|
+
|
|
98
|
+
const clearErrorForUser = useCallback((targetUserId: string) => {
|
|
99
|
+
followState.clearFollowError(targetUserId);
|
|
100
|
+
}, [followState]);
|
|
101
|
+
|
|
102
|
+
const updateCountsFromFollowAction = useCallback((targetUserId: string, action: 'follow' | 'unfollow', counts: { followers: number; following: number }) => {
|
|
103
|
+
const currentUserId = oxyServices.getCurrentUserId() || undefined;
|
|
104
|
+
followState.updateCountsFromFollowAction(targetUserId, action, counts, currentUserId);
|
|
105
|
+
}, [followState, oxyServices]);
|
|
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]);
|
|
112
|
+
|
|
113
|
+
if (isSingleUser && userId) {
|
|
114
|
+
return {
|
|
115
|
+
isFollowing,
|
|
116
|
+
isLoading,
|
|
117
|
+
error,
|
|
118
|
+
toggleFollow,
|
|
119
|
+
setFollowStatus,
|
|
120
|
+
fetchStatus,
|
|
121
|
+
clearError,
|
|
122
|
+
// Follower count methods
|
|
123
|
+
followerCount,
|
|
124
|
+
followingCount,
|
|
125
|
+
isLoadingCounts,
|
|
126
|
+
fetchUserCounts,
|
|
127
|
+
setFollowerCount,
|
|
128
|
+
setFollowingCount,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
followData,
|
|
134
|
+
toggleFollowForUser,
|
|
135
|
+
setFollowStatusForUser,
|
|
136
|
+
fetchStatusForUser,
|
|
137
|
+
fetchAllStatuses,
|
|
138
|
+
clearErrorForUser,
|
|
139
|
+
isAnyLoading,
|
|
140
|
+
hasAnyError,
|
|
141
|
+
allFollowing,
|
|
142
|
+
allNotFollowing,
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Convenience hook for just follower counts
|
|
147
|
+
export const useFollowerCounts = (userId: string) => {
|
|
148
|
+
const { oxyServices } = useWebOxy();
|
|
149
|
+
const followState = useFollowStore();
|
|
150
|
+
|
|
151
|
+
const followerCount = followState.followerCounts[userId] ?? null;
|
|
152
|
+
const followingCount = followState.followingCounts[userId] ?? null;
|
|
153
|
+
const isLoadingCounts = followState.loadingCounts[userId] ?? false;
|
|
154
|
+
|
|
155
|
+
const fetchUserCounts = useCallback(async () => {
|
|
156
|
+
await followState.fetchUserCounts(userId, oxyServices);
|
|
157
|
+
}, [userId, followState, oxyServices]);
|
|
158
|
+
|
|
159
|
+
const setFollowerCount = useCallback((count: number) => {
|
|
160
|
+
followState.setFollowerCount(userId, count);
|
|
161
|
+
}, [userId, followState]);
|
|
162
|
+
|
|
163
|
+
const setFollowingCount = useCallback((count: number) => {
|
|
164
|
+
followState.setFollowingCount(userId, count);
|
|
165
|
+
}, [userId, followState]);
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
followerCount,
|
|
169
|
+
followingCount,
|
|
170
|
+
isLoadingCounts,
|
|
171
|
+
fetchUserCounts,
|
|
172
|
+
setFollowerCount,
|
|
173
|
+
setFollowingCount,
|
|
174
|
+
};
|
|
175
|
+
};
|