@oxyhq/services 22.12.0 → 22.12.2
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/components/FollowButton.js +5 -8
- package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSurfaceHeader.js +2 -2
- package/lib/commonjs/ui/hooks/useSurfaceHeader.js.map +1 -1
- package/lib/commonjs/ui/navigation/surfaceNavStack.js +8 -7
- package/lib/commonjs/ui/navigation/surfaceNavStack.js.map +1 -1
- package/lib/commonjs/ui/screens/ChangeAvatarScreen.js +48 -42
- package/lib/commonjs/ui/screens/ChangeAvatarScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/CreateAccountScreen.js +55 -98
- package/lib/commonjs/ui/screens/CreateAccountScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/FileManagementScreen.js +32 -13
- package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js +35 -41
- package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
- package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js +14 -6
- package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js.map +1 -1
- package/lib/module/ui/components/FollowButton.js +5 -8
- package/lib/module/ui/components/FollowButton.js.map +1 -1
- package/lib/module/ui/hooks/useSurfaceHeader.js +2 -2
- package/lib/module/ui/hooks/useSurfaceHeader.js.map +1 -1
- package/lib/module/ui/navigation/surfaceNavStack.js +8 -7
- package/lib/module/ui/navigation/surfaceNavStack.js.map +1 -1
- package/lib/module/ui/screens/ChangeAvatarScreen.js +48 -42
- package/lib/module/ui/screens/ChangeAvatarScreen.js.map +1 -1
- package/lib/module/ui/screens/CreateAccountScreen.js +58 -101
- package/lib/module/ui/screens/CreateAccountScreen.js.map +1 -1
- package/lib/module/ui/screens/FileManagementScreen.js +32 -13
- package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js +35 -41
- package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
- package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js +14 -6
- package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js.map +1 -1
- package/lib/typescript/commonjs/ui/components/FollowButton.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useSurfaceHeader.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/navigation/surfaceNavStack.d.ts +7 -4
- package/lib/typescript/commonjs/ui/navigation/surfaceNavStack.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/ChangeAvatarScreen.d.ts +9 -5
- package/lib/typescript/commonjs/ui/screens/ChangeAvatarScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/CreateAccountScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts +1 -1
- package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts +3 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/types/fileManagement.d.ts +10 -0
- package/lib/typescript/commonjs/ui/types/fileManagement.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/FollowButton.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useSurfaceHeader.d.ts.map +1 -1
- package/lib/typescript/module/ui/navigation/surfaceNavStack.d.ts +7 -4
- package/lib/typescript/module/ui/navigation/surfaceNavStack.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/ChangeAvatarScreen.d.ts +9 -5
- package/lib/typescript/module/ui/screens/ChangeAvatarScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/CreateAccountScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts +1 -1
- package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts +3 -1
- package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
- package/lib/typescript/module/ui/types/fileManagement.d.ts +10 -0
- package/lib/typescript/module/ui/types/fileManagement.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/ui/components/FollowButton.tsx +5 -8
- package/src/ui/hooks/useSurfaceHeader.ts +2 -17
- package/src/ui/navigation/__tests__/surfaceNavStack.test.ts +49 -17
- package/src/ui/navigation/surfaceNavStack.ts +15 -11
- package/src/ui/screens/ChangeAvatarScreen.tsx +46 -39
- package/src/ui/screens/CreateAccountScreen.tsx +123 -167
- package/src/ui/screens/FileManagementScreen.tsx +28 -12
- package/src/ui/screens/fileManagement/PhotoPickerSection.tsx +35 -44
- package/src/ui/screens/fileManagement/hooks/useFileUploadState.ts +16 -6
- package/src/ui/types/fileManagement.ts +10 -0
|
@@ -4,11 +4,15 @@
|
|
|
4
4
|
* Shows the current avatar large, then the four sources the user can pick from:
|
|
5
5
|
* device gallery, camera, their existing Oxy files, and removing the current
|
|
6
6
|
* photo. Every source that yields an image NAVIGATES WITHIN THIS SURFACE to
|
|
7
|
-
* `AvatarCrop` (never presents it on top), so the panel morphs from
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* `AvatarCrop` (never presents it on top), so the panel morphs from frame to
|
|
8
|
+
* frame instead of stacking sheets. The device / camera sources navigate straight
|
|
9
|
+
* to the cropper; the "My Oxy files" source first navigates to the `FileManagement`
|
|
10
|
+
* picker frame, which then forward-navigates to the cropper on selection — a real
|
|
11
|
+
* three-frame stack (this list → picker → cropper). So Cancel on the cropper
|
|
12
|
+
* returns to whichever frame opened it (the picker when re-picking from Oxy files,
|
|
13
|
+
* this list otherwise), and Cancel on the picker returns here. The forward morph
|
|
14
|
+
* is flash-free because only the top frame renders and the cropper does all its
|
|
15
|
+
* own async work (URL resolution + measurement), so no frame is left on screen
|
|
12
16
|
* waiting.
|
|
13
17
|
*
|
|
14
18
|
* The surface resolves with an {@link AvatarCropResult} (the crop confirmed) or
|
|
@@ -37,10 +41,10 @@ import { surfaces as bloomSurfaces } from '@oxyhq/bloom/surfaces';
|
|
|
37
41
|
import { useTheme } from '@oxyhq/bloom/theme';
|
|
38
42
|
import { toast } from '@oxyhq/bloom';
|
|
39
43
|
import { getAccountDisplayName, logger } from '@oxyhq/core';
|
|
44
|
+
import type { FileMetadata } from '@oxyhq/core';
|
|
40
45
|
import { useOxy } from '../context/OxyContext';
|
|
41
46
|
import { useI18n } from '../hooks/useI18n';
|
|
42
47
|
import { useSurfaceHeader } from '../hooks/useSurfaceHeader';
|
|
43
|
-
import { openWithinOrPresent } from '../navigation/surfaces';
|
|
44
48
|
import { SettingsIcon } from '../components/SettingsIcon';
|
|
45
49
|
import type { BaseScreenProps } from '../types/navigation';
|
|
46
50
|
|
|
@@ -229,44 +233,47 @@ const ChangeAvatarScreen: React.FC<BaseScreenProps> = ({ navigate, dismiss }) =>
|
|
|
229
233
|
);
|
|
230
234
|
|
|
231
235
|
/**
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
+
* Forward the file the user picked from their Oxy library on to the crop
|
|
237
|
+
* editor. Given to the `FileManagement` picker as its `onPicked` handler, so
|
|
238
|
+
* SELECTING a file pushes the cropper ON TOP of the picker frame — the picker
|
|
239
|
+
* stays below, and Cancel on the cropper returns to it to re-pick.
|
|
240
|
+
*
|
|
241
|
+
* The file's ID is handed straight to the cropper WITHOUT resolving its
|
|
242
|
+
* (usually private) download URL here: the cropper resolves it and shows its
|
|
243
|
+
* own loading state on the dark canvas, so the picker never lingers for the
|
|
244
|
+
* network round-trip (the "flash back to the avatar screen" the resolve-here
|
|
245
|
+
* approach caused).
|
|
236
246
|
*/
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const file = await openWithinOrPresent('FileManagement', {
|
|
242
|
-
selectMode: true,
|
|
243
|
-
multiSelect: false,
|
|
244
|
-
disabledMimeTypes: NON_IMAGE_MIME_TYPES,
|
|
245
|
-
});
|
|
246
|
-
if (!file) return;
|
|
247
|
-
|
|
247
|
+
const handleOxyFilePicked = useCallback(
|
|
248
|
+
(file: FileMetadata) => {
|
|
249
|
+
// The image-only picker never surfaces non-images, but guard
|
|
250
|
+
// defensively — a non-image would break the cropper.
|
|
248
251
|
if (!file.contentType?.startsWith('image/')) {
|
|
249
252
|
toast.error(t('editProfile.toasts.selectImage'));
|
|
250
253
|
return;
|
|
251
254
|
}
|
|
252
|
-
|
|
253
|
-
// Hand the file's ID straight to the cropper WITHOUT resolving its
|
|
254
|
-
// (usually private) download URL here. The cropper resolves it — and
|
|
255
|
-
// shows its own loading state on the dark canvas while it does — so
|
|
256
|
-
// this source list is never left on screen for the network round-trip
|
|
257
|
-
// that resolution takes (the "goes back to the avatar screen" flash).
|
|
258
255
|
goToCrop({ imageFileId: file.id });
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
256
|
+
},
|
|
257
|
+
[goToCrop, t],
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Pick from the files the user already has on Oxy. Navigates WITHIN this
|
|
262
|
+
* surface to the image-only `FileManagement` picker as a real frame (not a
|
|
263
|
+
* resolve-and-pop sub-flow): selecting a file forward-navigates to the crop
|
|
264
|
+
* editor via {@link handleOxyFilePicked}, giving a three-frame stack
|
|
265
|
+
* (ChangeAvatar → picker → crop). Back from the cropper returns to the picker
|
|
266
|
+
* to re-pick; back from the picker returns here. The cropper's confirm
|
|
267
|
+
* resolves the whole avatar flow with the crop result.
|
|
268
|
+
*/
|
|
269
|
+
const pickFromOxyFiles = useCallback(() => {
|
|
270
|
+
navigate?.('FileManagement', {
|
|
271
|
+
selectMode: true,
|
|
272
|
+
multiSelect: false,
|
|
273
|
+
disabledMimeTypes: NON_IMAGE_MIME_TYPES,
|
|
274
|
+
onPicked: handleOxyFilePicked,
|
|
275
|
+
});
|
|
276
|
+
}, [navigate, handleOxyFilePicked]);
|
|
270
277
|
|
|
271
278
|
/** Confirm, then resolve the surface with the removal so the caller writes it. */
|
|
272
279
|
const removeCurrentPhoto = useCallback(async () => {
|
|
@@ -341,7 +348,7 @@ const ChangeAvatarScreen: React.FC<BaseScreenProps> = ({ navigate, dismiss }) =>
|
|
|
341
348
|
title={t('changeAvatar.sources.files.title')}
|
|
342
349
|
description={t('changeAvatar.sources.files.description')}
|
|
343
350
|
disabled={busy}
|
|
344
|
-
onPress={() =>
|
|
351
|
+
onPress={() => pickFromOxyFiles()}
|
|
345
352
|
/>
|
|
346
353
|
<SettingsListItem
|
|
347
354
|
icon={<SettingsIcon name="trash-can-outline" color={bloomTheme.colors.error} />}
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
|
-
import { useState, useCallback, useRef
|
|
3
|
-
import {
|
|
4
|
-
View,
|
|
5
|
-
ActivityIndicator,
|
|
6
|
-
TouchableOpacity,
|
|
7
|
-
} from 'react-native';
|
|
2
|
+
import { useState, useCallback, useRef } from 'react';
|
|
3
|
+
import { View, ActivityIndicator } from 'react-native';
|
|
8
4
|
import { Ionicons } from '@expo/vector-icons';
|
|
9
5
|
import type { AccountKind, CreateAccountInput, OrganizationCategory } from '@oxyhq/core';
|
|
10
6
|
import { ORGANIZATION_CATEGORIES } from '@oxyhq/core';
|
|
@@ -15,6 +11,7 @@ import { useTheme } from '@oxyhq/bloom/theme';
|
|
|
15
11
|
import { Text } from '@oxyhq/bloom/typography';
|
|
16
12
|
import { Button } from '@oxyhq/bloom/button';
|
|
17
13
|
import { TextField, TextFieldInput } from '@oxyhq/bloom/text-field';
|
|
14
|
+
import { SettingsListGroup, SettingsListItem } from '@oxyhq/bloom/settings-list';
|
|
18
15
|
import { useOxy } from '../context/OxyContext';
|
|
19
16
|
import { toast } from '@oxyhq/bloom';
|
|
20
17
|
|
|
@@ -172,15 +169,6 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({
|
|
|
172
169
|
checkUsername(cleaned);
|
|
173
170
|
}, [checkUsername]);
|
|
174
171
|
|
|
175
|
-
// Cleanup debounce timer
|
|
176
|
-
useEffect(() => {
|
|
177
|
-
return () => {
|
|
178
|
-
if (debounceTimerRef.current) {
|
|
179
|
-
clearTimeout(debounceTimerRef.current);
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
}, []);
|
|
183
|
-
|
|
184
172
|
const canCreate = usernameStatus === 'available' && displayName.trim().length > 0 && !isCreating;
|
|
185
173
|
|
|
186
174
|
const handleCreate = useCallback(async () => {
|
|
@@ -234,168 +222,136 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({
|
|
|
234
222
|
const title = t('accounts.create.title') || 'Create account';
|
|
235
223
|
|
|
236
224
|
return (
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
225
|
+
<View className="px-screen-margin pt-space-16 pb-space-32">
|
|
226
|
+
|
|
227
|
+
{/* Account type — canonical grouped selection rows (checkmark on the chosen one) */}
|
|
228
|
+
<SettingsListGroup title={t('accounts.create.typeSection') || 'Account type'}>
|
|
229
|
+
{KIND_OPTIONS.map((option) => {
|
|
230
|
+
const selected = option.value === kind;
|
|
231
|
+
return (
|
|
232
|
+
<SettingsListItem
|
|
233
|
+
key={option.value}
|
|
234
|
+
icon={(
|
|
235
|
+
<Ionicons
|
|
236
|
+
name={option.icon}
|
|
237
|
+
size={22}
|
|
238
|
+
color={selected ? bloomTheme.colors.primary : bloomTheme.colors.icon}
|
|
239
|
+
/>
|
|
240
|
+
)}
|
|
241
|
+
title={kindLabel(t, option.value)}
|
|
242
|
+
description={kindDescription(t, option.value)}
|
|
243
|
+
onPress={() => setKind(option.value)}
|
|
244
|
+
showChevron={false}
|
|
245
|
+
rightElement={selected ? (
|
|
246
|
+
<Ionicons name="checkmark-circle" size={20} color={bloomTheme.colors.primary} />
|
|
247
|
+
) : undefined}
|
|
248
|
+
accessibilityLabel={kindLabel(t, option.value)}
|
|
249
|
+
/>
|
|
250
|
+
);
|
|
251
|
+
})}
|
|
252
|
+
</SettingsListGroup>
|
|
253
|
+
|
|
254
|
+
{/* Organization category — grouped selection rows, shown only for organizations */}
|
|
255
|
+
{kind === 'organization' ? (
|
|
256
|
+
<SettingsListGroup title={t('accounts.create.organizationCategory.label')}>
|
|
257
|
+
{ORGANIZATION_CATEGORY_OPTIONS.map((option) => {
|
|
258
|
+
const selected = option === organizationCategory;
|
|
259
|
+
return (
|
|
260
|
+
<SettingsListItem
|
|
261
|
+
key={option}
|
|
262
|
+
title={organizationCategoryLabel(t, option)}
|
|
263
|
+
onPress={() => setOrganizationCategory(option)}
|
|
264
|
+
showChevron={false}
|
|
265
|
+
rightElement={selected ? (
|
|
266
|
+
<Ionicons name="checkmark-circle" size={20} color={bloomTheme.colors.primary} />
|
|
267
|
+
) : undefined}
|
|
268
|
+
accessibilityLabel={organizationCategoryLabel(t, option)}
|
|
269
|
+
/>
|
|
270
|
+
);
|
|
271
|
+
})}
|
|
272
|
+
</SettingsListGroup>
|
|
273
|
+
) : null}
|
|
274
|
+
|
|
275
|
+
{/* Details — a grouped section card hosting the form fields */}
|
|
276
|
+
<SettingsListGroup title={t('accounts.create.detailsSection') || 'Details'}>
|
|
277
|
+
<View className="p-space-16 gap-space-16">
|
|
278
|
+
{/* Username */}
|
|
242
279
|
<View className="gap-space-8">
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
<Text className="text-caption font-caption text-text-secondary">
|
|
273
|
-
{kindDescription(t, option.value)}
|
|
274
|
-
</Text>
|
|
275
|
-
</View>
|
|
276
|
-
{selected ? (
|
|
277
|
-
<Ionicons name="checkmark-circle" size={20} color={bloomTheme.colors.primary} />
|
|
278
|
-
) : null}
|
|
279
|
-
</TouchableOpacity>
|
|
280
|
-
);
|
|
281
|
-
})}
|
|
280
|
+
<TextField isInvalid={usernameIsInvalid}>
|
|
281
|
+
<TextFieldInput
|
|
282
|
+
floatingLabel
|
|
283
|
+
label={t('accounts.create.username.label') || 'Username'}
|
|
284
|
+
value={username}
|
|
285
|
+
onChangeText={handleUsernameChange}
|
|
286
|
+
isInvalid={usernameIsInvalid}
|
|
287
|
+
autoCapitalize="none"
|
|
288
|
+
autoCorrect={false}
|
|
289
|
+
autoComplete="off"
|
|
290
|
+
maxLength={USERNAME_MAX}
|
|
291
|
+
/>
|
|
292
|
+
</TextField>
|
|
293
|
+
{(usernameStatus === 'checking' || usernameMessage) ? (
|
|
294
|
+
<View className="flex-row items-center gap-space-4 px-space-4">
|
|
295
|
+
{usernameStatus === 'checking' ? (
|
|
296
|
+
<ActivityIndicator size="small" color={bloomTheme.colors.primary} />
|
|
297
|
+
) : usernameStatus === 'available' ? (
|
|
298
|
+
<Ionicons name="checkmark-circle" size={16} color={bloomTheme.colors.success} />
|
|
299
|
+
) : usernameIsInvalid ? (
|
|
300
|
+
<Ionicons name="alert-circle" size={16} color={bloomTheme.colors.negative} />
|
|
301
|
+
) : null}
|
|
302
|
+
{usernameMessage ? (
|
|
303
|
+
<Text className="text-caption font-caption" style={{ color: statusColor }}>
|
|
304
|
+
{usernameMessage}
|
|
305
|
+
</Text>
|
|
306
|
+
) : null}
|
|
307
|
+
</View>
|
|
308
|
+
) : null}
|
|
282
309
|
</View>
|
|
283
310
|
|
|
284
|
-
{
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
accessibilityLabel={organizationCategoryLabel(t, option)}
|
|
298
|
-
onPress={() => setOrganizationCategory(option)}
|
|
299
|
-
activeOpacity={0.7}
|
|
300
|
-
className="px-space-12 py-space-8 rounded-radius-full"
|
|
301
|
-
style={{
|
|
302
|
-
backgroundColor: selected ? bloomTheme.colors.primarySubtle : bloomTheme.colors.card,
|
|
303
|
-
borderWidth: 1,
|
|
304
|
-
borderColor: selected ? bloomTheme.colors.primary : bloomTheme.colors.border,
|
|
305
|
-
}}
|
|
306
|
-
>
|
|
307
|
-
<Text
|
|
308
|
-
className="text-caption font-captionBold"
|
|
309
|
-
style={{ color: selected ? bloomTheme.colors.primary : bloomTheme.colors.text }}
|
|
310
|
-
>
|
|
311
|
-
{organizationCategoryLabel(t, option)}
|
|
312
|
-
</Text>
|
|
313
|
-
</TouchableOpacity>
|
|
314
|
-
);
|
|
315
|
-
})}
|
|
316
|
-
</View>
|
|
317
|
-
</View>
|
|
318
|
-
) : null}
|
|
319
|
-
|
|
320
|
-
{/* Form Content */}
|
|
321
|
-
<View className="gap-space-16 p-space-16 rounded-radius-20 bg-fill">
|
|
322
|
-
{/* Username */}
|
|
323
|
-
<View className="gap-space-8">
|
|
324
|
-
<TextField isInvalid={usernameIsInvalid}>
|
|
325
|
-
<TextFieldInput
|
|
326
|
-
floatingLabel
|
|
327
|
-
label={t('accounts.create.username.label') || 'Username'}
|
|
328
|
-
value={username}
|
|
329
|
-
onChangeText={handleUsernameChange}
|
|
330
|
-
isInvalid={usernameIsInvalid}
|
|
331
|
-
autoCapitalize="none"
|
|
332
|
-
autoCorrect={false}
|
|
333
|
-
autoComplete="off"
|
|
334
|
-
maxLength={USERNAME_MAX}
|
|
335
|
-
/>
|
|
336
|
-
</TextField>
|
|
337
|
-
{(usernameStatus === 'checking' || usernameMessage) ? (
|
|
338
|
-
<View className="flex-row items-center gap-space-4 px-space-4">
|
|
339
|
-
{usernameStatus === 'checking' ? (
|
|
340
|
-
<ActivityIndicator size="small" color={bloomTheme.colors.primary} />
|
|
341
|
-
) : usernameStatus === 'available' ? (
|
|
342
|
-
<Ionicons name="checkmark-circle" size={16} color={bloomTheme.colors.success} />
|
|
343
|
-
) : usernameIsInvalid ? (
|
|
344
|
-
<Ionicons name="alert-circle" size={16} color={bloomTheme.colors.negative} />
|
|
345
|
-
) : null}
|
|
346
|
-
{usernameMessage ? (
|
|
347
|
-
<Text className="text-caption font-caption" style={{ color: statusColor }}>
|
|
348
|
-
{usernameMessage}
|
|
349
|
-
</Text>
|
|
350
|
-
) : null}
|
|
351
|
-
</View>
|
|
352
|
-
) : null}
|
|
353
|
-
</View>
|
|
354
|
-
|
|
355
|
-
{/* Display Name */}
|
|
311
|
+
{/* Display Name */}
|
|
312
|
+
<TextField>
|
|
313
|
+
<TextFieldInput
|
|
314
|
+
floatingLabel
|
|
315
|
+
label={t('accounts.create.displayName.label') || 'Display name'}
|
|
316
|
+
value={displayName}
|
|
317
|
+
onChangeText={setDisplayName}
|
|
318
|
+
maxLength={DISPLAY_NAME_MAX}
|
|
319
|
+
/>
|
|
320
|
+
</TextField>
|
|
321
|
+
|
|
322
|
+
{/* Bio */}
|
|
323
|
+
<View className="gap-space-4">
|
|
356
324
|
<TextField>
|
|
357
325
|
<TextFieldInput
|
|
358
326
|
floatingLabel
|
|
359
|
-
label={t('accounts.create.
|
|
360
|
-
value={
|
|
361
|
-
onChangeText={
|
|
362
|
-
maxLength={
|
|
327
|
+
label={t('accounts.create.bio.label') || 'Bio (optional)'}
|
|
328
|
+
value={bio}
|
|
329
|
+
onChangeText={setBio}
|
|
330
|
+
maxLength={BIO_MAX}
|
|
331
|
+
multiline
|
|
332
|
+
numberOfLines={3}
|
|
333
|
+
textAlignVertical="top"
|
|
363
334
|
/>
|
|
364
335
|
</TextField>
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
<TextField>
|
|
369
|
-
<TextFieldInput
|
|
370
|
-
floatingLabel
|
|
371
|
-
label={t('accounts.create.bio.label') || 'Bio (optional)'}
|
|
372
|
-
value={bio}
|
|
373
|
-
onChangeText={setBio}
|
|
374
|
-
maxLength={BIO_MAX}
|
|
375
|
-
multiline
|
|
376
|
-
numberOfLines={3}
|
|
377
|
-
textAlignVertical="top"
|
|
378
|
-
/>
|
|
379
|
-
</TextField>
|
|
380
|
-
<Text className="text-caption font-caption text-text-tertiary px-space-4 text-right">
|
|
381
|
-
{bio.length}/{BIO_MAX}
|
|
382
|
-
</Text>
|
|
383
|
-
</View>
|
|
336
|
+
<Text className="text-caption font-caption text-text-tertiary px-space-4 text-right">
|
|
337
|
+
{bio.length}/{BIO_MAX}
|
|
338
|
+
</Text>
|
|
384
339
|
</View>
|
|
385
|
-
|
|
386
|
-
{/* Create Button */}
|
|
387
|
-
<Button
|
|
388
|
-
variant="primary"
|
|
389
|
-
onPress={handleCreate}
|
|
390
|
-
disabled={!canCreate}
|
|
391
|
-
loading={isCreating}
|
|
392
|
-
accessibilityLabel={title}
|
|
393
|
-
className="w-full"
|
|
394
|
-
>
|
|
395
|
-
{title}
|
|
396
|
-
</Button>
|
|
397
340
|
</View>
|
|
398
|
-
|
|
341
|
+
</SettingsListGroup>
|
|
342
|
+
|
|
343
|
+
{/* Create Button */}
|
|
344
|
+
<Button
|
|
345
|
+
variant="primary"
|
|
346
|
+
onPress={handleCreate}
|
|
347
|
+
disabled={!canCreate}
|
|
348
|
+
loading={isCreating}
|
|
349
|
+
accessibilityLabel={title}
|
|
350
|
+
className="w-full"
|
|
351
|
+
>
|
|
352
|
+
{title}
|
|
353
|
+
</Button>
|
|
354
|
+
</View>
|
|
399
355
|
);
|
|
400
356
|
};
|
|
401
357
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { useState, useCallback, useMemo, useRef } from 'react';
|
|
2
3
|
import {
|
|
3
4
|
View,
|
|
4
5
|
Text,
|
|
@@ -87,6 +88,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
87
88
|
selectMode = false,
|
|
88
89
|
multiSelect = false,
|
|
89
90
|
onSelect,
|
|
91
|
+
onPicked,
|
|
90
92
|
onConfirmSelection,
|
|
91
93
|
initialSelectedIds = [],
|
|
92
94
|
maxSelection,
|
|
@@ -304,7 +306,13 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
304
306
|
}
|
|
305
307
|
|
|
306
308
|
if (!multiSelect) {
|
|
307
|
-
if (
|
|
309
|
+
if (onPicked) {
|
|
310
|
+
// Forward-navigation picker (e.g. the avatar flow): hand the file
|
|
311
|
+
// to the caller, which pushes the NEXT frame (the cropper) on top
|
|
312
|
+
// of this picker. The picker frame stays below so Back returns here
|
|
313
|
+
// to re-pick — no dismiss, no session close.
|
|
314
|
+
onPicked(file);
|
|
315
|
+
} else if (onSelect) {
|
|
308
316
|
// Legacy callback caller (inbox, WelcomeNewUser): keep the
|
|
309
317
|
// callback + afterSelect behaviour unchanged.
|
|
310
318
|
onSelect(file);
|
|
@@ -314,8 +322,8 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
314
322
|
onClose?.();
|
|
315
323
|
}
|
|
316
324
|
} else {
|
|
317
|
-
// Promise-based picker
|
|
318
|
-
//
|
|
325
|
+
// Promise-based picker: resolve the surface's present() promise
|
|
326
|
+
// with the picked file.
|
|
319
327
|
dismiss?.(file);
|
|
320
328
|
}
|
|
321
329
|
return;
|
|
@@ -335,7 +343,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
335
343
|
return next;
|
|
336
344
|
});
|
|
337
345
|
scrollToSelectedFile(file.id);
|
|
338
|
-
}, [multiSelect, onSelect, onClose, goBack, dismiss, disabledMimeTypes, maxSelection, afterSelect, defaultVisibility, oxyServices, linkContext, t, scrollToSelectedFile]);
|
|
346
|
+
}, [multiSelect, onPicked, onSelect, onClose, goBack, dismiss, disabledMimeTypes, maxSelection, afterSelect, defaultVisibility, oxyServices, linkContext, t, scrollToSelectedFile]);
|
|
339
347
|
|
|
340
348
|
const confirmMultiSelection = useCallback(async () => {
|
|
341
349
|
if (!selectMode || !multiSelect) return;
|
|
@@ -426,6 +434,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
426
434
|
multiSelect,
|
|
427
435
|
afterSelect,
|
|
428
436
|
onSelect,
|
|
437
|
+
onPicked,
|
|
429
438
|
goBack,
|
|
430
439
|
onClose,
|
|
431
440
|
selectedIds,
|
|
@@ -998,16 +1007,21 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
998
1007
|
const isOwner = user?.id === targetUserId;
|
|
999
1008
|
const allowUpload = isOwner && allowUploadInSelectMode;
|
|
1000
1009
|
|
|
1001
|
-
// Cancel = the nav bar's back affordance.
|
|
1002
|
-
//
|
|
1010
|
+
// Cancel = the nav bar's back affordance. A forward-nav picker (`onPicked`,
|
|
1011
|
+
// e.g. the avatar flow) pops its own frame; legacy callback callers keep their
|
|
1012
|
+
// close/back behaviour; a promise-based picker dismisses its surface.
|
|
1003
1013
|
const pickerCancel = useCallback(() => {
|
|
1004
|
-
if (
|
|
1014
|
+
if (onPicked) {
|
|
1015
|
+
// Forward-nav picker: Cancel/back pops THIS frame, returning to the
|
|
1016
|
+
// frame that opened the picker — never closes the whole session.
|
|
1017
|
+
goBack?.();
|
|
1018
|
+
} else if (onSelect || onConfirmSelection) {
|
|
1005
1019
|
if (onClose) onClose();
|
|
1006
1020
|
else goBack?.();
|
|
1007
1021
|
} else {
|
|
1008
1022
|
dismiss?.();
|
|
1009
1023
|
}
|
|
1010
|
-
}, [onSelect, onConfirmSelection, onClose, goBack, dismiss]);
|
|
1024
|
+
}, [onPicked, onSelect, onConfirmSelection, onClose, goBack, dismiss]);
|
|
1011
1025
|
|
|
1012
1026
|
// The picker's ONE trailing CTA, declared through the Dialog header's
|
|
1013
1027
|
// `primaryAction` (a proper Bloom Button with loading/disabled) — the
|
|
@@ -1100,12 +1114,14 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
1100
1114
|
<Skeleton.Box width="100%" height={44} borderRadius={22} />
|
|
1101
1115
|
</View>
|
|
1102
1116
|
|
|
1103
|
-
{/* Photo grid */}
|
|
1117
|
+
{/* Photo grid — static skeleton placeholders; index keys are stable */}
|
|
1104
1118
|
<View style={{ padding: GRID_PADDING, gap: TILE_GAP }}>
|
|
1105
1119
|
{Array.from({ length: GRID_ROWS }, (_, row) => (
|
|
1106
|
-
|
|
1120
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: fixed-size skeleton grid, order never changes
|
|
1121
|
+
<Skeleton.Row key={`skeleton-row-${row}`} style={{ gap: TILE_GAP }}>
|
|
1107
1122
|
{Array.from({ length: GRID_COLUMNS }, (_, col) => (
|
|
1108
|
-
|
|
1123
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: fixed-size skeleton grid, order never changes
|
|
1124
|
+
<Skeleton.Box key={`skeleton-${row}-${col}`} width={tileSize} height={tileSize} borderRadius={6} />
|
|
1109
1125
|
))}
|
|
1110
1126
|
</Skeleton.Row>
|
|
1111
1127
|
))}
|