@oxyhq/services 5.16.4 → 5.16.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/core/OxyServices.base.js +3 -1
- package/lib/commonjs/core/OxyServices.base.js.map +1 -1
- package/lib/commonjs/core/mixins/OxyServices.assets.js +20 -330
- package/lib/commonjs/core/mixins/OxyServices.assets.js.map +1 -1
- package/lib/commonjs/index.js +156 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +95 -9
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/hooks/mutations/index.js +60 -20
- package/lib/commonjs/ui/hooks/mutations/index.js.map +1 -1
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +230 -1
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/index.js +96 -30
- package/lib/commonjs/ui/hooks/queries/index.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/queryKeys.js +5 -0
- package/lib/commonjs/ui/hooks/queries/queryKeys.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/useAccountQueries.js +75 -1
- package/lib/commonjs/ui/hooks/queries/useAccountQueries.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/useServicesQueries.js +50 -2
- package/lib/commonjs/ui/hooks/queries/useServicesQueries.js.map +1 -1
- package/lib/commonjs/ui/hooks/useAssets.js +8 -29
- package/lib/commonjs/ui/hooks/useAssets.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountOverviewScreen.js +6 -6
- package/lib/commonjs/ui/screens/AccountOverviewScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js +3 -3
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/FileManagementScreen.js +14 -10
- package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/commonjs/ui/utils/fileManagement.js +88 -0
- package/lib/commonjs/ui/utils/fileManagement.js.map +1 -1
- package/lib/module/core/OxyServices.base.js +3 -1
- package/lib/module/core/OxyServices.base.js.map +1 -1
- package/lib/module/core/mixins/OxyServices.assets.js +20 -331
- package/lib/module/core/mixins/OxyServices.assets.js.map +1 -1
- package/lib/module/index.js +17 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +95 -9
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/hooks/mutations/index.js +12 -3
- package/lib/module/ui/hooks/mutations/index.js.map +1 -1
- package/lib/module/ui/hooks/mutations/useAccountMutations.js +227 -0
- package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/module/ui/hooks/queries/index.js +15 -4
- package/lib/module/ui/hooks/queries/index.js.map +1 -1
- package/lib/module/ui/hooks/queries/queryKeys.js +5 -0
- package/lib/module/ui/hooks/queries/queryKeys.js.map +1 -1
- package/lib/module/ui/hooks/queries/useAccountQueries.js +73 -0
- package/lib/module/ui/hooks/queries/useAccountQueries.js.map +1 -1
- package/lib/module/ui/hooks/queries/useServicesQueries.js +50 -2
- package/lib/module/ui/hooks/queries/useServicesQueries.js.map +1 -1
- package/lib/module/ui/hooks/useAssets.js +8 -29
- package/lib/module/ui/hooks/useAssets.js.map +1 -1
- package/lib/module/ui/screens/AccountOverviewScreen.js +6 -6
- package/lib/module/ui/screens/AccountOverviewScreen.js.map +1 -1
- package/lib/module/ui/screens/AccountSettingsScreen.js +3 -3
- package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/module/ui/screens/FileManagementScreen.js +12 -10
- package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/module/ui/utils/fileManagement.js +87 -0
- package/lib/module/ui/utils/fileManagement.js.map +1 -1
- package/lib/typescript/core/OxyServices.base.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.assets.d.ts +1 -70
- package/lib/typescript/core/mixins/OxyServices.assets.d.ts.map +1 -1
- package/lib/typescript/core/mixins/index.d.ts +4 -14
- package/lib/typescript/core/mixins/index.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/ui/context/OxyContext.d.ts +1 -0
- package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/mutations/index.d.ts +8 -2
- package/lib/typescript/ui/hooks/mutations/index.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts +19 -0
- package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/queries/index.d.ts +9 -3
- package/lib/typescript/ui/hooks/queries/index.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/queries/queryKeys.d.ts +4 -0
- package/lib/typescript/ui/hooks/queries/queryKeys.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts +6 -0
- package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/queries/useServicesQueries.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/useAssets.d.ts.map +1 -1
- package/lib/typescript/ui/screens/AccountOverviewScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/FileManagementScreen.d.ts.map +1 -1
- package/lib/typescript/ui/utils/fileManagement.d.ts +48 -0
- package/lib/typescript/ui/utils/fileManagement.d.ts.map +1 -1
- package/package.json +6 -2
- package/src/core/OxyServices.base.ts +5 -1
- package/src/core/mixins/OxyServices.assets.ts +21 -338
- package/src/index.ts +49 -2
- package/src/ui/context/OxyContext.tsx +98 -7
- package/src/ui/hooks/mutations/index.ts +24 -3
- package/src/ui/hooks/mutations/useAccountMutations.ts +205 -0
- package/src/ui/hooks/queries/index.ts +29 -4
- package/src/ui/hooks/queries/queryKeys.ts +6 -0
- package/src/ui/hooks/queries/useAccountQueries.ts +69 -0
- package/src/ui/hooks/queries/useServicesQueries.ts +49 -2
- package/src/ui/hooks/useAssets.ts +8 -28
- package/src/ui/screens/AccountOverviewScreen.tsx +4 -3
- package/src/ui/screens/AccountSettingsScreen.tsx +3 -5
- package/src/ui/screens/FileManagementScreen.tsx +10 -11
- package/src/ui/utils/fileManagement.ts +105 -0
|
@@ -30,6 +30,9 @@ import { useQueryClient } from '@tanstack/react-query';
|
|
|
30
30
|
import { clearQueryCache } from '../hooks/queryClient';
|
|
31
31
|
import { useAccountStore } from '../stores/accountStore';
|
|
32
32
|
import { KeyManager } from '../../crypto/keyManager';
|
|
33
|
+
import { translate } from '../../i18n';
|
|
34
|
+
import { queryKeys } from '../hooks/queries/queryKeys';
|
|
35
|
+
import { useUpdateProfile } from '../hooks/mutations/useAccountMutations';
|
|
33
36
|
|
|
34
37
|
export interface OxyContextState {
|
|
35
38
|
user: User | null;
|
|
@@ -84,6 +87,7 @@ export interface OxyContextState {
|
|
|
84
87
|
oxyServices: OxyServices;
|
|
85
88
|
useFollow?: UseFollowHook;
|
|
86
89
|
showBottomSheet?: (screenOrConfig: RouteName | { screen: RouteName; props?: Record<string, unknown> }) => void;
|
|
90
|
+
openAvatarPicker: () => void;
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
const OxyContext = createContext<OxyContextState | null>(null);
|
|
@@ -380,16 +384,40 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
380
384
|
if (!storage) return;
|
|
381
385
|
|
|
382
386
|
let wasOffline = false;
|
|
383
|
-
let
|
|
387
|
+
let checkTimeout: NodeJS.Timeout | null = null;
|
|
388
|
+
|
|
389
|
+
// Circuit breaker and exponential backoff state
|
|
390
|
+
const stateRef = {
|
|
391
|
+
consecutiveFailures: 0,
|
|
392
|
+
currentInterval: 10000, // Start with 10 seconds
|
|
393
|
+
baseInterval: 10000, // Base interval in milliseconds
|
|
394
|
+
maxInterval: 60000, // Maximum interval (60 seconds)
|
|
395
|
+
maxFailures: 5, // Circuit breaker threshold
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const scheduleNextCheck = () => {
|
|
399
|
+
if (checkTimeout) {
|
|
400
|
+
clearTimeout(checkTimeout);
|
|
401
|
+
}
|
|
402
|
+
checkTimeout = setTimeout(() => {
|
|
403
|
+
checkNetworkAndSync();
|
|
404
|
+
}, stateRef.currentInterval);
|
|
405
|
+
};
|
|
384
406
|
|
|
385
407
|
const checkNetworkAndSync = async () => {
|
|
386
408
|
try {
|
|
387
409
|
// Try a lightweight health check to see if we're online
|
|
388
410
|
await oxyServices.healthCheck().catch(() => {
|
|
389
411
|
wasOffline = true;
|
|
390
|
-
|
|
412
|
+
throw new Error('Health check failed');
|
|
391
413
|
});
|
|
392
414
|
|
|
415
|
+
// Health check succeeded - reset circuit breaker and backoff
|
|
416
|
+
if (stateRef.consecutiveFailures > 0) {
|
|
417
|
+
stateRef.consecutiveFailures = 0;
|
|
418
|
+
stateRef.currentInterval = stateRef.baseInterval;
|
|
419
|
+
}
|
|
420
|
+
|
|
393
421
|
// If we were offline and now we're online, sync identity if needed
|
|
394
422
|
if (wasOffline) {
|
|
395
423
|
if (__DEV__ && logger) {
|
|
@@ -414,18 +442,36 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
414
442
|
} catch (error) {
|
|
415
443
|
// Network check failed - we're offline
|
|
416
444
|
wasOffline = true;
|
|
445
|
+
|
|
446
|
+
// Increment failure count and apply exponential backoff
|
|
447
|
+
stateRef.consecutiveFailures++;
|
|
448
|
+
|
|
449
|
+
// Calculate new interval with exponential backoff, capped at maxInterval
|
|
450
|
+
const backoffMultiplier = Math.min(
|
|
451
|
+
Math.pow(2, stateRef.consecutiveFailures - 1),
|
|
452
|
+
stateRef.maxInterval / stateRef.baseInterval
|
|
453
|
+
);
|
|
454
|
+
stateRef.currentInterval = Math.min(
|
|
455
|
+
stateRef.baseInterval * backoffMultiplier,
|
|
456
|
+
stateRef.maxInterval
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
// If we hit the circuit breaker threshold, use max interval
|
|
460
|
+
if (stateRef.consecutiveFailures >= stateRef.maxFailures) {
|
|
461
|
+
stateRef.currentInterval = stateRef.maxInterval;
|
|
462
|
+
}
|
|
463
|
+
} finally {
|
|
464
|
+
// Always schedule next check (will use updated interval)
|
|
465
|
+
scheduleNextCheck();
|
|
417
466
|
}
|
|
418
467
|
};
|
|
419
468
|
|
|
420
469
|
// Check immediately
|
|
421
470
|
checkNetworkAndSync();
|
|
422
471
|
|
|
423
|
-
// Check periodically (every 10 seconds when app is active)
|
|
424
|
-
checkInterval = setInterval(checkNetworkAndSync, 10000);
|
|
425
|
-
|
|
426
472
|
return () => {
|
|
427
|
-
if (
|
|
428
|
-
|
|
473
|
+
if (checkTimeout) {
|
|
474
|
+
clearTimeout(checkTimeout);
|
|
429
475
|
}
|
|
430
476
|
};
|
|
431
477
|
}, [oxyServices, storage, syncIdentity, logger]);
|
|
@@ -440,6 +486,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
440
486
|
|
|
441
487
|
const useFollowHook = loadUseFollowHook();
|
|
442
488
|
|
|
489
|
+
// Create update profile mutation for avatar picker
|
|
490
|
+
const updateProfileMutation = useUpdateProfile();
|
|
491
|
+
|
|
443
492
|
const restoreSessionsFromStorage = useCallback(async (): Promise<void> => {
|
|
444
493
|
if (!storage) {
|
|
445
494
|
return;
|
|
@@ -573,6 +622,46 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
573
622
|
[],
|
|
574
623
|
);
|
|
575
624
|
|
|
625
|
+
// Create openAvatarPicker function
|
|
626
|
+
const openAvatarPicker = useCallback(() => {
|
|
627
|
+
showBottomSheetForContext({
|
|
628
|
+
screen: 'FileManagement' as RouteName,
|
|
629
|
+
props: {
|
|
630
|
+
selectMode: true,
|
|
631
|
+
multiSelect: false,
|
|
632
|
+
disabledMimeTypes: ['video/', 'audio/', 'application/pdf'],
|
|
633
|
+
afterSelect: 'none', // Don't navigate away - stay on current screen
|
|
634
|
+
onSelect: async (file: any) => {
|
|
635
|
+
if (!file.contentType.startsWith('image/')) {
|
|
636
|
+
toast.error(translate(currentLanguage, 'editProfile.toasts.selectImage') || 'Please select an image file');
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
try {
|
|
640
|
+
// Update file visibility to public for avatar (skip if temporary asset ID)
|
|
641
|
+
if (file.id && !file.id.startsWith('temp-')) {
|
|
642
|
+
try {
|
|
643
|
+
await oxyServices.assetUpdateVisibility(file.id, 'public');
|
|
644
|
+
console.log('[OxyContext] Avatar visibility updated to public');
|
|
645
|
+
} catch (visError: any) {
|
|
646
|
+
// Only log non-404 errors (404 means asset doesn't exist yet, which is OK)
|
|
647
|
+
if (visError?.response?.status !== 404) {
|
|
648
|
+
console.warn('[OxyContext] Failed to update avatar visibility, continuing anyway:', visError);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Update user profile using mutation hook (provides optimistic updates, error handling, retry)
|
|
654
|
+
await updateProfileMutation.mutateAsync({ avatar: file.id });
|
|
655
|
+
|
|
656
|
+
toast.success(translate(currentLanguage, 'editProfile.toasts.avatarUpdated') || 'Avatar updated');
|
|
657
|
+
} catch (e: any) {
|
|
658
|
+
toast.error(e.message || translate(currentLanguage, 'editProfile.toasts.updateAvatarFailed') || 'Failed to update avatar');
|
|
659
|
+
}
|
|
660
|
+
},
|
|
661
|
+
},
|
|
662
|
+
});
|
|
663
|
+
}, [oxyServices, currentLanguage, showBottomSheetForContext, updateProfileMutation]);
|
|
664
|
+
|
|
576
665
|
const contextValue: OxyContextState = useMemo(() => ({
|
|
577
666
|
user,
|
|
578
667
|
sessions,
|
|
@@ -612,6 +701,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
612
701
|
oxyServices,
|
|
613
702
|
useFollow: useFollowHook,
|
|
614
703
|
showBottomSheet: showBottomSheetForContext,
|
|
704
|
+
openAvatarPicker,
|
|
615
705
|
}), [
|
|
616
706
|
activeSessionId,
|
|
617
707
|
createIdentity,
|
|
@@ -647,6 +737,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
647
737
|
useFollowHook,
|
|
648
738
|
user,
|
|
649
739
|
showBottomSheetForContext,
|
|
740
|
+
openAvatarPicker,
|
|
650
741
|
]);
|
|
651
742
|
|
|
652
743
|
return (
|
|
@@ -1,4 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Mutation Hooks
|
|
3
|
+
*
|
|
4
|
+
* TanStack Query mutation hooks for updating Oxy services data.
|
|
5
|
+
* All mutations handle authentication, error handling, and query invalidation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Account mutation hooks
|
|
9
|
+
export {
|
|
10
|
+
useUpdateProfile,
|
|
11
|
+
useUploadAvatar,
|
|
12
|
+
useUpdateAccountSettings,
|
|
13
|
+
useUpdatePrivacySettings,
|
|
14
|
+
useUploadFile,
|
|
15
|
+
} from './useAccountMutations';
|
|
16
|
+
|
|
17
|
+
// Service mutation hooks (sessions, devices)
|
|
18
|
+
export {
|
|
19
|
+
useSwitchSession,
|
|
20
|
+
useLogoutSession,
|
|
21
|
+
useLogoutAll,
|
|
22
|
+
useUpdateDeviceName,
|
|
23
|
+
useRemoveDevice,
|
|
24
|
+
} from './useServicesMutations';
|
|
4
25
|
|
|
@@ -275,3 +275,208 @@ export const useUpdateAccountSettings = () => {
|
|
|
275
275
|
});
|
|
276
276
|
};
|
|
277
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Update privacy settings with optimistic updates and authentication handling
|
|
280
|
+
*/
|
|
281
|
+
export const useUpdatePrivacySettings = () => {
|
|
282
|
+
const { oxyServices, activeSessionId, user, syncIdentity } = useOxy();
|
|
283
|
+
const queryClient = useQueryClient();
|
|
284
|
+
|
|
285
|
+
return useMutation({
|
|
286
|
+
mutationFn: async ({ settings, userId }: { settings: Record<string, any>; userId?: string }) => {
|
|
287
|
+
const targetUserId = userId || user?.id;
|
|
288
|
+
if (!targetUserId) {
|
|
289
|
+
throw new Error('User ID is required');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Ensure we have a valid token before making the request
|
|
293
|
+
if (!oxyServices.hasValidToken() && activeSessionId) {
|
|
294
|
+
try {
|
|
295
|
+
// Try to get token for the session
|
|
296
|
+
await oxyServices.getTokenBySession(activeSessionId);
|
|
297
|
+
} catch (tokenError) {
|
|
298
|
+
// If getting token fails, might be an offline session - try syncing
|
|
299
|
+
const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError);
|
|
300
|
+
if (errorMessage.includes('AUTH_REQUIRED_OFFLINE_SESSION') || errorMessage.includes('offline')) {
|
|
301
|
+
try {
|
|
302
|
+
await syncIdentity();
|
|
303
|
+
// Retry getting token after sync
|
|
304
|
+
await oxyServices.getTokenBySession(activeSessionId);
|
|
305
|
+
} catch (syncError) {
|
|
306
|
+
throw new Error('Session needs to be synced. Please try again.');
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
throw tokenError;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
return await oxyServices.updatePrivacySettings(settings, targetUserId);
|
|
316
|
+
} catch (error: any) {
|
|
317
|
+
const errorMessage = error?.message || '';
|
|
318
|
+
const status = error?.status || error?.response?.status;
|
|
319
|
+
|
|
320
|
+
// Handle authentication errors
|
|
321
|
+
if (status === 401 || errorMessage.includes('Authentication required') || errorMessage.includes('Invalid or missing authorization header')) {
|
|
322
|
+
// Try to sync session and get token
|
|
323
|
+
if (activeSessionId) {
|
|
324
|
+
try {
|
|
325
|
+
await syncIdentity();
|
|
326
|
+
await oxyServices.getTokenBySession(activeSessionId);
|
|
327
|
+
// Retry the update after getting token
|
|
328
|
+
return await oxyServices.updatePrivacySettings(settings, targetUserId);
|
|
329
|
+
} catch (retryError) {
|
|
330
|
+
throw new Error('Authentication failed. Please sign in again.');
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
throw new Error('No active session. Please sign in.');
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// TanStack Query will automatically retry on network errors
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
// Optimistic update
|
|
342
|
+
onMutate: async ({ settings, userId }) => {
|
|
343
|
+
const targetUserId = userId || user?.id;
|
|
344
|
+
if (!targetUserId) return;
|
|
345
|
+
|
|
346
|
+
// Cancel outgoing refetches
|
|
347
|
+
await queryClient.cancelQueries({ queryKey: queryKeys.privacy.settings(targetUserId) });
|
|
348
|
+
await queryClient.cancelQueries({ queryKey: queryKeys.accounts.current() });
|
|
349
|
+
|
|
350
|
+
// Snapshot previous values
|
|
351
|
+
const previousPrivacySettings = queryClient.getQueryData(queryKeys.privacy.settings(targetUserId));
|
|
352
|
+
const previousUser = queryClient.getQueryData<User>(queryKeys.accounts.current());
|
|
353
|
+
|
|
354
|
+
// Optimistically update privacy settings
|
|
355
|
+
if (previousPrivacySettings) {
|
|
356
|
+
queryClient.setQueryData(queryKeys.privacy.settings(targetUserId), {
|
|
357
|
+
...previousPrivacySettings,
|
|
358
|
+
...settings,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Also update user query if available
|
|
363
|
+
if (previousUser) {
|
|
364
|
+
queryClient.setQueryData<User>(queryKeys.accounts.current(), {
|
|
365
|
+
...previousUser,
|
|
366
|
+
privacySettings: {
|
|
367
|
+
...previousUser.privacySettings,
|
|
368
|
+
...settings,
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return { previousPrivacySettings, previousUser };
|
|
374
|
+
},
|
|
375
|
+
// On error, rollback
|
|
376
|
+
onError: (error, { userId }, context) => {
|
|
377
|
+
const targetUserId = userId || user?.id;
|
|
378
|
+
if (context?.previousPrivacySettings && targetUserId) {
|
|
379
|
+
queryClient.setQueryData(queryKeys.privacy.settings(targetUserId), context.previousPrivacySettings);
|
|
380
|
+
}
|
|
381
|
+
if (context?.previousUser) {
|
|
382
|
+
queryClient.setQueryData(queryKeys.accounts.current(), context.previousUser);
|
|
383
|
+
}
|
|
384
|
+
toast.error(error instanceof Error ? error.message : 'Failed to update privacy settings');
|
|
385
|
+
},
|
|
386
|
+
// On success, invalidate and refetch
|
|
387
|
+
onSuccess: (data, { userId }) => {
|
|
388
|
+
const targetUserId = userId || user?.id;
|
|
389
|
+
if (targetUserId) {
|
|
390
|
+
queryClient.setQueryData(queryKeys.privacy.settings(targetUserId), data);
|
|
391
|
+
}
|
|
392
|
+
// Also update account query if it contains privacy settings
|
|
393
|
+
const currentUser = queryClient.getQueryData<User>(queryKeys.accounts.current());
|
|
394
|
+
if (currentUser) {
|
|
395
|
+
queryClient.setQueryData<User>(queryKeys.accounts.current(), {
|
|
396
|
+
...currentUser,
|
|
397
|
+
privacySettings: data,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
invalidateAccountQueries(queryClient);
|
|
401
|
+
},
|
|
402
|
+
// Always refetch after error or success
|
|
403
|
+
onSettled: (data, error, { userId }) => {
|
|
404
|
+
const targetUserId = userId || user?.id;
|
|
405
|
+
if (targetUserId) {
|
|
406
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.privacy.settings(targetUserId) });
|
|
407
|
+
}
|
|
408
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.accounts.current() });
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Upload file with authentication handling and progress tracking
|
|
415
|
+
*/
|
|
416
|
+
export const useUploadFile = () => {
|
|
417
|
+
const { oxyServices, activeSessionId, syncIdentity } = useOxy();
|
|
418
|
+
|
|
419
|
+
return useMutation({
|
|
420
|
+
mutationFn: async ({
|
|
421
|
+
file,
|
|
422
|
+
visibility,
|
|
423
|
+
metadata,
|
|
424
|
+
onProgress
|
|
425
|
+
}: {
|
|
426
|
+
file: File;
|
|
427
|
+
visibility?: 'private' | 'public' | 'unlisted';
|
|
428
|
+
metadata?: Record<string, any>;
|
|
429
|
+
onProgress?: (progress: number) => void;
|
|
430
|
+
}) => {
|
|
431
|
+
// Ensure we have a valid token before making the request
|
|
432
|
+
if (!oxyServices.hasValidToken() && activeSessionId) {
|
|
433
|
+
try {
|
|
434
|
+
// Try to get token for the session
|
|
435
|
+
await oxyServices.getTokenBySession(activeSessionId);
|
|
436
|
+
} catch (tokenError) {
|
|
437
|
+
// If getting token fails, might be an offline session - try syncing
|
|
438
|
+
const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError);
|
|
439
|
+
if (errorMessage.includes('AUTH_REQUIRED_OFFLINE_SESSION') || errorMessage.includes('offline')) {
|
|
440
|
+
try {
|
|
441
|
+
await syncIdentity();
|
|
442
|
+
// Retry getting token after sync
|
|
443
|
+
await oxyServices.getTokenBySession(activeSessionId);
|
|
444
|
+
} catch (syncError) {
|
|
445
|
+
throw new Error('Session needs to be synced. Please try again.');
|
|
446
|
+
}
|
|
447
|
+
} else {
|
|
448
|
+
throw tokenError;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
try {
|
|
454
|
+
return await oxyServices.assetUpload(file as any, visibility, metadata, onProgress);
|
|
455
|
+
} catch (error: any) {
|
|
456
|
+
const errorMessage = error?.message || '';
|
|
457
|
+
const status = error?.status || error?.response?.status;
|
|
458
|
+
|
|
459
|
+
// Handle authentication errors
|
|
460
|
+
if (status === 401 || errorMessage.includes('Authentication required') || errorMessage.includes('Invalid or missing authorization header')) {
|
|
461
|
+
// Try to sync session and get token
|
|
462
|
+
if (activeSessionId) {
|
|
463
|
+
try {
|
|
464
|
+
await syncIdentity();
|
|
465
|
+
await oxyServices.getTokenBySession(activeSessionId);
|
|
466
|
+
// Retry the upload after getting token
|
|
467
|
+
return await oxyServices.assetUpload(file as any, visibility, metadata, onProgress);
|
|
468
|
+
} catch (retryError) {
|
|
469
|
+
throw new Error('Authentication failed. Please sign in again.');
|
|
470
|
+
}
|
|
471
|
+
} else {
|
|
472
|
+
throw new Error('No active session. Please sign in.');
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// TanStack Query will automatically retry on network errors
|
|
477
|
+
throw error;
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
};
|
|
482
|
+
|
|
@@ -1,5 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Query Hooks
|
|
3
|
+
*
|
|
4
|
+
* TanStack Query hooks for fetching Oxy services data.
|
|
5
|
+
* All hooks follow the same pattern with optional `enabled` parameter.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Account and user query hooks
|
|
9
|
+
export {
|
|
10
|
+
useUserProfile,
|
|
11
|
+
useUserProfiles,
|
|
12
|
+
useCurrentUser,
|
|
13
|
+
useUserById,
|
|
14
|
+
useUserByUsername,
|
|
15
|
+
useUsersBySessions,
|
|
16
|
+
usePrivacySettings,
|
|
17
|
+
} from './useAccountQueries';
|
|
18
|
+
|
|
19
|
+
// Service query hooks (sessions, devices, security)
|
|
20
|
+
export {
|
|
21
|
+
useSessions,
|
|
22
|
+
useSession,
|
|
23
|
+
useDeviceSessions,
|
|
24
|
+
useUserDevices,
|
|
25
|
+
useSecurityInfo,
|
|
26
|
+
} from './useServicesQueries';
|
|
27
|
+
|
|
28
|
+
// Query keys and invalidation helpers (for advanced usage)
|
|
29
|
+
export { queryKeys, invalidateAccountQueries, invalidateUserQueries, invalidateSessionQueries } from './queryKeys';
|
|
5
30
|
|
|
@@ -48,6 +48,12 @@ export const queryKeys = {
|
|
|
48
48
|
details: () => [...queryKeys.devices.all, 'detail'] as const,
|
|
49
49
|
detail: (deviceId: string) => [...queryKeys.devices.details(), deviceId] as const,
|
|
50
50
|
},
|
|
51
|
+
|
|
52
|
+
// Privacy settings queries
|
|
53
|
+
privacy: {
|
|
54
|
+
all: ['privacy'] as const,
|
|
55
|
+
settings: (userId?: string) => [...queryKeys.privacy.all, 'settings', userId || 'current'] as const,
|
|
56
|
+
},
|
|
51
57
|
} as const;
|
|
52
58
|
|
|
53
59
|
/**
|
|
@@ -124,3 +124,72 @@ export const useUsersBySessions = (sessionIds: string[], options?: { enabled?: b
|
|
|
124
124
|
});
|
|
125
125
|
};
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Get privacy settings for a user
|
|
129
|
+
*/
|
|
130
|
+
export const usePrivacySettings = (userId?: string, options?: { enabled?: boolean }) => {
|
|
131
|
+
const { oxyServices, activeSessionId, syncIdentity, user } = useOxy();
|
|
132
|
+
const targetUserId = userId || user?.id;
|
|
133
|
+
|
|
134
|
+
return useQuery({
|
|
135
|
+
queryKey: queryKeys.privacy.settings(userId),
|
|
136
|
+
queryFn: async () => {
|
|
137
|
+
if (!targetUserId) {
|
|
138
|
+
throw new Error('User ID is required');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Ensure we have a valid token before making the request
|
|
142
|
+
if (!oxyServices.hasValidToken() && activeSessionId) {
|
|
143
|
+
try {
|
|
144
|
+
// Try to get token for the session
|
|
145
|
+
await oxyServices.getTokenBySession(activeSessionId);
|
|
146
|
+
} catch (tokenError) {
|
|
147
|
+
// If getting token fails, might be an offline session - try syncing
|
|
148
|
+
const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError);
|
|
149
|
+
if (errorMessage.includes('AUTH_REQUIRED_OFFLINE_SESSION') || errorMessage.includes('offline')) {
|
|
150
|
+
try {
|
|
151
|
+
await syncIdentity();
|
|
152
|
+
// Retry getting token after sync
|
|
153
|
+
await oxyServices.getTokenBySession(activeSessionId);
|
|
154
|
+
} catch (syncError) {
|
|
155
|
+
throw new Error('Session needs to be synced. Please try again.');
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
throw tokenError;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
return await oxyServices.getPrivacySettings(targetUserId);
|
|
165
|
+
} catch (error: any) {
|
|
166
|
+
const errorMessage = error?.message || '';
|
|
167
|
+
const status = error?.status || error?.response?.status;
|
|
168
|
+
|
|
169
|
+
// Handle authentication errors
|
|
170
|
+
if (status === 401 || errorMessage.includes('Authentication required') || errorMessage.includes('Invalid or missing authorization header')) {
|
|
171
|
+
// Try to sync session and get token
|
|
172
|
+
if (activeSessionId) {
|
|
173
|
+
try {
|
|
174
|
+
await syncIdentity();
|
|
175
|
+
await oxyServices.getTokenBySession(activeSessionId);
|
|
176
|
+
// Retry the request after getting token
|
|
177
|
+
return await oxyServices.getPrivacySettings(targetUserId);
|
|
178
|
+
} catch (retryError) {
|
|
179
|
+
throw new Error('Authentication failed. Please sign in again.');
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
throw new Error('No active session. Please sign in.');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// TanStack Query will automatically retry on network errors
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
enabled: (options?.enabled !== false) && !!targetUserId,
|
|
191
|
+
staleTime: 2 * 60 * 1000, // 2 minutes
|
|
192
|
+
gcTime: 10 * 60 * 1000, // 10 minutes
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
|
|
@@ -89,12 +89,59 @@ export const useDeviceSessions = (options?: { enabled?: boolean }) => {
|
|
|
89
89
|
* Get user devices
|
|
90
90
|
*/
|
|
91
91
|
export const useUserDevices = (options?: { enabled?: boolean }) => {
|
|
92
|
-
const { oxyServices, isAuthenticated } = useOxy();
|
|
92
|
+
const { oxyServices, isAuthenticated, activeSessionId, syncIdentity } = useOxy();
|
|
93
93
|
|
|
94
94
|
return useQuery({
|
|
95
95
|
queryKey: queryKeys.devices.list(),
|
|
96
96
|
queryFn: async () => {
|
|
97
|
-
|
|
97
|
+
// Ensure we have a valid token before making the request
|
|
98
|
+
if (!oxyServices.hasValidToken() && activeSessionId) {
|
|
99
|
+
try {
|
|
100
|
+
// Try to get token for the session
|
|
101
|
+
await oxyServices.getTokenBySession(activeSessionId);
|
|
102
|
+
} catch (tokenError) {
|
|
103
|
+
// If getting token fails, might be an offline session - try syncing
|
|
104
|
+
const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError);
|
|
105
|
+
if (errorMessage.includes('AUTH_REQUIRED_OFFLINE_SESSION') || errorMessage.includes('offline')) {
|
|
106
|
+
try {
|
|
107
|
+
await syncIdentity();
|
|
108
|
+
// Retry getting token after sync
|
|
109
|
+
await oxyServices.getTokenBySession(activeSessionId);
|
|
110
|
+
} catch (syncError) {
|
|
111
|
+
throw new Error('Session needs to be synced. Please try again.');
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
throw tokenError;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
return await oxyServices.getUserDevices();
|
|
121
|
+
} catch (error: any) {
|
|
122
|
+
const errorMessage = error?.message || '';
|
|
123
|
+
const status = error?.status || error?.response?.status;
|
|
124
|
+
|
|
125
|
+
// Handle authentication errors
|
|
126
|
+
if (status === 401 || errorMessage.includes('Authentication required') || errorMessage.includes('Invalid or missing authorization header')) {
|
|
127
|
+
// Try to sync session and get token
|
|
128
|
+
if (activeSessionId) {
|
|
129
|
+
try {
|
|
130
|
+
await syncIdentity();
|
|
131
|
+
await oxyServices.getTokenBySession(activeSessionId);
|
|
132
|
+
// Retry the request after getting token
|
|
133
|
+
return await oxyServices.getUserDevices();
|
|
134
|
+
} catch (retryError) {
|
|
135
|
+
throw new Error('Authentication failed. Please sign in again.');
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
throw new Error('No active session. Please sign in.');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// TanStack Query will automatically retry on network errors
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
98
145
|
},
|
|
99
146
|
enabled: (options?.enabled !== false) && isAuthenticated,
|
|
100
147
|
staleTime: 5 * 60 * 1000,
|
|
@@ -58,43 +58,23 @@ export const useAssets = () => {
|
|
|
58
58
|
clearErrors();
|
|
59
59
|
setUploading(true);
|
|
60
60
|
|
|
61
|
-
//
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
// Initialize progress tracking
|
|
65
|
-
const initialProgress: AssetUploadProgress = {
|
|
66
|
-
fileId: '', // Will be set after init
|
|
67
|
-
uploaded: 0,
|
|
68
|
-
total: file.size,
|
|
69
|
-
percentage: 0,
|
|
70
|
-
status: 'uploading'
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
// Upload with progress callback (visibility undefined, metadata, then onProgress)
|
|
74
|
-
const result = await oxyInstance.assetUpload(file as any, undefined, metadata, (percentage: number) => {
|
|
75
|
-
if (initialProgress.fileId) {
|
|
76
|
-
setUploadProgress(initialProgress.fileId, {
|
|
77
|
-
...initialProgress,
|
|
78
|
-
uploaded: Math.round((percentage / 100) * file.size),
|
|
79
|
-
percentage,
|
|
80
|
-
status: percentage < 100 ? 'uploading' : 'processing'
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
});
|
|
61
|
+
// Upload file (progress tracking simplified for now)
|
|
62
|
+
const result = await oxyInstance.assetUpload(file as any, undefined, metadata);
|
|
84
63
|
|
|
85
64
|
// Update progress with final status
|
|
86
|
-
if (result
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
fileId
|
|
65
|
+
if (result?.file) {
|
|
66
|
+
const fileId = result.file.id;
|
|
67
|
+
setUploadProgress(fileId, {
|
|
68
|
+
fileId,
|
|
90
69
|
uploaded: file.size,
|
|
70
|
+
total: file.size,
|
|
91
71
|
percentage: 100,
|
|
92
72
|
status: 'complete'
|
|
93
73
|
});
|
|
94
74
|
|
|
95
75
|
// Remove progress after a short delay
|
|
96
76
|
setTimeout(() => {
|
|
97
|
-
removeUploadProgress(
|
|
77
|
+
removeUploadProgress(fileId);
|
|
98
78
|
}, 2000);
|
|
99
79
|
}
|
|
100
80
|
|
|
@@ -108,10 +108,11 @@ const AccountOverviewScreen: React.FC<BaseScreenProps> = ({
|
|
|
108
108
|
return undefined;
|
|
109
109
|
}, [user?.avatar, oxyServices]);
|
|
110
110
|
|
|
111
|
-
// Handle avatar press
|
|
111
|
+
// Handle avatar press - use openAvatarPicker from context
|
|
112
|
+
const { openAvatarPicker } = useOxy();
|
|
112
113
|
const handleAvatarPress = useCallback(() => {
|
|
113
|
-
|
|
114
|
-
}, [
|
|
114
|
+
openAvatarPicker();
|
|
115
|
+
}, [openAvatarPicker]);
|
|
115
116
|
|
|
116
117
|
// Play Lottie animation once when component mounts
|
|
117
118
|
useEffect(() => {
|