@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.
Files changed (102) hide show
  1. package/lib/commonjs/core/OxyServices.base.js +3 -1
  2. package/lib/commonjs/core/OxyServices.base.js.map +1 -1
  3. package/lib/commonjs/core/mixins/OxyServices.assets.js +20 -330
  4. package/lib/commonjs/core/mixins/OxyServices.assets.js.map +1 -1
  5. package/lib/commonjs/index.js +156 -0
  6. package/lib/commonjs/index.js.map +1 -1
  7. package/lib/commonjs/ui/context/OxyContext.js +95 -9
  8. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  9. package/lib/commonjs/ui/hooks/mutations/index.js +60 -20
  10. package/lib/commonjs/ui/hooks/mutations/index.js.map +1 -1
  11. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +230 -1
  12. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  13. package/lib/commonjs/ui/hooks/queries/index.js +96 -30
  14. package/lib/commonjs/ui/hooks/queries/index.js.map +1 -1
  15. package/lib/commonjs/ui/hooks/queries/queryKeys.js +5 -0
  16. package/lib/commonjs/ui/hooks/queries/queryKeys.js.map +1 -1
  17. package/lib/commonjs/ui/hooks/queries/useAccountQueries.js +75 -1
  18. package/lib/commonjs/ui/hooks/queries/useAccountQueries.js.map +1 -1
  19. package/lib/commonjs/ui/hooks/queries/useServicesQueries.js +50 -2
  20. package/lib/commonjs/ui/hooks/queries/useServicesQueries.js.map +1 -1
  21. package/lib/commonjs/ui/hooks/useAssets.js +8 -29
  22. package/lib/commonjs/ui/hooks/useAssets.js.map +1 -1
  23. package/lib/commonjs/ui/screens/AccountOverviewScreen.js +6 -6
  24. package/lib/commonjs/ui/screens/AccountOverviewScreen.js.map +1 -1
  25. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +3 -3
  26. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
  27. package/lib/commonjs/ui/screens/FileManagementScreen.js +14 -10
  28. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  29. package/lib/commonjs/ui/utils/fileManagement.js +88 -0
  30. package/lib/commonjs/ui/utils/fileManagement.js.map +1 -1
  31. package/lib/module/core/OxyServices.base.js +3 -1
  32. package/lib/module/core/OxyServices.base.js.map +1 -1
  33. package/lib/module/core/mixins/OxyServices.assets.js +20 -331
  34. package/lib/module/core/mixins/OxyServices.assets.js.map +1 -1
  35. package/lib/module/index.js +17 -1
  36. package/lib/module/index.js.map +1 -1
  37. package/lib/module/ui/context/OxyContext.js +95 -9
  38. package/lib/module/ui/context/OxyContext.js.map +1 -1
  39. package/lib/module/ui/hooks/mutations/index.js +12 -3
  40. package/lib/module/ui/hooks/mutations/index.js.map +1 -1
  41. package/lib/module/ui/hooks/mutations/useAccountMutations.js +227 -0
  42. package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  43. package/lib/module/ui/hooks/queries/index.js +15 -4
  44. package/lib/module/ui/hooks/queries/index.js.map +1 -1
  45. package/lib/module/ui/hooks/queries/queryKeys.js +5 -0
  46. package/lib/module/ui/hooks/queries/queryKeys.js.map +1 -1
  47. package/lib/module/ui/hooks/queries/useAccountQueries.js +73 -0
  48. package/lib/module/ui/hooks/queries/useAccountQueries.js.map +1 -1
  49. package/lib/module/ui/hooks/queries/useServicesQueries.js +50 -2
  50. package/lib/module/ui/hooks/queries/useServicesQueries.js.map +1 -1
  51. package/lib/module/ui/hooks/useAssets.js +8 -29
  52. package/lib/module/ui/hooks/useAssets.js.map +1 -1
  53. package/lib/module/ui/screens/AccountOverviewScreen.js +6 -6
  54. package/lib/module/ui/screens/AccountOverviewScreen.js.map +1 -1
  55. package/lib/module/ui/screens/AccountSettingsScreen.js +3 -3
  56. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  57. package/lib/module/ui/screens/FileManagementScreen.js +12 -10
  58. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  59. package/lib/module/ui/utils/fileManagement.js +87 -0
  60. package/lib/module/ui/utils/fileManagement.js.map +1 -1
  61. package/lib/typescript/core/OxyServices.base.d.ts.map +1 -1
  62. package/lib/typescript/core/mixins/OxyServices.assets.d.ts +1 -70
  63. package/lib/typescript/core/mixins/OxyServices.assets.d.ts.map +1 -1
  64. package/lib/typescript/core/mixins/index.d.ts +4 -14
  65. package/lib/typescript/core/mixins/index.d.ts.map +1 -1
  66. package/lib/typescript/index.d.ts +2 -0
  67. package/lib/typescript/index.d.ts.map +1 -1
  68. package/lib/typescript/ui/context/OxyContext.d.ts +1 -0
  69. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  70. package/lib/typescript/ui/hooks/mutations/index.d.ts +8 -2
  71. package/lib/typescript/ui/hooks/mutations/index.d.ts.map +1 -1
  72. package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts +19 -0
  73. package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  74. package/lib/typescript/ui/hooks/queries/index.d.ts +9 -3
  75. package/lib/typescript/ui/hooks/queries/index.d.ts.map +1 -1
  76. package/lib/typescript/ui/hooks/queries/queryKeys.d.ts +4 -0
  77. package/lib/typescript/ui/hooks/queries/queryKeys.d.ts.map +1 -1
  78. package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts +6 -0
  79. package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
  80. package/lib/typescript/ui/hooks/queries/useServicesQueries.d.ts.map +1 -1
  81. package/lib/typescript/ui/hooks/useAssets.d.ts.map +1 -1
  82. package/lib/typescript/ui/screens/AccountOverviewScreen.d.ts.map +1 -1
  83. package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  84. package/lib/typescript/ui/screens/FileManagementScreen.d.ts.map +1 -1
  85. package/lib/typescript/ui/utils/fileManagement.d.ts +48 -0
  86. package/lib/typescript/ui/utils/fileManagement.d.ts.map +1 -1
  87. package/package.json +6 -2
  88. package/src/core/OxyServices.base.ts +5 -1
  89. package/src/core/mixins/OxyServices.assets.ts +21 -338
  90. package/src/index.ts +49 -2
  91. package/src/ui/context/OxyContext.tsx +98 -7
  92. package/src/ui/hooks/mutations/index.ts +24 -3
  93. package/src/ui/hooks/mutations/useAccountMutations.ts +205 -0
  94. package/src/ui/hooks/queries/index.ts +29 -4
  95. package/src/ui/hooks/queries/queryKeys.ts +6 -0
  96. package/src/ui/hooks/queries/useAccountQueries.ts +69 -0
  97. package/src/ui/hooks/queries/useServicesQueries.ts +49 -2
  98. package/src/ui/hooks/useAssets.ts +8 -28
  99. package/src/ui/screens/AccountOverviewScreen.tsx +4 -3
  100. package/src/ui/screens/AccountSettingsScreen.tsx +3 -5
  101. package/src/ui/screens/FileManagementScreen.tsx +10 -11
  102. 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 checkInterval: NodeJS.Timeout | null = null;
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
- return;
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 (checkInterval) {
428
- clearInterval(checkInterval);
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
- // Export all mutation hooks
2
- export * from './useAccountMutations';
3
- export * from './useServicesMutations';
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
- // Export all query hooks
2
- export * from './useAccountQueries';
3
- export * from './useServicesQueries';
4
- export * from './queryKeys';
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
- return await oxyServices.getUserDevices();
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
- // Calculate SHA256 for progress tracking
62
- const sha256 = await oxyInstance.calculateSHA256(file);
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.file && initialProgress.fileId) {
87
- setUploadProgress(initialProgress.fileId, {
88
- ...initialProgress,
89
- fileId: result.file.id,
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(result.file.id);
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 to navigate to EditProfile
111
+ // Handle avatar press - use openAvatarPicker from context
112
+ const { openAvatarPicker } = useOxy();
112
113
  const handleAvatarPress = useCallback(() => {
113
- navigate?.('EditProfile', { initialSection: 'profilePicture', initialField: 'avatar' });
114
- }, [navigate]);
114
+ openAvatarPicker();
115
+ }, [openAvatarPicker]);
115
116
 
116
117
  // Play Lottie animation once when component mounts
117
118
  useEffect(() => {