@oxyhq/services 5.16.7 → 5.16.9
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/ui/context/OxyContext.js +57 -2
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountOverviewScreen.js +5 -7
- 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/utils/fileManagement.js +88 -0
- package/lib/commonjs/ui/utils/fileManagement.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +57 -2
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/screens/AccountOverviewScreen.js +5 -7
- 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/utils/fileManagement.js +87 -0
- package/lib/module/ui/utils/fileManagement.js.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/screens/AccountOverviewScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/AccountSettingsScreen.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/ui/context/OxyContext.tsx +62 -11
- package/src/ui/screens/AccountOverviewScreen.tsx +4 -3
- package/src/ui/screens/AccountSettingsScreen.tsx +3 -5
- package/src/ui/screens/FileManagementScreen.tsx +1 -1
- package/src/ui/utils/fileManagement.ts +105 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/services",
|
|
3
|
-
"version": "5.16.
|
|
3
|
+
"version": "5.16.9",
|
|
4
4
|
"description": "Reusable OxyHQ module to handle authentication, user management, karma system, device-based session management and more 🚀",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
@@ -161,7 +161,8 @@
|
|
|
161
161
|
"react-native-gesture-handler": "~2.28.0",
|
|
162
162
|
"react-native-reanimated": "~4.1.1",
|
|
163
163
|
"react-native-safe-area-context": "~5.6.0",
|
|
164
|
-
"react-native-svg": ">=13.0.0"
|
|
164
|
+
"react-native-svg": ">=13.0.0",
|
|
165
|
+
"expo-document-picker": "~14.0.0"
|
|
165
166
|
},
|
|
166
167
|
"peerDependenciesMeta": {
|
|
167
168
|
"@expo/vector-icons": {
|
|
@@ -190,6 +191,9 @@
|
|
|
190
191
|
},
|
|
191
192
|
"@react-navigation/native": {
|
|
192
193
|
"optional": true
|
|
194
|
+
},
|
|
195
|
+
"expo-document-picker": {
|
|
196
|
+
"optional": true
|
|
193
197
|
}
|
|
194
198
|
},
|
|
195
199
|
"react-native-builder-bob": {
|
|
@@ -30,6 +30,8 @@ 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';
|
|
33
35
|
|
|
34
36
|
export interface OxyContextState {
|
|
35
37
|
user: User | null;
|
|
@@ -84,6 +86,7 @@ export interface OxyContextState {
|
|
|
84
86
|
oxyServices: OxyServices;
|
|
85
87
|
useFollow?: UseFollowHook;
|
|
86
88
|
showBottomSheet?: (screenOrConfig: RouteName | { screen: RouteName; props?: Record<string, unknown> }) => void;
|
|
89
|
+
openAvatarPicker: () => void;
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
const OxyContext = createContext<OxyContextState | null>(null);
|
|
@@ -323,7 +326,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
323
326
|
const clearAllAccountData = useCallback(async (): Promise<void> => {
|
|
324
327
|
// Clear TanStack Query cache (in-memory)
|
|
325
328
|
queryClient.clear();
|
|
326
|
-
|
|
329
|
+
|
|
327
330
|
// Clear persisted query cache
|
|
328
331
|
if (storage) {
|
|
329
332
|
try {
|
|
@@ -334,10 +337,10 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
334
337
|
}
|
|
335
338
|
}
|
|
336
339
|
}
|
|
337
|
-
|
|
340
|
+
|
|
338
341
|
// Clear session state (sessions, activeSessionId, storage)
|
|
339
342
|
await clearSessionState();
|
|
340
|
-
|
|
343
|
+
|
|
341
344
|
// Clear identity sync state from storage
|
|
342
345
|
if (storage) {
|
|
343
346
|
try {
|
|
@@ -348,14 +351,14 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
348
351
|
}
|
|
349
352
|
}
|
|
350
353
|
}
|
|
351
|
-
|
|
354
|
+
|
|
352
355
|
// Reset auth store identity sync state
|
|
353
356
|
useAuthStore.getState().setIdentitySynced(false);
|
|
354
357
|
useAuthStore.getState().setSyncing(false);
|
|
355
|
-
|
|
358
|
+
|
|
356
359
|
// Reset account store
|
|
357
360
|
useAccountStore.getState().reset();
|
|
358
|
-
|
|
361
|
+
|
|
359
362
|
// Clear HTTP service cache
|
|
360
363
|
oxyServices.clearCache();
|
|
361
364
|
}, [queryClient, storage, clearSessionState, logger, oxyServices]);
|
|
@@ -369,7 +372,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
369
372
|
): Promise<void> => {
|
|
370
373
|
// First, clear all account data
|
|
371
374
|
await clearAllAccountData();
|
|
372
|
-
|
|
375
|
+
|
|
373
376
|
// Then delete the identity keys
|
|
374
377
|
await KeyManager.deleteIdentity(skipBackup, force, userConfirmed);
|
|
375
378
|
}, [clearAllAccountData]);
|
|
@@ -381,7 +384,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
381
384
|
|
|
382
385
|
let wasOffline = false;
|
|
383
386
|
let checkTimeout: NodeJS.Timeout | null = null;
|
|
384
|
-
|
|
387
|
+
|
|
385
388
|
// Circuit breaker and exponential backoff state
|
|
386
389
|
const stateRef = {
|
|
387
390
|
consecutiveFailures: 0,
|
|
@@ -438,10 +441,10 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
438
441
|
} catch (error) {
|
|
439
442
|
// Network check failed - we're offline
|
|
440
443
|
wasOffline = true;
|
|
441
|
-
|
|
444
|
+
|
|
442
445
|
// Increment failure count and apply exponential backoff
|
|
443
446
|
stateRef.consecutiveFailures++;
|
|
444
|
-
|
|
447
|
+
|
|
445
448
|
// Calculate new interval with exponential backoff, capped at maxInterval
|
|
446
449
|
const backoffMultiplier = Math.min(
|
|
447
450
|
Math.pow(2, stateRef.consecutiveFailures - 1),
|
|
@@ -451,7 +454,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
451
454
|
stateRef.baseInterval * backoffMultiplier,
|
|
452
455
|
stateRef.maxInterval
|
|
453
456
|
);
|
|
454
|
-
|
|
457
|
+
|
|
455
458
|
// If we hit the circuit breaker threshold, use max interval
|
|
456
459
|
if (stateRef.consecutiveFailures >= stateRef.maxFailures) {
|
|
457
460
|
stateRef.currentInterval = stateRef.maxInterval;
|
|
@@ -615,6 +618,52 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
615
618
|
[],
|
|
616
619
|
);
|
|
617
620
|
|
|
621
|
+
// Create openAvatarPicker function
|
|
622
|
+
const openAvatarPicker = useCallback(() => {
|
|
623
|
+
showBottomSheetForContext({
|
|
624
|
+
screen: 'FileManagement' as RouteName,
|
|
625
|
+
props: {
|
|
626
|
+
selectMode: true,
|
|
627
|
+
multiSelect: false,
|
|
628
|
+
disabledMimeTypes: ['video/', 'audio/', 'application/pdf'],
|
|
629
|
+
afterSelect: 'none', // Don't navigate away - stay on current screen
|
|
630
|
+
onSelect: async (file: any) => {
|
|
631
|
+
if (!file.contentType.startsWith('image/')) {
|
|
632
|
+
toast.error(translate(currentLanguage, 'editProfile.toasts.selectImage') || 'Please select an image file');
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
try {
|
|
636
|
+
// Update file visibility to public for avatar (skip if temporary asset ID)
|
|
637
|
+
if (file.id && !file.id.startsWith('temp-')) {
|
|
638
|
+
try {
|
|
639
|
+
await oxyServices.assetUpdateVisibility(file.id, 'public');
|
|
640
|
+
console.log('[OxyContext] Avatar visibility updated to public');
|
|
641
|
+
} catch (visError: any) {
|
|
642
|
+
// Only log non-404 errors (404 means asset doesn't exist yet, which is OK)
|
|
643
|
+
if (visError?.response?.status !== 404) {
|
|
644
|
+
console.warn('[OxyContext] Failed to update avatar visibility, continuing anyway:', visError);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Update user profile directly
|
|
650
|
+
await oxyServices.updateProfile({ avatar: file.id });
|
|
651
|
+
|
|
652
|
+
// Invalidate queries to refresh user data
|
|
653
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.accounts.current() });
|
|
654
|
+
if (activeSessionId) {
|
|
655
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.users.profile(activeSessionId) });
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
toast.success(translate(currentLanguage, 'editProfile.toasts.avatarUpdated') || 'Avatar updated');
|
|
659
|
+
} catch (e: any) {
|
|
660
|
+
toast.error(e.message || translate(currentLanguage, 'editProfile.toasts.updateAvatarFailed') || 'Failed to update avatar');
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
},
|
|
664
|
+
});
|
|
665
|
+
}, [oxyServices, currentLanguage, showBottomSheetForContext, queryClient, activeSessionId]);
|
|
666
|
+
|
|
618
667
|
const contextValue: OxyContextState = useMemo(() => ({
|
|
619
668
|
user,
|
|
620
669
|
sessions,
|
|
@@ -654,6 +703,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
654
703
|
oxyServices,
|
|
655
704
|
useFollow: useFollowHook,
|
|
656
705
|
showBottomSheet: showBottomSheetForContext,
|
|
706
|
+
openAvatarPicker,
|
|
657
707
|
}), [
|
|
658
708
|
activeSessionId,
|
|
659
709
|
createIdentity,
|
|
@@ -689,6 +739,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
689
739
|
useFollowHook,
|
|
690
740
|
user,
|
|
691
741
|
showBottomSheetForContext,
|
|
742
|
+
openAvatarPicker,
|
|
692
743
|
]);
|
|
693
744
|
|
|
694
745
|
return (
|
|
@@ -77,6 +77,7 @@ const AccountOverviewScreen: React.FC<BaseScreenProps> = ({
|
|
|
77
77
|
activeSessionId,
|
|
78
78
|
oxyServices,
|
|
79
79
|
isAuthenticated,
|
|
80
|
+
openAvatarPicker,
|
|
80
81
|
} = useOxy();
|
|
81
82
|
const { t } = useI18n();
|
|
82
83
|
const [showMoreAccounts, setShowMoreAccounts] = useState(false);
|
|
@@ -108,10 +109,10 @@ const AccountOverviewScreen: React.FC<BaseScreenProps> = ({
|
|
|
108
109
|
return undefined;
|
|
109
110
|
}, [user?.avatar, oxyServices]);
|
|
110
111
|
|
|
111
|
-
// Handle avatar press
|
|
112
|
+
// Handle avatar press - use openAvatarPicker from context
|
|
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(() => {
|
|
@@ -75,12 +75,12 @@ const AccountSettingsScreen: React.FC<BaseScreenProps & { initialField?: string;
|
|
|
75
75
|
} = useOxy();
|
|
76
76
|
const { t } = useI18n();
|
|
77
77
|
const normalizedTheme = normalizeTheme(theme);
|
|
78
|
-
|
|
78
|
+
|
|
79
79
|
// Use TanStack Query for user data
|
|
80
80
|
const { data: user, isLoading: userLoading } = useCurrentUser({ enabled: isAuthenticated });
|
|
81
81
|
const updateProfileMutation = useUpdateProfile();
|
|
82
82
|
const uploadAvatarMutation = useUploadAvatar();
|
|
83
|
-
|
|
83
|
+
|
|
84
84
|
// Fallback to store for backward compatibility
|
|
85
85
|
const userFromStore = useAuthStore((state) => state.user);
|
|
86
86
|
const finalUser = user || userFromStore;
|
|
@@ -503,9 +503,7 @@ const AccountSettingsScreen: React.FC<BaseScreenProps & { initialField?: string;
|
|
|
503
503
|
});
|
|
504
504
|
};
|
|
505
505
|
|
|
506
|
-
const openAvatarPicker =
|
|
507
|
-
toast.info?.(t('editProfile.toasts.avatarPickerUnavailable') || 'Avatar picker is not available in this build.');
|
|
508
|
-
}, [t]);
|
|
506
|
+
const { openAvatarPicker } = useOxy();
|
|
509
507
|
|
|
510
508
|
// Handlers to open modals
|
|
511
509
|
const handleOpenDisplayNameModal = useCallback(() => setShowEditDisplayNameModal(true), []);
|
|
@@ -769,7 +769,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
769
769
|
|
|
770
770
|
try {
|
|
771
771
|
setIsPickingDocument(true);
|
|
772
|
-
|
|
772
|
+
|
|
773
773
|
// Use expo-document-picker (works on all platforms including web)
|
|
774
774
|
// On web, it uses the native file input and provides File objects directly
|
|
775
775
|
const result = await DocumentPicker.getDocumentAsync({
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Alert } from 'react-native';
|
|
2
2
|
import type { FileMetadata } from '../../models/interfaces';
|
|
3
3
|
import { File as ExpoFile } from 'expo-file-system';
|
|
4
|
+
import { toast } from '../../lib/sonner';
|
|
5
|
+
import type { RouteName } from '../navigation/routes';
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Format file size in bytes to human-readable string
|
|
@@ -188,3 +190,106 @@ export async function uploadFileRaw(
|
|
|
188
190
|
return await oxyServices.uploadRawFile(file, visibility);
|
|
189
191
|
}
|
|
190
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Configuration for creating an avatar picker handler
|
|
195
|
+
*/
|
|
196
|
+
export interface AvatarPickerConfig {
|
|
197
|
+
/** Navigation function from BaseScreenProps */
|
|
198
|
+
navigate?: (screen: RouteName, props?: Record<string, unknown>) => void;
|
|
199
|
+
/** OxyServices instance */
|
|
200
|
+
oxyServices: any;
|
|
201
|
+
/** TanStack Query mutation for updating profile */
|
|
202
|
+
updateProfileMutation: {
|
|
203
|
+
mutateAsync: (updates: { avatar: string }) => Promise<any>;
|
|
204
|
+
};
|
|
205
|
+
/** Callback to update local avatar state */
|
|
206
|
+
onAvatarSelected?: (fileId: string) => void;
|
|
207
|
+
/** i18n translation function */
|
|
208
|
+
t: (key: string) => string | undefined;
|
|
209
|
+
/** Optional context name for logging (e.g., 'AccountSettings', 'WelcomeNewUser') */
|
|
210
|
+
contextName?: string;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Creates a reusable avatar picker handler function.
|
|
215
|
+
*
|
|
216
|
+
* This function navigates to the FileManagement screen and handles:
|
|
217
|
+
* - Image file validation
|
|
218
|
+
* - File visibility update to public
|
|
219
|
+
* - Profile avatar update via mutation
|
|
220
|
+
* - Success/error toast notifications
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* ```tsx
|
|
224
|
+
* const openAvatarPicker = createAvatarPickerHandler({
|
|
225
|
+
* navigate,
|
|
226
|
+
* oxyServices,
|
|
227
|
+
* updateProfileMutation,
|
|
228
|
+
* onAvatarSelected: setAvatarFileId,
|
|
229
|
+
* t,
|
|
230
|
+
* contextName: 'AccountSettings'
|
|
231
|
+
* });
|
|
232
|
+
*
|
|
233
|
+
* <TouchableOpacity onPress={openAvatarPicker}>
|
|
234
|
+
* <Text>Change Avatar</Text>
|
|
235
|
+
* </TouchableOpacity>
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
export function createAvatarPickerHandler(config: AvatarPickerConfig): () => void {
|
|
239
|
+
const {
|
|
240
|
+
navigate,
|
|
241
|
+
oxyServices,
|
|
242
|
+
updateProfileMutation,
|
|
243
|
+
onAvatarSelected,
|
|
244
|
+
t,
|
|
245
|
+
contextName = 'AvatarPicker'
|
|
246
|
+
} = config;
|
|
247
|
+
|
|
248
|
+
return () => {
|
|
249
|
+
if (!navigate) {
|
|
250
|
+
console.warn(`[${contextName}] navigate function is not available`);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
navigate('FileManagement', {
|
|
255
|
+
selectMode: true,
|
|
256
|
+
multiSelect: false,
|
|
257
|
+
disabledMimeTypes: ['video/', 'audio/', 'application/pdf'],
|
|
258
|
+
afterSelect: 'none', // Don't navigate away - stay on current screen
|
|
259
|
+
onSelect: async (file: any) => {
|
|
260
|
+
if (!file.contentType.startsWith('image/')) {
|
|
261
|
+
toast.error(t('editProfile.toasts.selectImage') || 'Please select an image file');
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
// Update file visibility to public for avatar (skip if temporary asset ID)
|
|
267
|
+
if (file.id && !file.id.startsWith('temp-')) {
|
|
268
|
+
try {
|
|
269
|
+
await oxyServices.assetUpdateVisibility(file.id, 'public');
|
|
270
|
+
console.log(`[${contextName}] Avatar visibility updated to public`);
|
|
271
|
+
} catch (visError: any) {
|
|
272
|
+
// Only log non-404 errors (404 means asset doesn't exist yet, which is OK)
|
|
273
|
+
if (visError?.response?.status !== 404) {
|
|
274
|
+
console.warn(`[${contextName}] Failed to update avatar visibility, continuing anyway:`, visError);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Update local state if callback provided
|
|
280
|
+
if (onAvatarSelected) {
|
|
281
|
+
onAvatarSelected(file.id);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Update user using TanStack Query mutation
|
|
285
|
+
await updateProfileMutation.mutateAsync({ avatar: file.id });
|
|
286
|
+
|
|
287
|
+
toast.success(t('editProfile.toasts.avatarUpdated') || 'Avatar updated');
|
|
288
|
+
} catch (e: any) {
|
|
289
|
+
toast.error(e.message || t('editProfile.toasts.updateAvatarFailed') || 'Failed to update avatar');
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|