@oxyhq/services 5.21.5 → 5.21.7
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/crypto/keyManager.js +67 -22
- package/lib/commonjs/crypto/keyManager.js.map +1 -1
- package/lib/commonjs/index.js +66 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/BottomSheetRouter.js +100 -286
- package/lib/commonjs/ui/components/BottomSheetRouter.js.map +1 -1
- package/lib/commonjs/ui/components/GroupedItem.js +0 -3
- package/lib/commonjs/ui/components/GroupedItem.js.map +1 -1
- package/lib/commonjs/ui/components/OxyProvider.js +14 -19
- package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
- package/lib/commonjs/ui/components/fileManagement/AnimatedButton.js +57 -0
- package/lib/commonjs/ui/components/fileManagement/AnimatedButton.js.map +1 -0
- package/lib/commonjs/ui/components/profile/EditBioModal.js +24 -156
- package/lib/commonjs/ui/components/profile/EditBioModal.js.map +1 -1
- package/lib/commonjs/ui/components/profile/EditDisplayNameModal.js +28 -178
- package/lib/commonjs/ui/components/profile/EditDisplayNameModal.js.map +1 -1
- package/lib/commonjs/ui/components/profile/EditEmailModal.js +32 -159
- package/lib/commonjs/ui/components/profile/EditEmailModal.js.map +1 -1
- package/lib/commonjs/ui/components/profile/EditLocationModal.js +45 -227
- package/lib/commonjs/ui/components/profile/EditLocationModal.js.map +1 -1
- package/lib/commonjs/ui/components/profile/EditUsernameModal.js +30 -155
- package/lib/commonjs/ui/components/profile/EditUsernameModal.js.map +1 -1
- package/lib/commonjs/ui/hooks/mutations/mutationFactory.js +177 -0
- package/lib/commonjs/ui/hooks/mutations/mutationFactory.js.map +1 -0
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +10 -123
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/useAccountQueries.js +2 -32
- package/lib/commonjs/ui/hooks/queries/useAccountQueries.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/useServicesQueries.js +2 -31
- package/lib/commonjs/ui/hooks/queries/useServicesQueries.js.map +1 -1
- package/lib/commonjs/ui/hooks/useFileFiltering.js +76 -0
- package/lib/commonjs/ui/hooks/useFileFiltering.js.map +1 -0
- package/lib/commonjs/ui/navigation/bottomSheetManager.js +43 -145
- package/lib/commonjs/ui/navigation/bottomSheetManager.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js +0 -2
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/FileManagementScreen.js +2 -2
- package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/commonjs/ui/utils/authHelpers.js +164 -0
- package/lib/commonjs/ui/utils/authHelpers.js.map +1 -0
- package/lib/commonjs/ui/utils/avatarUtils.js +18 -61
- package/lib/commonjs/ui/utils/avatarUtils.js.map +1 -1
- package/lib/module/crypto/keyManager.js +67 -22
- package/lib/module/crypto/keyManager.js.map +1 -1
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/BottomSheetRouter.js +102 -284
- package/lib/module/ui/components/BottomSheetRouter.js.map +1 -1
- package/lib/module/ui/components/GroupedItem.js +0 -3
- package/lib/module/ui/components/GroupedItem.js.map +1 -1
- package/lib/module/ui/components/OxyProvider.js +14 -19
- package/lib/module/ui/components/OxyProvider.js.map +1 -1
- package/lib/module/ui/components/fileManagement/AnimatedButton.js +50 -0
- package/lib/module/ui/components/fileManagement/AnimatedButton.js.map +1 -0
- package/lib/module/ui/components/profile/EditBioModal.js +24 -156
- package/lib/module/ui/components/profile/EditBioModal.js.map +1 -1
- package/lib/module/ui/components/profile/EditDisplayNameModal.js +28 -178
- package/lib/module/ui/components/profile/EditDisplayNameModal.js.map +1 -1
- package/lib/module/ui/components/profile/EditEmailModal.js +32 -159
- package/lib/module/ui/components/profile/EditEmailModal.js.map +1 -1
- package/lib/module/ui/components/profile/EditLocationModal.js +45 -227
- package/lib/module/ui/components/profile/EditLocationModal.js.map +1 -1
- package/lib/module/ui/components/profile/EditUsernameModal.js +30 -155
- package/lib/module/ui/components/profile/EditUsernameModal.js.map +1 -1
- package/lib/module/ui/hooks/mutations/mutationFactory.js +173 -0
- package/lib/module/ui/hooks/mutations/mutationFactory.js.map +1 -0
- package/lib/module/ui/hooks/mutations/useAccountMutations.js +10 -122
- package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/module/ui/hooks/queries/useAccountQueries.js +2 -32
- package/lib/module/ui/hooks/queries/useAccountQueries.js.map +1 -1
- package/lib/module/ui/hooks/queries/useServicesQueries.js +2 -31
- package/lib/module/ui/hooks/queries/useServicesQueries.js.map +1 -1
- package/lib/module/ui/hooks/useFileFiltering.js +72 -0
- package/lib/module/ui/hooks/useFileFiltering.js.map +1 -0
- package/lib/module/ui/navigation/bottomSheetManager.js +37 -135
- package/lib/module/ui/navigation/bottomSheetManager.js.map +1 -1
- package/lib/module/ui/screens/AccountSettingsScreen.js +0 -2
- package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/module/ui/screens/FileManagementScreen.js +2 -2
- package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/module/ui/utils/authHelpers.js +154 -0
- package/lib/module/ui/utils/authHelpers.js.map +1 -0
- package/lib/module/ui/utils/avatarUtils.js +18 -61
- package/lib/module/ui/utils/avatarUtils.js.map +1 -1
- package/lib/typescript/commonjs/crypto/keyManager.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +6 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/BottomSheetRouter.d.ts +2 -7
- package/lib/typescript/commonjs/ui/components/BottomSheetRouter.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/GroupedItem.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/fileManagement/AnimatedButton.d.ts +16 -0
- package/lib/typescript/commonjs/ui/components/fileManagement/AnimatedButton.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/profile/EditBioModal.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/profile/EditDisplayNameModal.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/profile/EditEmailModal.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/profile/EditLocationModal.d.ts +1 -0
- package/lib/typescript/commonjs/ui/components/profile/EditLocationModal.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/profile/EditUsernameModal.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/mutationFactory.d.ts +76 -0
- package/lib/typescript/commonjs/ui/hooks/mutations/mutationFactory.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +29 -4
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/queries/useAccountQueries.d.ts +1 -1
- package/lib/typescript/commonjs/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/queries/useServicesQueries.d.ts +1 -1
- package/lib/typescript/commonjs/ui/hooks/queries/useServicesQueries.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useFileFiltering.d.ts +29 -0
- package/lib/typescript/commonjs/ui/hooks/useFileFiltering.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/navigation/bottomSheetManager.d.ts +11 -60
- package/lib/typescript/commonjs/ui/navigation/bottomSheetManager.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/authHelpers.d.ts +99 -0
- package/lib/typescript/commonjs/ui/utils/authHelpers.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/utils/avatarUtils.d.ts.map +1 -1
- package/lib/typescript/module/crypto/keyManager.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +6 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/BottomSheetRouter.d.ts +2 -7
- package/lib/typescript/module/ui/components/BottomSheetRouter.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/GroupedItem.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/fileManagement/AnimatedButton.d.ts +16 -0
- package/lib/typescript/module/ui/components/fileManagement/AnimatedButton.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/profile/EditBioModal.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/profile/EditDisplayNameModal.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/profile/EditEmailModal.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/profile/EditLocationModal.d.ts +1 -0
- package/lib/typescript/module/ui/components/profile/EditLocationModal.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/profile/EditUsernameModal.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/mutations/mutationFactory.d.ts +76 -0
- package/lib/typescript/module/ui/hooks/mutations/mutationFactory.d.ts.map +1 -0
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +29 -4
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/queries/useAccountQueries.d.ts +1 -1
- package/lib/typescript/module/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/queries/useServicesQueries.d.ts +1 -1
- package/lib/typescript/module/ui/hooks/queries/useServicesQueries.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useFileFiltering.d.ts +29 -0
- package/lib/typescript/module/ui/hooks/useFileFiltering.d.ts.map +1 -0
- package/lib/typescript/module/ui/navigation/bottomSheetManager.d.ts +11 -60
- package/lib/typescript/module/ui/navigation/bottomSheetManager.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/authHelpers.d.ts +99 -0
- package/lib/typescript/module/ui/utils/authHelpers.d.ts.map +1 -0
- package/lib/typescript/module/ui/utils/avatarUtils.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/crypto/keyManager.ts +23 -22
- package/src/index.ts +25 -0
- package/src/ui/components/BottomSheetRouter.tsx +97 -319
- package/src/ui/components/GroupedItem.tsx +0 -4
- package/src/ui/components/OxyProvider.tsx +13 -18
- package/src/ui/components/fileManagement/AnimatedButton.tsx +56 -0
- package/src/ui/components/profile/EditBioModal.tsx +38 -176
- package/src/ui/components/profile/EditDisplayNameModal.tsx +48 -195
- package/src/ui/components/profile/EditEmailModal.tsx +49 -180
- package/src/ui/components/profile/EditLocationModal.tsx +76 -263
- package/src/ui/components/profile/EditUsernameModal.tsx +47 -175
- package/src/ui/hooks/mutations/mutationFactory.ts +215 -0
- package/src/ui/hooks/mutations/useAccountMutations.ts +48 -136
- package/src/ui/hooks/queries/useAccountQueries.ts +6 -33
- package/src/ui/hooks/queries/useServicesQueries.ts +6 -32
- package/src/ui/hooks/useFileFiltering.ts +115 -0
- package/src/ui/navigation/bottomSheetManager.ts +43 -150
- package/src/ui/screens/AccountSettingsScreen.tsx +0 -2
- package/src/ui/screens/FileManagementScreen.tsx +2 -2
- package/src/ui/utils/authHelpers.ts +183 -0
- package/src/ui/utils/avatarUtils.ts +25 -65
- package/lib/commonjs/ui/hooks/use-haptic-press.js +0 -21
- package/lib/commonjs/ui/hooks/use-haptic-press.js.map +0 -1
- package/lib/module/ui/hooks/use-haptic-press.js +0 -17
- package/lib/module/ui/hooks/use-haptic-press.js.map +0 -1
- package/lib/typescript/commonjs/ui/hooks/use-haptic-press.d.ts +0 -8
- package/lib/typescript/commonjs/ui/hooks/use-haptic-press.d.ts.map +0 -1
- package/lib/typescript/module/ui/hooks/use-haptic-press.d.ts +0 -8
- package/lib/typescript/module/ui/hooks/use-haptic-press.d.ts.map +0 -1
- package/src/ui/hooks/use-haptic-press.ts +0 -15
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useQuery, useQueries } from '@tanstack/react-query';
|
|
2
2
|
import type { User } from '../../../models/interfaces';
|
|
3
|
-
import type { OxyServices } from '../../../core';
|
|
4
3
|
import { queryKeys } from './queryKeys';
|
|
5
4
|
import { useOxy } from '../../context/OxyContext';
|
|
5
|
+
import { authenticatedApiCall } from '../../utils/authHelpers';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Get user profile by session ID
|
|
@@ -138,38 +138,11 @@ export const usePrivacySettings = (userId?: string, options?: { enabled?: boolea
|
|
|
138
138
|
throw new Error('User ID is required');
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
// Session sync should be handled by the app layer (e.g., accounts app's useIdentity hook)
|
|
151
|
-
throw new Error('Session needs to be synced. Please try again.');
|
|
152
|
-
} else {
|
|
153
|
-
throw tokenError;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
try {
|
|
159
|
-
return await oxyServices.getPrivacySettings(targetUserId);
|
|
160
|
-
} catch (error: any) {
|
|
161
|
-
const errorMessage = error?.message || '';
|
|
162
|
-
const status = error?.status || error?.response?.status;
|
|
163
|
-
|
|
164
|
-
// Handle authentication errors
|
|
165
|
-
if (status === 401 || errorMessage.includes('Authentication required') || errorMessage.includes('Invalid or missing authorization header')) {
|
|
166
|
-
// Session sync should be handled by the app layer
|
|
167
|
-
throw new Error('Authentication failed. Please sign in again.');
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// TanStack Query will automatically retry on network errors
|
|
171
|
-
throw error;
|
|
172
|
-
}
|
|
141
|
+
return authenticatedApiCall(
|
|
142
|
+
oxyServices,
|
|
143
|
+
activeSessionId,
|
|
144
|
+
() => oxyServices.getPrivacySettings(targetUserId)
|
|
145
|
+
);
|
|
173
146
|
},
|
|
174
147
|
enabled: (options?.enabled !== false) && !!targetUserId,
|
|
175
148
|
staleTime: 2 * 60 * 1000, // 2 minutes
|
|
@@ -3,6 +3,7 @@ import type { ClientSession } from '../../../models/session';
|
|
|
3
3
|
import { queryKeys } from './queryKeys';
|
|
4
4
|
import { useOxy } from '../../context/OxyContext';
|
|
5
5
|
import { fetchSessionsWithFallback, mapSessionsToClient } from '../../utils/sessionHelpers';
|
|
6
|
+
import { authenticatedApiCall } from '../../utils/authHelpers';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Get all active sessions for the current user
|
|
@@ -94,38 +95,11 @@ export const useUserDevices = (options?: { enabled?: boolean }) => {
|
|
|
94
95
|
return useQuery({
|
|
95
96
|
queryKey: queryKeys.devices.list(),
|
|
96
97
|
queryFn: async () => {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
// Session sync should be handled by the app layer
|
|
107
|
-
throw new Error('Session needs to be synced. Please try again.');
|
|
108
|
-
} else {
|
|
109
|
-
throw tokenError;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
return await oxyServices.getUserDevices();
|
|
116
|
-
} catch (error: any) {
|
|
117
|
-
const errorMessage = error?.message || '';
|
|
118
|
-
const status = error?.status || error?.response?.status;
|
|
119
|
-
|
|
120
|
-
// Handle authentication errors
|
|
121
|
-
if (status === 401 || errorMessage.includes('Authentication required') || errorMessage.includes('Invalid or missing authorization header')) {
|
|
122
|
-
// Session sync should be handled by the app layer
|
|
123
|
-
throw new Error('Authentication failed. Please sign in again.');
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// TanStack Query will automatically retry on network errors
|
|
127
|
-
throw error;
|
|
128
|
-
}
|
|
98
|
+
return authenticatedApiCall(
|
|
99
|
+
oxyServices,
|
|
100
|
+
activeSessionId,
|
|
101
|
+
() => oxyServices.getUserDevices()
|
|
102
|
+
);
|
|
129
103
|
},
|
|
130
104
|
enabled: (options?.enabled !== false) && isAuthenticated,
|
|
131
105
|
staleTime: 5 * 60 * 1000,
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { useMemo, useState, useCallback } from 'react';
|
|
2
|
+
import type { FileMetadata } from '../../models/interfaces';
|
|
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
|
+
}
|
|
@@ -3,189 +3,82 @@ import { isValidRoute } from './routes';
|
|
|
3
3
|
import { createStore } from 'zustand/vanilla';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Bottom Sheet Manager
|
|
7
|
-
*
|
|
8
|
-
* Uses Zustand (vanilla) for robust state management without React dependencies.
|
|
9
|
-
* This ensures the manager can be imported safely anywhere.
|
|
6
|
+
* Bottom Sheet State Manager
|
|
10
7
|
*/
|
|
11
8
|
|
|
12
|
-
export interface
|
|
13
|
-
screen: RouteName;
|
|
14
|
-
props: Record<string, unknown>;
|
|
15
|
-
step?: number; // For step-based screens
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface BottomSheetRouterState {
|
|
9
|
+
export interface BottomSheetState {
|
|
19
10
|
currentScreen: RouteName | null;
|
|
20
11
|
screenProps: Record<string, unknown>;
|
|
21
|
-
currentStep?: number;
|
|
22
|
-
|
|
12
|
+
currentStep?: number;
|
|
13
|
+
history: Array<{ screen: RouteName; props: Record<string, unknown>; step?: number }>;
|
|
23
14
|
isOpen: boolean;
|
|
24
15
|
}
|
|
25
16
|
|
|
26
|
-
|
|
27
|
-
const initialState: BottomSheetRouterState = {
|
|
17
|
+
const initialState: BottomSheetState = {
|
|
28
18
|
currentScreen: null,
|
|
29
19
|
screenProps: {},
|
|
30
20
|
currentStep: undefined,
|
|
31
|
-
|
|
21
|
+
history: [],
|
|
32
22
|
isOpen: false,
|
|
33
23
|
};
|
|
34
24
|
|
|
35
|
-
|
|
36
|
-
export const bottomSheetStore = createStore<BottomSheetRouterState>(() => initialState);
|
|
25
|
+
export const bottomSheetStore = createStore<BottomSheetState>(() => initialState);
|
|
37
26
|
|
|
38
|
-
|
|
39
|
-
type BottomSheetRefObject = { current: { present: () => void; dismiss: () => void } | null } | null;
|
|
40
|
-
let bottomSheetRef: BottomSheetRefObject = null;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Set the bottom sheet ref so showBottomSheet can control it
|
|
44
|
-
*/
|
|
45
|
-
export const setBottomSheetRef = (ref: BottomSheetRefObject) => {
|
|
46
|
-
bottomSheetRef = ref;
|
|
47
|
-
};
|
|
27
|
+
export const getState = () => bottomSheetStore.getState();
|
|
48
28
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
bottomSheetStore.setState((state) => ({ ...state, ...updates }));
|
|
55
|
-
};
|
|
29
|
+
export const showBottomSheet = (
|
|
30
|
+
screenOrConfig: RouteName | { screen: RouteName; props?: Record<string, unknown> },
|
|
31
|
+
): void => {
|
|
32
|
+
const screen = typeof screenOrConfig === 'string' ? screenOrConfig : screenOrConfig.screen;
|
|
33
|
+
const props = typeof screenOrConfig === 'string' ? {} : (screenOrConfig.props || {});
|
|
56
34
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
export const subscribeToBottomSheetState = (listener: (state: BottomSheetRouterState) => void) => {
|
|
62
|
-
return bottomSheetStore.subscribe(listener);
|
|
63
|
-
};
|
|
35
|
+
if (!isValidRoute(screen)) {
|
|
36
|
+
if (__DEV__) console.warn(`[BottomSheet] Invalid route: ${screen}`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
64
39
|
|
|
65
|
-
|
|
66
|
-
* Get the current bottom sheet state
|
|
67
|
-
* (Wrapper around store.getState for backward compatibility)
|
|
68
|
-
*/
|
|
69
|
-
export const getBottomSheetState = (): BottomSheetRouterState => {
|
|
70
|
-
return bottomSheetStore.getState();
|
|
71
|
-
};
|
|
40
|
+
const state = bottomSheetStore.getState();
|
|
72
41
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const addToHistory = options?.addToHistory !== false; // Default to true
|
|
83
|
-
|
|
84
|
-
// If adding to history and there's a current screen, push it to history
|
|
85
|
-
if (addToHistory && currentState.currentScreen) {
|
|
86
|
-
const historyEntry: NavigationHistoryEntry = {
|
|
87
|
-
screen: currentState.currentScreen,
|
|
88
|
-
props: { ...currentState.screenProps },
|
|
89
|
-
step: currentState.currentStep,
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
// We need to create a new array for immutability
|
|
93
|
-
const newHistory = [...currentState.navigationHistory, historyEntry];
|
|
94
|
-
bottomSheetStore.setState({ navigationHistory: newHistory });
|
|
42
|
+
// Push current screen to history if navigating to different screen
|
|
43
|
+
if (state.currentScreen && state.currentScreen !== screen) {
|
|
44
|
+
bottomSheetStore.setState({
|
|
45
|
+
history: [...state.history, {
|
|
46
|
+
screen: state.currentScreen,
|
|
47
|
+
props: state.screenProps,
|
|
48
|
+
step: state.currentStep,
|
|
49
|
+
}],
|
|
50
|
+
});
|
|
95
51
|
}
|
|
96
|
-
|
|
97
|
-
// Determine the new step
|
|
98
|
-
const newStep = options?.step ??
|
|
99
|
-
(props?.initialStep !== undefined ? props.initialStep :
|
|
100
|
-
(addToHistory ? undefined : currentState.currentStep));
|
|
101
|
-
|
|
52
|
+
|
|
102
53
|
bottomSheetStore.setState({
|
|
103
54
|
currentScreen: screen,
|
|
104
|
-
screenProps: props
|
|
105
|
-
currentStep:
|
|
55
|
+
screenProps: props,
|
|
56
|
+
currentStep: typeof props.initialStep === 'number' ? props.initialStep : undefined,
|
|
106
57
|
isOpen: true,
|
|
107
58
|
});
|
|
108
|
-
|
|
109
|
-
// Present the sheet after state update
|
|
110
|
-
if (bottomSheetRef?.current) {
|
|
111
|
-
bottomSheetRef.current.present();
|
|
112
|
-
}
|
|
113
59
|
};
|
|
114
60
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
*/
|
|
118
|
-
export const managerCloseBottomSheet = (): void => {
|
|
119
|
-
bottomSheetStore.setState({
|
|
120
|
-
currentScreen: null,
|
|
121
|
-
screenProps: {},
|
|
122
|
-
currentStep: undefined,
|
|
123
|
-
navigationHistory: [],
|
|
124
|
-
isOpen: false,
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
if (bottomSheetRef?.current) {
|
|
128
|
-
bottomSheetRef.current.dismiss();
|
|
129
|
-
}
|
|
61
|
+
export const closeBottomSheet = (): void => {
|
|
62
|
+
bottomSheetStore.setState(initialState);
|
|
130
63
|
};
|
|
131
64
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
* Returns true if back navigation was successful, false if history is empty
|
|
135
|
-
*/
|
|
136
|
-
export const managerGoBack = (): boolean => {
|
|
137
|
-
const currentState = bottomSheetStore.getState();
|
|
65
|
+
export const goBack = (): boolean => {
|
|
66
|
+
const { history } = bottomSheetStore.getState();
|
|
138
67
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const previous = currentState.navigationHistory[currentState.navigationHistory.length - 1];
|
|
142
|
-
const newHistory = currentState.navigationHistory.slice(0, -1);
|
|
143
|
-
|
|
68
|
+
if (history.length > 0) {
|
|
69
|
+
const prev = history[history.length - 1];
|
|
144
70
|
bottomSheetStore.setState({
|
|
145
|
-
currentScreen:
|
|
146
|
-
screenProps:
|
|
147
|
-
currentStep:
|
|
148
|
-
|
|
149
|
-
isOpen: true,
|
|
71
|
+
currentScreen: prev.screen,
|
|
72
|
+
screenProps: prev.props,
|
|
73
|
+
currentStep: prev.step,
|
|
74
|
+
history: history.slice(0, -1),
|
|
150
75
|
});
|
|
151
|
-
|
|
152
76
|
return true;
|
|
153
77
|
}
|
|
154
|
-
|
|
155
|
-
return false;
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Public API for showing bottom sheets
|
|
160
|
-
*/
|
|
161
|
-
export const showBottomSheet = (
|
|
162
|
-
screenOrConfig: RouteName | { screen: RouteName; props?: Record<string, unknown> },
|
|
163
|
-
): void => {
|
|
164
|
-
let screen: RouteName;
|
|
165
|
-
let props: Record<string, unknown> = {};
|
|
166
|
-
|
|
167
|
-
if (typeof screenOrConfig === 'string') {
|
|
168
|
-
screen = screenOrConfig;
|
|
169
|
-
} else {
|
|
170
|
-
screen = screenOrConfig.screen;
|
|
171
|
-
props = screenOrConfig.props || {};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (!isValidRoute(screen)) {
|
|
175
|
-
if (__DEV__) {
|
|
176
|
-
console.warn(`[BottomSheetAPI] Invalid route: ${screen}`);
|
|
177
|
-
}
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
78
|
|
|
181
|
-
|
|
79
|
+
return false;
|
|
182
80
|
};
|
|
183
81
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
*/
|
|
187
|
-
export const closeBottomSheet = (): void => {
|
|
188
|
-
managerCloseBottomSheet();
|
|
82
|
+
export const updateState = (updates: Partial<BottomSheetState>) => {
|
|
83
|
+
bottomSheetStore.setState((state) => ({ ...state, ...updates }));
|
|
189
84
|
};
|
|
190
|
-
|
|
191
|
-
|
|
@@ -29,7 +29,6 @@ import { useThemeStyles } from '../hooks/useThemeStyles';
|
|
|
29
29
|
import { useColorScheme } from '../hooks/use-color-scheme';
|
|
30
30
|
import { Colors } from '../constants/theme';
|
|
31
31
|
import { normalizeColorScheme, normalizeTheme } from '../utils/themeUtils';
|
|
32
|
-
import { useHapticPress } from '../hooks/use-haptic-press';
|
|
33
32
|
import { EditDisplayNameModal } from '../components/profile/EditDisplayNameModal';
|
|
34
33
|
import { EditUsernameModal } from '../components/profile/EditUsernameModal';
|
|
35
34
|
import { EditEmailModal } from '../components/profile/EditEmailModal';
|
|
@@ -176,7 +175,6 @@ const AccountSettingsScreen: React.FC<BaseScreenProps & { initialField?: string;
|
|
|
176
175
|
// Get theme colors using centralized hook
|
|
177
176
|
const colorScheme = useColorScheme();
|
|
178
177
|
const themeStyles = useThemeStyles(theme || 'light', colorScheme);
|
|
179
|
-
const handlePressIn = useHapticPress();
|
|
180
178
|
|
|
181
179
|
// Extract colors for convenience - ensure it's always defined
|
|
182
180
|
// useThemeStyles always returns colors, but add safety check for edge cases
|
|
@@ -549,8 +549,8 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
549
549
|
});
|
|
550
550
|
|
|
551
551
|
// Attempt to refresh file list incrementally – fetch single file metadata if API allows
|
|
552
|
-
|
|
553
|
-
|
|
552
|
+
const f = result?.file ?? result?.files?.[0];
|
|
553
|
+
if (f) {
|
|
554
554
|
const merged: FileMetadata = {
|
|
555
555
|
id: f.id,
|
|
556
556
|
filename: f.originalName || f.sha256 || raw.name,
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication helper utilities to reduce code duplication across hooks and utilities.
|
|
3
|
+
* These functions handle common token validation and authentication error patterns.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { OxyServices } from '../../core';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Error thrown when session sync is required
|
|
10
|
+
*/
|
|
11
|
+
export class SessionSyncRequiredError extends Error {
|
|
12
|
+
constructor(message = 'Session needs to be synced. Please try again.') {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'SessionSyncRequiredError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Error thrown when authentication fails
|
|
20
|
+
*/
|
|
21
|
+
export class AuthenticationFailedError extends Error {
|
|
22
|
+
constructor(message = 'Authentication failed. Please sign in again.') {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = 'AuthenticationFailedError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Ensures a valid token exists before making authenticated API calls.
|
|
30
|
+
* If no valid token exists and an active session ID is available,
|
|
31
|
+
* attempts to refresh the token using the session.
|
|
32
|
+
*
|
|
33
|
+
* @param oxyServices - The OxyServices instance
|
|
34
|
+
* @param activeSessionId - The active session ID (if available)
|
|
35
|
+
* @throws {SessionSyncRequiredError} If the session needs to be synced (offline session)
|
|
36
|
+
* @throws {Error} If token refresh fails for other reasons
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* // In a mutation or query function:
|
|
41
|
+
* await ensureValidToken(oxyServices, activeSessionId);
|
|
42
|
+
* return await oxyServices.updateProfile(updates);
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export async function ensureValidToken(
|
|
46
|
+
oxyServices: OxyServices,
|
|
47
|
+
activeSessionId: string | null | undefined
|
|
48
|
+
): Promise<void> {
|
|
49
|
+
if (oxyServices.hasValidToken() || !activeSessionId) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
await oxyServices.getTokenBySession(activeSessionId);
|
|
55
|
+
} catch (tokenError) {
|
|
56
|
+
const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError);
|
|
57
|
+
|
|
58
|
+
if (errorMessage.includes('AUTH_REQUIRED_OFFLINE_SESSION') || errorMessage.includes('offline')) {
|
|
59
|
+
throw new SessionSyncRequiredError();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
throw tokenError;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Options for handling API authentication errors
|
|
68
|
+
*/
|
|
69
|
+
export interface HandleApiErrorOptions {
|
|
70
|
+
/** Optional callback to attempt session sync and retry */
|
|
71
|
+
syncSession?: () => Promise<unknown>;
|
|
72
|
+
/** The active session ID for retry attempts */
|
|
73
|
+
activeSessionId?: string | null;
|
|
74
|
+
/** The OxyServices instance for retry attempts */
|
|
75
|
+
oxyServices?: OxyServices;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Checks if an error is an authentication error (401 or auth-related message)
|
|
80
|
+
*
|
|
81
|
+
* @param error - The error to check
|
|
82
|
+
* @returns True if the error is an authentication error
|
|
83
|
+
*/
|
|
84
|
+
export function isAuthenticationError(error: unknown): boolean {
|
|
85
|
+
if (!error || typeof error !== 'object') {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const errorObj = error as { message?: string; status?: number; response?: { status?: number } };
|
|
90
|
+
const errorMessage = errorObj.message || '';
|
|
91
|
+
const status = errorObj.status || errorObj.response?.status;
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
status === 401 ||
|
|
95
|
+
errorMessage.includes('Authentication required') ||
|
|
96
|
+
errorMessage.includes('Invalid or missing authorization header')
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Wraps an API call with authentication error handling.
|
|
102
|
+
* If an authentication error occurs, it can optionally attempt to sync the session and retry.
|
|
103
|
+
*
|
|
104
|
+
* @param apiCall - The API call function to execute
|
|
105
|
+
* @param options - Optional error handling configuration
|
|
106
|
+
* @returns The result of the API call
|
|
107
|
+
* @throws {AuthenticationFailedError} If authentication fails and cannot be recovered
|
|
108
|
+
* @throws {Error} If the API call fails for non-auth reasons
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* // Simple usage:
|
|
113
|
+
* const result = await withAuthErrorHandling(
|
|
114
|
+
* () => oxyServices.updateProfile(updates)
|
|
115
|
+
* );
|
|
116
|
+
*
|
|
117
|
+
* // With retry on auth failure:
|
|
118
|
+
* const result = await withAuthErrorHandling(
|
|
119
|
+
* () => oxyServices.updateProfile(updates),
|
|
120
|
+
* { syncSession, activeSessionId, oxyServices }
|
|
121
|
+
* );
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
export async function withAuthErrorHandling<T>(
|
|
125
|
+
apiCall: () => Promise<T>,
|
|
126
|
+
options?: HandleApiErrorOptions
|
|
127
|
+
): Promise<T> {
|
|
128
|
+
try {
|
|
129
|
+
return await apiCall();
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (!isAuthenticationError(error)) {
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// If we have sync capabilities, try to recover
|
|
136
|
+
if (options?.syncSession && options?.activeSessionId && options?.oxyServices) {
|
|
137
|
+
try {
|
|
138
|
+
await options.syncSession();
|
|
139
|
+
await options.oxyServices.getTokenBySession(options.activeSessionId);
|
|
140
|
+
// Retry the API call after refreshing token
|
|
141
|
+
return await apiCall();
|
|
142
|
+
} catch {
|
|
143
|
+
throw new AuthenticationFailedError();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
throw new AuthenticationFailedError();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Combines token validation and auth error handling for a complete authenticated API call.
|
|
153
|
+
* This is the recommended helper for most authenticated API operations.
|
|
154
|
+
*
|
|
155
|
+
* @param oxyServices - The OxyServices instance
|
|
156
|
+
* @param activeSessionId - The active session ID
|
|
157
|
+
* @param apiCall - The API call function to execute
|
|
158
|
+
* @param syncSession - Optional callback to sync session on auth failure
|
|
159
|
+
* @returns The result of the API call
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```ts
|
|
163
|
+
* return await authenticatedApiCall(
|
|
164
|
+
* oxyServices,
|
|
165
|
+
* activeSessionId,
|
|
166
|
+
* () => oxyServices.updateProfile(updates)
|
|
167
|
+
* );
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
export async function authenticatedApiCall<T>(
|
|
171
|
+
oxyServices: OxyServices,
|
|
172
|
+
activeSessionId: string | null | undefined,
|
|
173
|
+
apiCall: () => Promise<T>,
|
|
174
|
+
syncSession?: () => Promise<unknown>
|
|
175
|
+
): Promise<T> {
|
|
176
|
+
await ensureValidToken(oxyServices, activeSessionId);
|
|
177
|
+
|
|
178
|
+
return withAuthErrorHandling(apiCall, {
|
|
179
|
+
syncSession,
|
|
180
|
+
activeSessionId,
|
|
181
|
+
oxyServices,
|
|
182
|
+
});
|
|
183
|
+
}
|