@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,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
+ };