@oxyhq/services 22.12.1 → 22.12.3
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 +44 -7
- 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/navigation/surfaces.js +8 -3
- package/lib/commonjs/ui/navigation/surfaces.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 +60 -98
- package/lib/commonjs/ui/screens/CreateAccountScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/FileManagementScreen.js +22 -8
- package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js +10 -7
- package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
- package/lib/commonjs/ui/screens/fileManagement/UploadBar.js +19 -11
- package/lib/commonjs/ui/screens/fileManagement/UploadBar.js.map +1 -1
- package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js +42 -9
- 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 +45 -8
- 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/navigation/surfaces.js +7 -1
- package/lib/module/ui/navigation/surfaces.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 +62 -100
- package/lib/module/ui/screens/CreateAccountScreen.js.map +1 -1
- package/lib/module/ui/screens/FileManagementScreen.js +22 -8
- package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js +10 -7
- package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
- package/lib/module/ui/screens/fileManagement/UploadBar.js +20 -12
- package/lib/module/ui/screens/fileManagement/UploadBar.js.map +1 -1
- package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js +43 -10
- 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 +7 -0
- 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/navigation/surfaces.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.map +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/UploadBar.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts +5 -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 +7 -0
- 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/navigation/surfaces.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.map +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/UploadBar.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts +5 -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 +65 -8
- package/src/ui/navigation/__tests__/surfaceNavStack.test.ts +49 -17
- package/src/ui/navigation/surfaceNavStack.ts +15 -11
- package/src/ui/navigation/surfaces.ts +9 -1
- package/src/ui/screens/ChangeAvatarScreen.tsx +46 -39
- package/src/ui/screens/CreateAccountScreen.tsx +128 -166
- package/src/ui/screens/FileManagementScreen.tsx +22 -8
- package/src/ui/screens/fileManagement/PhotoPickerSection.tsx +4 -5
- package/src/ui/screens/fileManagement/UploadBar.tsx +17 -10
- package/src/ui/screens/fileManagement/hooks/useFileUploadState.ts +51 -10
- 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
2
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
3
|
-
import {
|
|
4
|
-
View,
|
|
5
|
-
ActivityIndicator,
|
|
6
|
-
TouchableOpacity,
|
|
7
|
-
} from 'react-native';
|
|
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
|
|
|
@@ -122,6 +119,12 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({
|
|
|
122
119
|
|
|
123
120
|
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
124
121
|
|
|
122
|
+
useEffect(() => () => {
|
|
123
|
+
if (debounceTimerRef.current) {
|
|
124
|
+
clearTimeout(debounceTimerRef.current);
|
|
125
|
+
}
|
|
126
|
+
}, []);
|
|
127
|
+
|
|
125
128
|
// Debounced username availability check
|
|
126
129
|
const checkUsername = useCallback((value: string) => {
|
|
127
130
|
if (debounceTimerRef.current) {
|
|
@@ -172,15 +175,6 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({
|
|
|
172
175
|
checkUsername(cleaned);
|
|
173
176
|
}, [checkUsername]);
|
|
174
177
|
|
|
175
|
-
// Cleanup debounce timer
|
|
176
|
-
useEffect(() => {
|
|
177
|
-
return () => {
|
|
178
|
-
if (debounceTimerRef.current) {
|
|
179
|
-
clearTimeout(debounceTimerRef.current);
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
}, []);
|
|
183
|
-
|
|
184
178
|
const canCreate = usernameStatus === 'available' && displayName.trim().length > 0 && !isCreating;
|
|
185
179
|
|
|
186
180
|
const handleCreate = useCallback(async () => {
|
|
@@ -234,168 +228,136 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({
|
|
|
234
228
|
const title = t('accounts.create.title') || 'Create account';
|
|
235
229
|
|
|
236
230
|
return (
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
231
|
+
<View className="gap-space-16 px-screen-margin pt-space-16 pb-space-32">
|
|
232
|
+
|
|
233
|
+
{/* Account type — canonical grouped selection rows (checkmark on the chosen one) */}
|
|
234
|
+
<SettingsListGroup title={t('accounts.create.typeSection') || 'Account type'}>
|
|
235
|
+
{KIND_OPTIONS.map((option) => {
|
|
236
|
+
const selected = option.value === kind;
|
|
237
|
+
return (
|
|
238
|
+
<SettingsListItem
|
|
239
|
+
key={option.value}
|
|
240
|
+
icon={(
|
|
241
|
+
<Ionicons
|
|
242
|
+
name={option.icon}
|
|
243
|
+
size={22}
|
|
244
|
+
color={selected ? bloomTheme.colors.primary : bloomTheme.colors.icon}
|
|
245
|
+
/>
|
|
246
|
+
)}
|
|
247
|
+
title={kindLabel(t, option.value)}
|
|
248
|
+
description={kindDescription(t, option.value)}
|
|
249
|
+
onPress={() => setKind(option.value)}
|
|
250
|
+
showChevron={false}
|
|
251
|
+
rightElement={selected ? (
|
|
252
|
+
<Ionicons name="checkmark-circle" size={20} color={bloomTheme.colors.primary} />
|
|
253
|
+
) : undefined}
|
|
254
|
+
accessibilityLabel={kindLabel(t, option.value)}
|
|
255
|
+
/>
|
|
256
|
+
);
|
|
257
|
+
})}
|
|
258
|
+
</SettingsListGroup>
|
|
259
|
+
|
|
260
|
+
{/* Organization category — grouped selection rows, shown only for organizations */}
|
|
261
|
+
{kind === 'organization' ? (
|
|
262
|
+
<SettingsListGroup title={t('accounts.create.organizationCategory.label')}>
|
|
263
|
+
{ORGANIZATION_CATEGORY_OPTIONS.map((option) => {
|
|
264
|
+
const selected = option === organizationCategory;
|
|
265
|
+
return (
|
|
266
|
+
<SettingsListItem
|
|
267
|
+
key={option}
|
|
268
|
+
title={organizationCategoryLabel(t, option)}
|
|
269
|
+
onPress={() => setOrganizationCategory(option)}
|
|
270
|
+
showChevron={false}
|
|
271
|
+
rightElement={selected ? (
|
|
272
|
+
<Ionicons name="checkmark-circle" size={20} color={bloomTheme.colors.primary} />
|
|
273
|
+
) : undefined}
|
|
274
|
+
accessibilityLabel={organizationCategoryLabel(t, option)}
|
|
275
|
+
/>
|
|
276
|
+
);
|
|
277
|
+
})}
|
|
278
|
+
</SettingsListGroup>
|
|
279
|
+
) : null}
|
|
280
|
+
|
|
281
|
+
{/* Details — a grouped section card hosting the form fields */}
|
|
282
|
+
<SettingsListGroup title={t('accounts.create.detailsSection') || 'Details'}>
|
|
283
|
+
<View className="p-space-16 gap-space-16">
|
|
284
|
+
{/* Username */}
|
|
242
285
|
<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
|
-
})}
|
|
286
|
+
<TextField isInvalid={usernameIsInvalid}>
|
|
287
|
+
<TextFieldInput
|
|
288
|
+
floatingLabel
|
|
289
|
+
label={t('accounts.create.username.label') || 'Username'}
|
|
290
|
+
value={username}
|
|
291
|
+
onChangeText={handleUsernameChange}
|
|
292
|
+
isInvalid={usernameIsInvalid}
|
|
293
|
+
autoCapitalize="none"
|
|
294
|
+
autoCorrect={false}
|
|
295
|
+
autoComplete="off"
|
|
296
|
+
maxLength={USERNAME_MAX}
|
|
297
|
+
/>
|
|
298
|
+
</TextField>
|
|
299
|
+
{(usernameStatus === 'checking' || usernameMessage) ? (
|
|
300
|
+
<View className="flex-row items-center gap-space-4 px-space-4">
|
|
301
|
+
{usernameStatus === 'checking' ? (
|
|
302
|
+
<ActivityIndicator size="small" color={bloomTheme.colors.primary} />
|
|
303
|
+
) : usernameStatus === 'available' ? (
|
|
304
|
+
<Ionicons name="checkmark-circle" size={16} color={bloomTheme.colors.success} />
|
|
305
|
+
) : usernameIsInvalid ? (
|
|
306
|
+
<Ionicons name="alert-circle" size={16} color={bloomTheme.colors.negative} />
|
|
307
|
+
) : null}
|
|
308
|
+
{usernameMessage ? (
|
|
309
|
+
<Text className="text-caption font-caption" style={{ color: statusColor }}>
|
|
310
|
+
{usernameMessage}
|
|
311
|
+
</Text>
|
|
312
|
+
) : null}
|
|
313
|
+
</View>
|
|
314
|
+
) : null}
|
|
282
315
|
</View>
|
|
283
316
|
|
|
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 */}
|
|
317
|
+
{/* Display Name */}
|
|
318
|
+
<TextField>
|
|
319
|
+
<TextFieldInput
|
|
320
|
+
floatingLabel
|
|
321
|
+
label={t('accounts.create.displayName.label') || 'Display name'}
|
|
322
|
+
value={displayName}
|
|
323
|
+
onChangeText={setDisplayName}
|
|
324
|
+
maxLength={DISPLAY_NAME_MAX}
|
|
325
|
+
/>
|
|
326
|
+
</TextField>
|
|
327
|
+
|
|
328
|
+
{/* Bio */}
|
|
329
|
+
<View className="gap-space-4">
|
|
356
330
|
<TextField>
|
|
357
331
|
<TextFieldInput
|
|
358
332
|
floatingLabel
|
|
359
|
-
label={t('accounts.create.
|
|
360
|
-
value={
|
|
361
|
-
onChangeText={
|
|
362
|
-
maxLength={
|
|
333
|
+
label={t('accounts.create.bio.label') || 'Bio (optional)'}
|
|
334
|
+
value={bio}
|
|
335
|
+
onChangeText={setBio}
|
|
336
|
+
maxLength={BIO_MAX}
|
|
337
|
+
multiline
|
|
338
|
+
numberOfLines={3}
|
|
339
|
+
textAlignVertical="top"
|
|
363
340
|
/>
|
|
364
341
|
</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>
|
|
342
|
+
<Text className="text-caption font-caption text-text-tertiary px-space-4 text-right">
|
|
343
|
+
{bio.length}/{BIO_MAX}
|
|
344
|
+
</Text>
|
|
384
345
|
</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
346
|
</View>
|
|
398
|
-
|
|
347
|
+
</SettingsListGroup>
|
|
348
|
+
|
|
349
|
+
{/* Create Button */}
|
|
350
|
+
<Button
|
|
351
|
+
variant="primary"
|
|
352
|
+
onPress={handleCreate}
|
|
353
|
+
disabled={!canCreate}
|
|
354
|
+
loading={isCreating}
|
|
355
|
+
accessibilityLabel={title}
|
|
356
|
+
className="w-full"
|
|
357
|
+
>
|
|
358
|
+
{title}
|
|
359
|
+
</Button>
|
|
360
|
+
</View>
|
|
399
361
|
);
|
|
400
362
|
};
|
|
401
363
|
|
|
@@ -88,6 +88,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
88
88
|
selectMode = false,
|
|
89
89
|
multiSelect = false,
|
|
90
90
|
onSelect,
|
|
91
|
+
onPicked,
|
|
91
92
|
onConfirmSelection,
|
|
92
93
|
initialSelectedIds = [],
|
|
93
94
|
maxSelection,
|
|
@@ -305,7 +306,13 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
305
306
|
}
|
|
306
307
|
|
|
307
308
|
if (!multiSelect) {
|
|
308
|
-
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) {
|
|
309
316
|
// Legacy callback caller (inbox, WelcomeNewUser): keep the
|
|
310
317
|
// callback + afterSelect behaviour unchanged.
|
|
311
318
|
onSelect(file);
|
|
@@ -315,8 +322,8 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
315
322
|
onClose?.();
|
|
316
323
|
}
|
|
317
324
|
} else {
|
|
318
|
-
// Promise-based picker
|
|
319
|
-
//
|
|
325
|
+
// Promise-based picker: resolve the surface's present() promise
|
|
326
|
+
// with the picked file.
|
|
320
327
|
dismiss?.(file);
|
|
321
328
|
}
|
|
322
329
|
return;
|
|
@@ -336,7 +343,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
336
343
|
return next;
|
|
337
344
|
});
|
|
338
345
|
scrollToSelectedFile(file.id);
|
|
339
|
-
}, [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]);
|
|
340
347
|
|
|
341
348
|
const confirmMultiSelection = useCallback(async () => {
|
|
342
349
|
if (!selectMode || !multiSelect) return;
|
|
@@ -427,8 +434,10 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
427
434
|
multiSelect,
|
|
428
435
|
afterSelect,
|
|
429
436
|
onSelect,
|
|
437
|
+
onPicked,
|
|
430
438
|
goBack,
|
|
431
439
|
onClose,
|
|
440
|
+
imagesOnly: isImageOnlyPicker,
|
|
432
441
|
selectedIds,
|
|
433
442
|
setSelectedIds,
|
|
434
443
|
t,
|
|
@@ -999,16 +1008,21 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
999
1008
|
const isOwner = user?.id === targetUserId;
|
|
1000
1009
|
const allowUpload = isOwner && allowUploadInSelectMode;
|
|
1001
1010
|
|
|
1002
|
-
// Cancel = the nav bar's back affordance.
|
|
1003
|
-
//
|
|
1011
|
+
// Cancel = the nav bar's back affordance. A forward-nav picker (`onPicked`,
|
|
1012
|
+
// e.g. the avatar flow) pops its own frame; legacy callback callers keep their
|
|
1013
|
+
// close/back behaviour; a promise-based picker dismisses its surface.
|
|
1004
1014
|
const pickerCancel = useCallback(() => {
|
|
1005
|
-
if (
|
|
1015
|
+
if (onPicked) {
|
|
1016
|
+
// Forward-nav picker: Cancel/back pops THIS frame, returning to the
|
|
1017
|
+
// frame that opened the picker — never closes the whole session.
|
|
1018
|
+
goBack?.();
|
|
1019
|
+
} else if (onSelect || onConfirmSelection) {
|
|
1006
1020
|
if (onClose) onClose();
|
|
1007
1021
|
else goBack?.();
|
|
1008
1022
|
} else {
|
|
1009
1023
|
dismiss?.();
|
|
1010
1024
|
}
|
|
1011
|
-
}, [onSelect, onConfirmSelection, onClose, goBack, dismiss]);
|
|
1025
|
+
}, [onPicked, onSelect, onConfirmSelection, onClose, goBack, dismiss]);
|
|
1012
1026
|
|
|
1013
1027
|
// The picker's ONE trailing CTA, declared through the Dialog header's
|
|
1014
1028
|
// `primaryAction` (a proper Bloom Button with loading/disabled) — the
|
|
@@ -305,7 +305,6 @@ const PhotoPickerCell = React.memo(function PhotoPickerCell(props: PhotoPickerCe
|
|
|
305
305
|
{isSelected && (
|
|
306
306
|
<Animated.View
|
|
307
307
|
ref={startRingPulse}
|
|
308
|
-
pointerEvents="none"
|
|
309
308
|
style={{
|
|
310
309
|
position: 'absolute',
|
|
311
310
|
top: 0,
|
|
@@ -316,14 +315,14 @@ const PhotoPickerCell = React.memo(function PhotoPickerCell(props: PhotoPickerCe
|
|
|
316
315
|
borderWidth: SELECTION_RING_WIDTH,
|
|
317
316
|
borderColor: primaryColor,
|
|
318
317
|
transform: [{ scale: ringScale }],
|
|
318
|
+
pointerEvents: 'none',
|
|
319
319
|
}}
|
|
320
320
|
/>
|
|
321
321
|
)}
|
|
322
322
|
{isSelected && (
|
|
323
323
|
<View
|
|
324
|
-
pointerEvents="none"
|
|
325
324
|
className="absolute top-1.5 right-1.5 min-w-[22px] h-[22px] px-1.5 rounded-full items-center justify-center"
|
|
326
|
-
style={{ backgroundColor: primaryColor }}
|
|
325
|
+
style={{ backgroundColor: primaryColor, pointerEvents: 'none' }}
|
|
327
326
|
>
|
|
328
327
|
<Text className="text-white text-[12px] font-bold leading-[14px]">
|
|
329
328
|
{selectionIndex > 0 ? String(selectionIndex) : ''}
|
|
@@ -546,12 +545,12 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
546
545
|
because the last tile in each row carries no right margin (a
|
|
547
546
|
full row + gutters fits the measured width by construction). */
|
|
548
547
|
<View
|
|
549
|
-
style={{ flexDirection: 'row', flexWrap: 'wrap' }}
|
|
550
|
-
pointerEvents="none"
|
|
548
|
+
style={{ flexDirection: 'row', flexWrap: 'wrap', pointerEvents: 'none' }}
|
|
551
549
|
accessibilityElementsHidden
|
|
552
550
|
importantForAccessibility="no-hide-descendants"
|
|
553
551
|
>
|
|
554
552
|
{Array.from({ length: skeletonTileCount }, (_, index) => (
|
|
553
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: fixed-size skeleton grid, order never changes
|
|
555
554
|
<Skeleton.Box
|
|
556
555
|
key={`skeleton-${index}`}
|
|
557
556
|
width={cellSize}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
|
-
import { View, Text, ActivityIndicator, StyleSheet } from 'react-native';
|
|
2
|
+
import { View, Text, ActivityIndicator, StyleSheet, Platform, type ViewStyle } from 'react-native';
|
|
3
3
|
import { Ionicons } from '@expo/vector-icons';
|
|
4
4
|
|
|
5
5
|
export interface UploadBarProps {
|
|
@@ -13,15 +13,19 @@ export interface UploadBarProps {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
// The banner drop-shadow has no Bloom/Tailwind token equivalent, so it stays an
|
|
16
|
-
// inline style (
|
|
16
|
+
// inline style. `boxShadow` on web (RN-Web deprecated the `shadow*` props);
|
|
17
|
+
// RN `shadow*`/`elevation` on native.
|
|
17
18
|
const shadowStyle = StyleSheet.create({
|
|
18
|
-
banner: {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
banner: Platform.select({
|
|
20
|
+
web: { boxShadow: '0px 2px 6px rgba(0, 0, 0, 0.1)' },
|
|
21
|
+
default: {
|
|
22
|
+
shadowColor: '#000',
|
|
23
|
+
shadowOpacity: 0.1,
|
|
24
|
+
shadowRadius: 6,
|
|
25
|
+
shadowOffset: { width: 0, height: 2 },
|
|
26
|
+
elevation: 2,
|
|
27
|
+
},
|
|
28
|
+
}) as ViewStyle,
|
|
25
29
|
});
|
|
26
30
|
|
|
27
31
|
/**
|
|
@@ -30,7 +34,10 @@ const shadowStyle = StyleSheet.create({
|
|
|
30
34
|
* gates rendering on `!selectMode && uploading`.
|
|
31
35
|
*/
|
|
32
36
|
const UploadBar: React.FC<UploadBarProps> = ({ uploadProgress, isDark, colors, t }) => (
|
|
33
|
-
<View
|
|
37
|
+
<View
|
|
38
|
+
className="absolute top-[72px] left-0 right-0 items-center z-50"
|
|
39
|
+
style={{ pointerEvents: 'none' }}
|
|
40
|
+
>
|
|
34
41
|
<View
|
|
35
42
|
className="flex-row items-center px-3.5 py-2.5 rounded-[24px] gap-2.5 border min-w-[200px]"
|
|
36
43
|
style={[shadowStyle.banner, { backgroundColor: isDark ? '#222831EE' : '#FFFFFFEE', borderColor: colors.border }]}
|