@oxy.so/services 12.10.1 → 12.12.0
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/accountSettings/AccountCategoriesSection.js +142 -0
- package/lib/commonjs/ui/components/accountSettings/AccountCategoriesSection.js.map +1 -0
- package/lib/commonjs/ui/components/accountSettings/AccountSettings.js +361 -0
- package/lib/commonjs/ui/components/accountSettings/AccountSettings.js.map +1 -0
- package/lib/commonjs/ui/hooks/useAvatarPicker.js +54 -48
- package/lib/commonjs/ui/hooks/useAvatarPicker.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js +16 -237
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/ChangeAvatarScreen.js +23 -4
- package/lib/commonjs/ui/screens/ChangeAvatarScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/CreateAccountScreen.js +6 -52
- package/lib/commonjs/ui/screens/CreateAccountScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/index.js +7 -0
- package/lib/commonjs/ui/screens/index.js.map +1 -1
- package/lib/module/ui/components/accountSettings/AccountCategoriesSection.js +134 -0
- package/lib/module/ui/components/accountSettings/AccountCategoriesSection.js.map +1 -0
- package/lib/module/ui/components/accountSettings/AccountSettings.js +355 -0
- package/lib/module/ui/components/accountSettings/AccountSettings.js.map +1 -0
- package/lib/module/ui/hooks/useAvatarPicker.js +53 -48
- package/lib/module/ui/hooks/useAvatarPicker.js.map +1 -1
- package/lib/module/ui/screens/AccountSettingsScreen.js +16 -235
- package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/module/ui/screens/ChangeAvatarScreen.js +23 -4
- package/lib/module/ui/screens/ChangeAvatarScreen.js.map +1 -1
- package/lib/module/ui/screens/CreateAccountScreen.js +7 -53
- package/lib/module/ui/screens/CreateAccountScreen.js.map +1 -1
- package/lib/module/ui/screens/index.js +4 -0
- package/lib/module/ui/screens/index.js.map +1 -1
- package/lib/typescript/commonjs/ui/components/accountSettings/AccountCategoriesSection.d.ts +26 -0
- package/lib/typescript/commonjs/ui/components/accountSettings/AccountCategoriesSection.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/accountSettings/AccountSettings.d.ts +80 -0
- package/lib/typescript/commonjs/ui/components/accountSettings/AccountSettings.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/hooks/useAvatarPicker.d.ts +22 -1
- package/lib/typescript/commonjs/ui/hooks/useAvatarPicker.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/AccountSettingsScreen.d.ts +4 -5
- package/lib/typescript/commonjs/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
- 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/index.d.ts +1 -0
- package/lib/typescript/commonjs/ui/screens/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/accountSettings/AccountCategoriesSection.d.ts +26 -0
- package/lib/typescript/module/ui/components/accountSettings/AccountCategoriesSection.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/accountSettings/AccountSettings.d.ts +80 -0
- package/lib/typescript/module/ui/components/accountSettings/AccountSettings.d.ts.map +1 -0
- package/lib/typescript/module/ui/hooks/useAvatarPicker.d.ts +22 -1
- package/lib/typescript/module/ui/hooks/useAvatarPicker.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/AccountSettingsScreen.d.ts +4 -5
- package/lib/typescript/module/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
- 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/index.d.ts +1 -0
- package/lib/typescript/module/ui/screens/index.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/ui/components/accountSettings/AccountCategoriesSection.tsx +180 -0
- package/src/ui/components/accountSettings/AccountSettings.tsx +421 -0
- package/src/ui/components/accountSettings/__tests__/AccountSettings.test.tsx +101 -0
- package/src/ui/hooks/useAvatarPicker.ts +65 -69
- package/src/ui/screens/AccountSettingsScreen.tsx +13 -277
- package/src/ui/screens/ChangeAvatarScreen.tsx +25 -6
- package/src/ui/screens/CreateAccountScreen.tsx +10 -61
- package/src/ui/screens/index.ts +8 -0
|
@@ -1,41 +1,17 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
|
-
import { OrganizationSettings } from '../components/organizations/OrganizationSettings';
|
|
3
|
-
import { useCallback, useMemo, useState } from 'react';
|
|
4
|
-
import { View } from 'react-native';
|
|
5
|
-
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
6
|
-
import { toast } from '@oxy.so/bloom/toast';
|
|
7
|
-
import { surfaces } from '@oxy.so/bloom/surfaces';
|
|
8
|
-
import { useTheme } from '@oxy.so/bloom/theme';
|
|
9
|
-
import { Text } from '@oxy.so/bloom/typography';
|
|
10
|
-
import { Button } from '@oxy.so/bloom/button';
|
|
11
|
-
import { TextField, TextFieldInput } from '@oxy.so/bloom/text-field';
|
|
12
|
-
import { GroupedSection, GroupedSectionItem } from '@oxy.so/bloom/grouped-section';
|
|
13
|
-
import {
|
|
14
|
-
accountNameInvalidKey,
|
|
15
|
-
accountNameInvalidMessage,
|
|
16
|
-
accountNameMaxLength,
|
|
17
|
-
getNormalizedUserHandle,
|
|
18
|
-
isValidAccountName,
|
|
19
|
-
type UpdateAccountInput,
|
|
20
|
-
} from '@oxy.so/core';
|
|
21
2
|
import type { BaseScreenProps } from '../types/navigation';
|
|
22
|
-
import {
|
|
23
|
-
|
|
3
|
+
import {
|
|
4
|
+
AccountSettings,
|
|
5
|
+
useAccountSettingsNode,
|
|
6
|
+
} from '../components/accountSettings/AccountSettings';
|
|
24
7
|
import { useI18n } from '../hooks/useI18n';
|
|
25
8
|
import { useSurfaceHeader } from '../hooks/useSurfaceHeader';
|
|
26
|
-
import { clearedFieldsFromAccountUpdate, upsertCachedUser } from '../hooks/queries/userCache';
|
|
27
|
-
|
|
28
|
-
const BIO_MAX = 160;
|
|
29
|
-
|
|
30
|
-
const errorMessage = (error: unknown, fallback: string): string =>
|
|
31
|
-
error instanceof Error && error.message ? error.message : fallback;
|
|
32
9
|
|
|
33
10
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* graph.
|
|
11
|
+
* The `AccountSettings` surface: {@link AccountSettings} for the `accountId`
|
|
12
|
+
* route prop, under the surface's own header. Self-surface for the caller's own
|
|
13
|
+
* personal account lives in {@link ManageAccountScreen}; this screen manages a
|
|
14
|
+
* non-personal account in the graph.
|
|
39
15
|
*/
|
|
40
16
|
const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
|
|
41
17
|
onClose,
|
|
@@ -43,259 +19,19 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
|
|
|
43
19
|
navigate,
|
|
44
20
|
accountId,
|
|
45
21
|
}) => {
|
|
46
|
-
const bloomTheme = useTheme();
|
|
47
|
-
const colors = bloomTheme.colors;
|
|
48
22
|
const { t } = useI18n();
|
|
49
|
-
|
|
50
|
-
const { oxyServices, canUsePrivateApi, user, accounts, switchToAccount } = useOxy();
|
|
51
|
-
const queryClient = useQueryClient();
|
|
52
|
-
|
|
53
23
|
const id = typeof accountId === 'string' ? accountId : '';
|
|
54
|
-
|
|
55
|
-
const accountQuery = useQuery({
|
|
56
|
-
queryKey: ['accounts', 'detail', id, user?.id, canUsePrivateApi],
|
|
57
|
-
initialData: canUsePrivateApi
|
|
58
|
-
? accounts?.find((account) => account.accountId === id)
|
|
59
|
-
: undefined,
|
|
60
|
-
initialDataUpdatedAt: 0,
|
|
61
|
-
queryFn: () => oxyServices.accounts.get(id),
|
|
62
|
-
enabled: canUsePrivateApi && id.length > 0,
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
const node = canUsePrivateApi && accountQuery.isSuccess ? accountQuery.data : null;
|
|
66
|
-
const permissions = node?.callerMembership?.permissions ?? [];
|
|
67
|
-
const isImplicitOwner = node ? node.relationship !== 'member' : false;
|
|
68
|
-
const can = useCallback(
|
|
69
|
-
(permission: string): boolean => isImplicitOwner || permissions.includes(permission),
|
|
70
|
-
[isImplicitOwner, permissions],
|
|
71
|
-
);
|
|
72
|
-
const canUpdate = can('account:update');
|
|
73
|
-
const canViewMembers = can('members:read');
|
|
74
|
-
const canArchive = can('account:delete');
|
|
75
|
-
|
|
76
|
-
// Editable fields, seeded lazily from the loaded account once.
|
|
77
|
-
const [seeded, setSeeded] = useState(false);
|
|
78
|
-
const [displayName, setDisplayName] = useState('');
|
|
79
|
-
const [displayNameError, setDisplayNameError] = useState('');
|
|
80
|
-
const [bio, setBio] = useState('');
|
|
81
|
-
|
|
82
|
-
// Seed the form from the account during render (no useEffect): the first time
|
|
83
|
-
// the query resolves we capture its values into local edit state.
|
|
84
|
-
if (node && !seeded) {
|
|
85
|
-
setDisplayName(node.account?.name?.displayName ?? getNormalizedUserHandle(node.account) ?? '');
|
|
86
|
-
setBio(node.account?.bio ?? '');
|
|
87
|
-
setSeeded(true);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const updateMutation = useMutation({
|
|
91
|
-
mutationKey: ['accounts', 'update', id],
|
|
92
|
-
mutationFn: (input: UpdateAccountInput) => oxyServices.accounts.update(id, input),
|
|
93
|
-
onSuccess: (updatedNode, input) => {
|
|
94
|
-
const cleared = clearedFieldsFromAccountUpdate(input);
|
|
95
|
-
upsertCachedUser(queryClient, updatedNode.account, user?.id, {
|
|
96
|
-
cleared: cleared.length > 0 ? cleared : undefined,
|
|
97
|
-
});
|
|
98
|
-
queryClient.invalidateQueries({ queryKey: ['accounts', 'detail', id] });
|
|
99
|
-
queryClient.invalidateQueries({ queryKey: ['accounts'] });
|
|
100
|
-
toast.success(t('accounts.settings.toasts.saved') || 'Account updated');
|
|
101
|
-
},
|
|
102
|
-
onError: (error) => {
|
|
103
|
-
toast.error(
|
|
104
|
-
errorMessage(error, t('accounts.settings.toasts.saveFailed') || 'Failed to update account'),
|
|
105
|
-
);
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
const archiveMutation = useMutation({
|
|
110
|
-
mutationKey: ['accounts', 'archive', id],
|
|
111
|
-
mutationFn: () => oxyServices.accounts.archive(id),
|
|
112
|
-
onSuccess: async () => {
|
|
113
|
-
// If we archived the account we're currently signed in AS, switch back to
|
|
114
|
-
// the personal account so the app isn't left as an archived identity.
|
|
115
|
-
if (user?.id === id) {
|
|
116
|
-
const personal = accounts.find((node) => node.relationship === 'self');
|
|
117
|
-
if (personal) {
|
|
118
|
-
await switchToAccount(personal.accountId).catch(() => undefined);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
queryClient.invalidateQueries({ queryKey: ['accounts'] });
|
|
122
|
-
toast.success(t('accounts.settings.toasts.archived') || 'Account archived');
|
|
123
|
-
(onClose ?? goBack)?.();
|
|
124
|
-
},
|
|
125
|
-
onError: (error) => {
|
|
126
|
-
toast.error(
|
|
127
|
-
errorMessage(
|
|
128
|
-
error,
|
|
129
|
-
t('accounts.settings.toasts.archiveFailed') || 'Failed to archive account',
|
|
130
|
-
),
|
|
131
|
-
);
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
const handleArchive = useCallback(async () => {
|
|
136
|
-
const confirmed = await surfaces.confirm({
|
|
137
|
-
title: t('accounts.settings.archive.confirmTitle') || 'Archive account',
|
|
138
|
-
description:
|
|
139
|
-
t('accounts.settings.archive.confirmDescription') ||
|
|
140
|
-
'Archive this account? It will be deactivated and its members will lose access.',
|
|
141
|
-
confirmLabel: t('accounts.settings.archive.title') || 'Archive account',
|
|
142
|
-
cancelLabel: t('common.cancel') || 'Cancel',
|
|
143
|
-
destructive: true,
|
|
144
|
-
});
|
|
145
|
-
if (confirmed) archiveMutation.mutate();
|
|
146
|
-
}, [archiveMutation, t]);
|
|
147
|
-
|
|
148
|
-
const handleDisplayNameChange = useCallback(
|
|
149
|
-
(value: string) => {
|
|
150
|
-
setDisplayName(value);
|
|
151
|
-
const trimmed = value.trim();
|
|
152
|
-
if (!trimmed) {
|
|
153
|
-
setDisplayNameError('');
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
const kind = node?.account.kind;
|
|
157
|
-
setDisplayNameError(
|
|
158
|
-
isValidAccountName(trimmed, kind)
|
|
159
|
-
? ''
|
|
160
|
-
: t(`accounts.settings.displayName.${accountNameInvalidKey(kind)}`) ||
|
|
161
|
-
accountNameInvalidMessage(kind),
|
|
162
|
-
);
|
|
163
|
-
},
|
|
164
|
-
[node?.account.kind, t],
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
const handleSave = useCallback(() => {
|
|
168
|
-
const trimmed = displayName.trim();
|
|
169
|
-
if (!trimmed || displayNameError) return;
|
|
170
|
-
// Managed accounts (org / project / bot / channel) have a title, not a
|
|
171
|
-
// given-and-family name — persist the explicit displayName column.
|
|
172
|
-
updateMutation.mutate({
|
|
173
|
-
name: { displayName: trimmed },
|
|
174
|
-
bio: bio.trim() ? bio.trim() : null,
|
|
175
|
-
});
|
|
176
|
-
}, [displayName, displayNameError, bio, updateMutation]);
|
|
177
|
-
|
|
178
|
-
const title = t('accounts.settings.title') || 'Account settings';
|
|
179
|
-
|
|
180
|
-
const accountHandle = useMemo(() => {
|
|
181
|
-
const username = node?.account?.username;
|
|
182
|
-
return username ? `@${username}` : '';
|
|
183
|
-
}, [node?.account?.username]);
|
|
24
|
+
const { node } = useAccountSettingsNode(id);
|
|
184
25
|
|
|
185
26
|
// The nav large title is the account's handle once loaded (falls back to the
|
|
186
27
|
// generic screen title before the account resolves).
|
|
28
|
+
const username = node?.account?.username;
|
|
187
29
|
useSurfaceHeader({
|
|
188
|
-
title:
|
|
189
|
-
subtitle:
|
|
190
|
-
t('accounts.settings.subtitle') || 'Manage this account’s profile, members, and access.',
|
|
30
|
+
title: username ? `@${username}` : t('accounts.settings.title'),
|
|
31
|
+
subtitle: t('accounts.settings.subtitle'),
|
|
191
32
|
});
|
|
192
33
|
|
|
193
|
-
|
|
194
|
-
return (
|
|
195
|
-
<>
|
|
196
|
-
<View className="items-center justify-center px-screen-margin py-space-40">
|
|
197
|
-
<Text className="text-body font-body text-text-secondary text-center">
|
|
198
|
-
{t('accounts.settings.errors.missingAccount') || 'No account selected.'}
|
|
199
|
-
</Text>
|
|
200
|
-
</View>
|
|
201
|
-
</>
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return (
|
|
206
|
-
<>
|
|
207
|
-
<View className="px-screen-margin pt-space-24 pb-space-32 gap-space-24">
|
|
208
|
-
{accountQuery.isLoading ? (
|
|
209
|
-
<View className="items-center py-space-32">{null}</View>
|
|
210
|
-
) : (
|
|
211
|
-
<>
|
|
212
|
-
{/* Profile edit */}
|
|
213
|
-
{canUpdate ? (
|
|
214
|
-
<View className="gap-space-16 p-space-16 rounded-radius-20 bg-fill">
|
|
215
|
-
<View className="gap-space-4">
|
|
216
|
-
<TextField isInvalid={Boolean(displayNameError)}>
|
|
217
|
-
<TextFieldInput
|
|
218
|
-
floatingLabel
|
|
219
|
-
label={t('accounts.settings.displayName.label') || 'Display name'}
|
|
220
|
-
value={displayName}
|
|
221
|
-
onChangeText={handleDisplayNameChange}
|
|
222
|
-
isInvalid={Boolean(displayNameError)}
|
|
223
|
-
maxLength={accountNameMaxLength(node?.account.kind)}
|
|
224
|
-
/>
|
|
225
|
-
</TextField>
|
|
226
|
-
{displayNameError ? (
|
|
227
|
-
<Text className="text-caption font-caption text-negative px-space-4">
|
|
228
|
-
{displayNameError}
|
|
229
|
-
</Text>
|
|
230
|
-
) : null}
|
|
231
|
-
</View>
|
|
232
|
-
<View className="gap-space-4">
|
|
233
|
-
<TextField>
|
|
234
|
-
<TextFieldInput
|
|
235
|
-
floatingLabel
|
|
236
|
-
label={t('accounts.settings.bio.label') || 'Bio (optional)'}
|
|
237
|
-
value={bio}
|
|
238
|
-
onChangeText={setBio}
|
|
239
|
-
maxLength={BIO_MAX}
|
|
240
|
-
multiline
|
|
241
|
-
numberOfLines={3}
|
|
242
|
-
textAlignVertical="top"
|
|
243
|
-
/>
|
|
244
|
-
</TextField>
|
|
245
|
-
<Text className="text-caption font-caption text-text-tertiary px-space-4 text-right">
|
|
246
|
-
{bio.length}/{BIO_MAX}
|
|
247
|
-
</Text>
|
|
248
|
-
</View>
|
|
249
|
-
<Button
|
|
250
|
-
appearance="solid"
|
|
251
|
-
tone="accent"
|
|
252
|
-
onPress={handleSave}
|
|
253
|
-
disabled={
|
|
254
|
-
updateMutation.isPending || !displayName.trim() || Boolean(displayNameError)
|
|
255
|
-
}
|
|
256
|
-
loading={updateMutation.isPending}
|
|
257
|
-
accessibilityLabel={t('accounts.settings.save') || 'Save changes'}
|
|
258
|
-
className="w-full"
|
|
259
|
-
>
|
|
260
|
-
{t('accounts.settings.save') || 'Save changes'}
|
|
261
|
-
</Button>
|
|
262
|
-
</View>
|
|
263
|
-
) : null}
|
|
264
|
-
|
|
265
|
-
{node?.account.kind === 'organization' ? (
|
|
266
|
-
<OrganizationSettings key={id} accountId={id} canUpdate={canUpdate} />
|
|
267
|
-
) : null}
|
|
268
|
-
|
|
269
|
-
{/* Members entry */}
|
|
270
|
-
{canViewMembers ? (
|
|
271
|
-
<GroupedSection title={t('accounts.settings.sections.access') || 'Access'}>
|
|
272
|
-
<GroupedSectionItem
|
|
273
|
-
icon={<SettingsIcon name="account-multiple" color={colors.info} />}
|
|
274
|
-
title={t('accounts.members.title') || 'Members'}
|
|
275
|
-
description={
|
|
276
|
-
t('accounts.settings.members.subtitle') || 'Invite people and manage roles'
|
|
277
|
-
}
|
|
278
|
-
onPress={() => navigate?.('AccountMembers', { accountId: id })}
|
|
279
|
-
/>
|
|
280
|
-
</GroupedSection>
|
|
281
|
-
) : null}
|
|
282
|
-
|
|
283
|
-
{/* Danger zone */}
|
|
284
|
-
{canArchive ? (
|
|
285
|
-
<GroupedSection title={t('accounts.settings.sections.dangerZone') || 'Danger zone'}>
|
|
286
|
-
<GroupedSectionItem
|
|
287
|
-
icon={<SettingsIcon name="archive-arrow-down" color={colors.error} />}
|
|
288
|
-
title={t('accounts.settings.archive.title') || 'Archive account'}
|
|
289
|
-
description={t('accounts.settings.archive.subtitle') || 'Deactivate this account'}
|
|
290
|
-
onPress={handleArchive}
|
|
291
|
-
/>
|
|
292
|
-
</GroupedSection>
|
|
293
|
-
) : null}
|
|
294
|
-
</>
|
|
295
|
-
)}
|
|
296
|
-
</View>
|
|
297
|
-
</>
|
|
298
|
-
);
|
|
34
|
+
return <AccountSettings accountId={id} navigate={navigate} onClose={onClose ?? goBack} />;
|
|
299
35
|
};
|
|
300
36
|
|
|
301
37
|
export default AccountSettingsScreen;
|
|
@@ -47,6 +47,7 @@ import { useI18n } from '../hooks/useI18n';
|
|
|
47
47
|
import { useSurfaceHeader } from '../hooks/useSurfaceHeader';
|
|
48
48
|
import { SettingsIcon } from '../components/SettingsIcon';
|
|
49
49
|
import type { BaseScreenProps } from '../types/navigation';
|
|
50
|
+
import type { AvatarPickerSubject } from '../hooks/useAvatarPicker';
|
|
50
51
|
|
|
51
52
|
/** Component name used in `logger` context for filtered diagnostics. */
|
|
52
53
|
const LOG_COMPONENT = 'ChangeAvatarScreen';
|
|
@@ -131,10 +132,25 @@ async function loadImagePicker(): Promise<ImagePickerModule | null> {
|
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
|
|
135
|
+
/**
|
|
136
|
+
* The `subject` route prop, when a caller is changing a picture that is not the
|
|
137
|
+
* signed-in user's (see `pickAvatarFile`). Route props arrive untyped.
|
|
138
|
+
*/
|
|
139
|
+
function readSubject(value: unknown): AvatarPickerSubject | null {
|
|
140
|
+
if (!value || typeof value !== 'object') return null;
|
|
141
|
+
const { avatar, displayName } = value as Record<string, unknown>;
|
|
142
|
+
return {
|
|
143
|
+
avatar: typeof avatar === 'string' && avatar.length > 0 ? avatar : null,
|
|
144
|
+
displayName: typeof displayName === 'string' ? displayName : undefined,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const ChangeAvatarScreen: React.FC<BaseScreenProps> = ({ navigate, dismiss, subject }) => {
|
|
135
149
|
const bloomTheme = useTheme();
|
|
136
150
|
const { t } = useI18n();
|
|
137
151
|
const { user, oxyServices } = useOxy();
|
|
152
|
+
const other = useMemo(() => readSubject(subject), [subject]);
|
|
153
|
+
const currentAvatar = other ? other.avatar : user?.avatar;
|
|
138
154
|
|
|
139
155
|
/** Set when a permission was refused — drives the inline denied notice. */
|
|
140
156
|
const [denied, setDenied] = useState<DeniedSource | null>(null);
|
|
@@ -144,13 +160,16 @@ const ChangeAvatarScreen: React.FC<BaseScreenProps> = ({ navigate, dismiss }) =>
|
|
|
144
160
|
useSurfaceHeader(useMemo(() => ({ title: t('changeAvatar.title') }), [t]));
|
|
145
161
|
|
|
146
162
|
const displayName = useMemo(
|
|
147
|
-
() =>
|
|
148
|
-
|
|
163
|
+
() =>
|
|
164
|
+
other
|
|
165
|
+
? (other.displayName ?? '')
|
|
166
|
+
: (user?.name?.displayName ?? getNormalizedUserHandle(user) ?? ''),
|
|
167
|
+
[other, user],
|
|
149
168
|
);
|
|
150
169
|
const avatarUri = useMemo(
|
|
151
170
|
// `thumb` is 256x256 — exactly a 2x source for the 132dp hero.
|
|
152
|
-
() => (
|
|
153
|
-
[
|
|
171
|
+
() => (currentAvatar ? oxyServices.assets.publicUrl(currentAvatar, 'thumb') : undefined),
|
|
172
|
+
[currentAvatar, oxyServices],
|
|
154
173
|
);
|
|
155
174
|
|
|
156
175
|
/**
|
|
@@ -362,7 +381,7 @@ const ChangeAvatarScreen: React.FC<BaseScreenProps> = ({ navigate, dismiss }) =>
|
|
|
362
381
|
title={t('changeAvatar.sources.remove.title')}
|
|
363
382
|
description={t('changeAvatar.sources.remove.description')}
|
|
364
383
|
destructive
|
|
365
|
-
disabled={busy || !
|
|
384
|
+
disabled={busy || !currentAvatar}
|
|
366
385
|
onPress={() => void removeCurrentPhoto()}
|
|
367
386
|
/>
|
|
368
387
|
</GroupedSection>
|
|
@@ -7,13 +7,10 @@ import { View, ActivityIndicator } from 'react-native';
|
|
|
7
7
|
import Ionicons from '../icons/Ionicons';
|
|
8
8
|
import type { AccountCategoryId, AccountKind, CreateAccountInput } from '@oxy.so/core';
|
|
9
9
|
import {
|
|
10
|
-
accountCategoryLabel,
|
|
11
10
|
accountNameInvalidKey,
|
|
12
11
|
accountNameInvalidMessage,
|
|
13
12
|
accountNameMaxLength,
|
|
14
13
|
isValidAccountName,
|
|
15
|
-
MAX_ACCOUNT_CATEGORIES,
|
|
16
|
-
SELECTABLE_ACCOUNT_CATEGORY_IDS,
|
|
17
14
|
} from '@oxy.so/core';
|
|
18
15
|
import {
|
|
19
16
|
applyBotUsernameSuffix,
|
|
@@ -33,6 +30,7 @@ import { Button } from '@oxy.so/bloom/button';
|
|
|
33
30
|
import { TextField, TextFieldInput } from '@oxy.so/bloom/text-field';
|
|
34
31
|
import { GroupedSection, GroupedSectionItem } from '@oxy.so/bloom/grouped-section';
|
|
35
32
|
import { useOxy } from '../context/oxyRuntimeContext';
|
|
33
|
+
import { AccountCategoriesSection } from '../components/accountSettings/AccountCategoriesSection';
|
|
36
34
|
import { toast } from '@oxy.so/bloom/toast';
|
|
37
35
|
|
|
38
36
|
type UsernameStatus = 'idle' | 'checking' | 'available' | 'taken' | 'invalid';
|
|
@@ -102,13 +100,6 @@ const kindDescription = (
|
|
|
102
100
|
}
|
|
103
101
|
};
|
|
104
102
|
|
|
105
|
-
/**
|
|
106
|
-
* Only the SELECTABLE ids are offered. The full `ACCOUNT_CATEGORY_IDS` still
|
|
107
|
-
* contains withdrawn ones so that accounts already carrying them keep working —
|
|
108
|
-
* offering them here would be how an account newly acquires one.
|
|
109
|
-
*/
|
|
110
|
-
const ACCOUNT_CATEGORY_OPTIONS: readonly AccountCategoryId[] = SELECTABLE_ACCOUNT_CATEGORY_IDS;
|
|
111
|
-
|
|
112
103
|
/**
|
|
113
104
|
* Create a new account in the unified account graph (an organization, project,
|
|
114
105
|
* or bot). The caller becomes its owner. Optionally nested under a parent
|
|
@@ -123,7 +114,7 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({
|
|
|
123
114
|
}) => {
|
|
124
115
|
const bloomTheme = useTheme();
|
|
125
116
|
const { oxyServices, createAccount, switchToAccount, user, canUsePrivateApi } = useOxy();
|
|
126
|
-
const { t
|
|
117
|
+
const { t } = useI18n();
|
|
127
118
|
|
|
128
119
|
const parentId = typeof parentAccountId === 'string' ? parentAccountId : undefined;
|
|
129
120
|
|
|
@@ -291,23 +282,6 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({
|
|
|
291
282
|
[kind, t],
|
|
292
283
|
);
|
|
293
284
|
|
|
294
|
-
/**
|
|
295
|
-
* Append on select, splice out on de-select. Never sorts — appending is what
|
|
296
|
-
* makes the FIRST category the user chose the primary one, and a sort would
|
|
297
|
-
* silently reassign that. De-selecting the primary promotes whatever the user
|
|
298
|
-
* picked next, which is the only interpretation that does not invent a choice
|
|
299
|
-
* on their behalf.
|
|
300
|
-
*/
|
|
301
|
-
const toggleAccountCategory = useCallback((category: AccountCategoryId) => {
|
|
302
|
-
setAccountCategories((current) => {
|
|
303
|
-
if (current.includes(category)) {
|
|
304
|
-
return current.filter((entry) => entry !== category);
|
|
305
|
-
}
|
|
306
|
-
if (current.length >= MAX_ACCOUNT_CATEGORIES) return current;
|
|
307
|
-
return [...current, category];
|
|
308
|
-
});
|
|
309
|
-
}, []);
|
|
310
|
-
|
|
311
285
|
const canCreate =
|
|
312
286
|
canUsePrivateApi &&
|
|
313
287
|
usernameStatus === 'available' &&
|
|
@@ -449,39 +423,14 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({
|
|
|
449
423
|
/>
|
|
450
424
|
</>
|
|
451
425
|
) : null}
|
|
452
|
-
{/* Categories —
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
const selected = position >= 0;
|
|
461
|
-
const label = accountCategoryLabel(locale, option);
|
|
462
|
-
return (
|
|
463
|
-
<GroupedSectionItem
|
|
464
|
-
key={option}
|
|
465
|
-
title={label}
|
|
466
|
-
disabled={!selected && accountCategories.length >= MAX_ACCOUNT_CATEGORIES}
|
|
467
|
-
onPress={() => toggleAccountCategory(option)}
|
|
468
|
-
showChevron={false}
|
|
469
|
-
rightElement={
|
|
470
|
-
selected ? (
|
|
471
|
-
<Text className="text-caption-1 font-semibold text-primary">
|
|
472
|
-
{position + 1}
|
|
473
|
-
</Text>
|
|
474
|
-
) : undefined
|
|
475
|
-
}
|
|
476
|
-
accessibilityLabel={label}
|
|
477
|
-
/>
|
|
478
|
-
);
|
|
479
|
-
})}
|
|
480
|
-
</GroupedSection>
|
|
481
|
-
<Text className="text-caption font-caption text-text-tertiary px-space-4">
|
|
482
|
-
{t('accounts.create.accountCategory.hint', { max: MAX_ACCOUNT_CATEGORIES })}
|
|
483
|
-
</Text>
|
|
484
|
-
</View>
|
|
426
|
+
{/* Categories — the same editor the account's settings use: what is
|
|
427
|
+
chosen, in order (the first is the primary), and the vocabulary to add
|
|
428
|
+
from. */}
|
|
429
|
+
<AccountCategoriesSection
|
|
430
|
+
value={accountCategories}
|
|
431
|
+
onChange={setAccountCategories}
|
|
432
|
+
disabled={isCreating}
|
|
433
|
+
/>
|
|
485
434
|
|
|
486
435
|
{/* Details — a grouped section card hosting the form fields */}
|
|
487
436
|
<GroupedSection title={t('accounts.create.detailsSection') || 'Details'}>
|
package/src/ui/screens/index.ts
CHANGED
|
@@ -18,3 +18,11 @@ export { default as AccountSettingsScreen } from './AccountSettingsScreen';
|
|
|
18
18
|
|
|
19
19
|
export { default as SupervisionScreen } from './SupervisionScreen';
|
|
20
20
|
export { default as OxyOneScreen } from './OxyOneScreen';
|
|
21
|
+
|
|
22
|
+
// The body of the `AccountSettings` surface, for an app embedding a managed
|
|
23
|
+
// account's settings in its own page with its own sections around them.
|
|
24
|
+
export {
|
|
25
|
+
AccountSettings,
|
|
26
|
+
type AccountSettingsProps,
|
|
27
|
+
type AccountSettingsArchiveOptions,
|
|
28
|
+
} from '../components/accountSettings/AccountSettings';
|