+4
-1
src/components/Dialog/index.tsx
+4
-1
src/components/Dialog/index.tsx
···
267
267
scrollEventThrottle={50}
268
268
onScroll={isAndroid ? onScroll : undefined}
269
269
keyboardShouldPersistTaps="handled"
270
-
stickyHeaderIndices={header ? [0] : undefined}>
270
+
// TODO: figure out why this positions the header absolutely (rather than stickily)
271
+
// on Android. fine to disable for now, because we don't have any
272
+
// dialogs that use this that actually scroll -sfn
273
+
stickyHeaderIndices={ios(header ? [0] : undefined)}>
271
274
{header}
272
275
{children}
273
276
</KeyboardAwareScrollView>
+454
src/components/dialogs/lists/CreateOrEditListDialog.tsx
+454
src/components/dialogs/lists/CreateOrEditListDialog.tsx
···
1
+
import {useCallback, useEffect, useMemo, useState} from 'react'
2
+
import {useWindowDimensions, View} from 'react-native'
3
+
import {type AppBskyGraphDefs, RichText as RichTextAPI} from '@atproto/api'
4
+
import {msg, Plural, Trans} from '@lingui/macro'
5
+
import {useLingui} from '@lingui/react'
6
+
7
+
import {cleanError} from '#/lib/strings/errors'
8
+
import {useWarnMaxGraphemeCount} from '#/lib/strings/helpers'
9
+
import {richTextToString} from '#/lib/strings/rich-text-helpers'
10
+
import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
11
+
import {logger} from '#/logger'
12
+
import {isWeb} from '#/platform/detection'
13
+
import {type ImageMeta} from '#/state/gallery'
14
+
import {
15
+
useListCreateMutation,
16
+
useListMetadataMutation,
17
+
} from '#/state/queries/list'
18
+
import {useAgent} from '#/state/session'
19
+
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
20
+
import * as Toast from '#/view/com/util/Toast'
21
+
import {EditableUserAvatar} from '#/view/com/util/UserAvatar'
22
+
import {atoms as a, useTheme, web} from '#/alf'
23
+
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
24
+
import * as Dialog from '#/components/Dialog'
25
+
import * as TextField from '#/components/forms/TextField'
26
+
import {Loader} from '#/components/Loader'
27
+
import * as Prompt from '#/components/Prompt'
28
+
import {Text} from '#/components/Typography'
29
+
30
+
const DISPLAY_NAME_MAX_GRAPHEMES = 64
31
+
const DESCRIPTION_MAX_GRAPHEMES = 300
32
+
33
+
export function CreateOrEditListDialog({
34
+
control,
35
+
list,
36
+
purpose,
37
+
onSave,
38
+
}: {
39
+
control: Dialog.DialogControlProps
40
+
list?: AppBskyGraphDefs.ListView
41
+
purpose?: AppBskyGraphDefs.ListPurpose
42
+
onSave?: (uri: string) => void
43
+
}) {
44
+
const {_} = useLingui()
45
+
const cancelControl = Dialog.useDialogControl()
46
+
const [dirty, setDirty] = useState(false)
47
+
const {height} = useWindowDimensions()
48
+
49
+
// 'You might lose unsaved changes' warning
50
+
useEffect(() => {
51
+
if (isWeb && dirty) {
52
+
const abortController = new AbortController()
53
+
const {signal} = abortController
54
+
window.addEventListener('beforeunload', evt => evt.preventDefault(), {
55
+
signal,
56
+
})
57
+
return () => {
58
+
abortController.abort()
59
+
}
60
+
}
61
+
}, [dirty])
62
+
63
+
const onPressCancel = useCallback(() => {
64
+
if (dirty) {
65
+
cancelControl.open()
66
+
} else {
67
+
control.close()
68
+
}
69
+
}, [dirty, control, cancelControl])
70
+
71
+
return (
72
+
<Dialog.Outer
73
+
control={control}
74
+
nativeOptions={{
75
+
preventDismiss: dirty,
76
+
minHeight: height,
77
+
}}
78
+
testID="createOrEditListDialog">
79
+
<DialogInner
80
+
list={list}
81
+
purpose={purpose}
82
+
onSave={onSave}
83
+
setDirty={setDirty}
84
+
onPressCancel={onPressCancel}
85
+
/>
86
+
87
+
<Prompt.Basic
88
+
control={cancelControl}
89
+
title={_(msg`Discard changes?`)}
90
+
description={_(msg`Are you sure you want to discard your changes?`)}
91
+
onConfirm={() => control.close()}
92
+
confirmButtonCta={_(msg`Discard`)}
93
+
confirmButtonColor="negative"
94
+
/>
95
+
</Dialog.Outer>
96
+
)
97
+
}
98
+
99
+
function DialogInner({
100
+
list,
101
+
purpose,
102
+
onSave,
103
+
setDirty,
104
+
onPressCancel,
105
+
}: {
106
+
list?: AppBskyGraphDefs.ListView
107
+
purpose?: AppBskyGraphDefs.ListPurpose
108
+
onSave?: (uri: string) => void
109
+
setDirty: (dirty: boolean) => void
110
+
onPressCancel: () => void
111
+
}) {
112
+
const activePurpose = useMemo(() => {
113
+
if (list?.purpose) {
114
+
return list.purpose
115
+
}
116
+
if (purpose) {
117
+
return purpose
118
+
}
119
+
return 'app.bsky.graph.defs#curatelist'
120
+
}, [list, purpose])
121
+
const isCurateList = activePurpose === 'app.bsky.graph.defs#curatelist'
122
+
123
+
const {_} = useLingui()
124
+
const t = useTheme()
125
+
const agent = useAgent()
126
+
const control = Dialog.useDialogContext()
127
+
const {
128
+
mutateAsync: createListMutation,
129
+
error: createListError,
130
+
isError: isCreateListError,
131
+
isPending: isCreatingList,
132
+
} = useListCreateMutation()
133
+
const {
134
+
mutateAsync: updateListMutation,
135
+
error: updateListError,
136
+
isError: isUpdateListError,
137
+
isPending: isUpdatingList,
138
+
} = useListMetadataMutation()
139
+
const [imageError, setImageError] = useState('')
140
+
const [displayNameTooShort, setDisplayNameTooShort] = useState(false)
141
+
const initialDisplayName = list?.name || ''
142
+
const [displayName, setDisplayName] = useState(initialDisplayName)
143
+
const initialDescription = list?.description || ''
144
+
const [descriptionRt, setDescriptionRt] = useState<RichTextAPI>(() => {
145
+
const text = list?.description
146
+
const facets = list?.descriptionFacets
147
+
148
+
if (!text || !facets) {
149
+
return new RichTextAPI({text: text || ''})
150
+
}
151
+
152
+
// We want to be working with a blank state here, so let's get the
153
+
// serialized version and turn it back into a RichText
154
+
const serialized = richTextToString(new RichTextAPI({text, facets}), false)
155
+
156
+
const richText = new RichTextAPI({text: serialized})
157
+
richText.detectFacetsWithoutResolution()
158
+
159
+
return richText
160
+
})
161
+
162
+
const [listAvatar, setListAvatar] = useState<string | undefined | null>(
163
+
list?.avatar,
164
+
)
165
+
const [newListAvatar, setNewListAvatar] = useState<
166
+
ImageMeta | undefined | null
167
+
>()
168
+
169
+
const dirty =
170
+
displayName !== initialDisplayName ||
171
+
descriptionRt.text !== initialDescription ||
172
+
listAvatar !== list?.avatar
173
+
174
+
useEffect(() => {
175
+
setDirty(dirty)
176
+
}, [dirty, setDirty])
177
+
178
+
const onSelectNewAvatar = useCallback(
179
+
(img: ImageMeta | null) => {
180
+
setImageError('')
181
+
if (img === null) {
182
+
setNewListAvatar(null)
183
+
setListAvatar(null)
184
+
return
185
+
}
186
+
try {
187
+
setNewListAvatar(img)
188
+
setListAvatar(img.path)
189
+
} catch (e: any) {
190
+
setImageError(cleanError(e))
191
+
}
192
+
},
193
+
[setNewListAvatar, setListAvatar, setImageError],
194
+
)
195
+
196
+
const onPressSave = useCallback(async () => {
197
+
setImageError('')
198
+
setDisplayNameTooShort(false)
199
+
try {
200
+
if (displayName.length === 0) {
201
+
setDisplayNameTooShort(true)
202
+
return
203
+
}
204
+
205
+
let richText = new RichTextAPI(
206
+
{text: descriptionRt.text.trimEnd()},
207
+
{cleanNewlines: true},
208
+
)
209
+
210
+
await richText.detectFacets(agent)
211
+
richText = shortenLinks(richText)
212
+
richText = stripInvalidMentions(richText)
213
+
214
+
if (list) {
215
+
await updateListMutation({
216
+
uri: list.uri,
217
+
name: displayName,
218
+
description: richText.text,
219
+
descriptionFacets: richText.facets,
220
+
avatar: newListAvatar,
221
+
})
222
+
Toast.show(
223
+
isCurateList
224
+
? _(msg({message: 'User list updated', context: 'toast'}))
225
+
: _(msg({message: 'Moderation list updated', context: 'toast'})),
226
+
)
227
+
control.close(() => onSave?.(list.uri))
228
+
} else {
229
+
const {uri} = await createListMutation({
230
+
purpose: activePurpose,
231
+
name: displayName,
232
+
description: richText.text,
233
+
descriptionFacets: richText.facets,
234
+
avatar: newListAvatar,
235
+
})
236
+
Toast.show(
237
+
isCurateList
238
+
? _(msg({message: 'User list created', context: 'toast'}))
239
+
: _(msg({message: 'Moderation list created', context: 'toast'})),
240
+
)
241
+
control.close(() => onSave?.(uri))
242
+
}
243
+
} catch (e: any) {
244
+
logger.error('Failed to create/edit list', {message: String(e)})
245
+
}
246
+
}, [
247
+
list,
248
+
createListMutation,
249
+
updateListMutation,
250
+
onSave,
251
+
control,
252
+
displayName,
253
+
descriptionRt,
254
+
newListAvatar,
255
+
setImageError,
256
+
activePurpose,
257
+
isCurateList,
258
+
agent,
259
+
_,
260
+
])
261
+
262
+
const displayNameTooLong = useWarnMaxGraphemeCount({
263
+
text: displayName,
264
+
maxCount: DISPLAY_NAME_MAX_GRAPHEMES,
265
+
})
266
+
const descriptionTooLong = useWarnMaxGraphemeCount({
267
+
text: descriptionRt,
268
+
maxCount: DESCRIPTION_MAX_GRAPHEMES,
269
+
})
270
+
271
+
const cancelButton = useCallback(
272
+
() => (
273
+
<Button
274
+
label={_(msg`Cancel`)}
275
+
onPress={onPressCancel}
276
+
size="small"
277
+
color="primary"
278
+
variant="ghost"
279
+
style={[a.rounded_full]}
280
+
testID="editProfileCancelBtn">
281
+
<ButtonText style={[a.text_md]}>
282
+
<Trans>Cancel</Trans>
283
+
</ButtonText>
284
+
</Button>
285
+
),
286
+
[onPressCancel, _],
287
+
)
288
+
289
+
const saveButton = useCallback(
290
+
() => (
291
+
<Button
292
+
label={_(msg`Save`)}
293
+
onPress={onPressSave}
294
+
disabled={
295
+
!dirty ||
296
+
isCreatingList ||
297
+
isUpdatingList ||
298
+
displayNameTooLong ||
299
+
descriptionTooLong
300
+
}
301
+
size="small"
302
+
color="primary"
303
+
variant="ghost"
304
+
style={[a.rounded_full]}
305
+
testID="editProfileSaveBtn">
306
+
<ButtonText style={[a.text_md, !dirty && t.atoms.text_contrast_low]}>
307
+
<Trans>Save</Trans>
308
+
</ButtonText>
309
+
{(isCreatingList || isUpdatingList) && <ButtonIcon icon={Loader} />}
310
+
</Button>
311
+
),
312
+
[
313
+
_,
314
+
t,
315
+
dirty,
316
+
onPressSave,
317
+
isCreatingList,
318
+
isUpdatingList,
319
+
displayNameTooLong,
320
+
descriptionTooLong,
321
+
],
322
+
)
323
+
324
+
const onChangeDisplayName = useCallback(
325
+
(text: string) => {
326
+
setDisplayName(text)
327
+
if (text.length > 0 && displayNameTooShort) {
328
+
setDisplayNameTooShort(false)
329
+
}
330
+
},
331
+
[displayNameTooShort],
332
+
)
333
+
334
+
const onChangeDescription = useCallback(
335
+
(newText: string) => {
336
+
const richText = new RichTextAPI({text: newText})
337
+
richText.detectFacetsWithoutResolution()
338
+
339
+
setDescriptionRt(richText)
340
+
},
341
+
[setDescriptionRt],
342
+
)
343
+
344
+
const title = list
345
+
? isCurateList
346
+
? _(msg`Edit user list`)
347
+
: _(msg`Edit moderation list`)
348
+
: isCurateList
349
+
? _(msg`Create user list`)
350
+
: _(msg`Create moderation list`)
351
+
352
+
return (
353
+
<Dialog.ScrollableInner
354
+
label={title}
355
+
style={[a.overflow_hidden, web({maxWidth: 500})]}
356
+
contentContainerStyle={[a.px_0, a.pt_0]}
357
+
header={
358
+
<Dialog.Header renderLeft={cancelButton} renderRight={saveButton}>
359
+
<Dialog.HeaderText>{title}</Dialog.HeaderText>
360
+
</Dialog.Header>
361
+
}>
362
+
{isUpdateListError && (
363
+
<ErrorMessage message={cleanError(updateListError)} />
364
+
)}
365
+
{isCreateListError && (
366
+
<ErrorMessage message={cleanError(createListError)} />
367
+
)}
368
+
{imageError !== '' && <ErrorMessage message={imageError} />}
369
+
<View style={[a.pt_xl, a.px_xl, a.gap_xl]}>
370
+
<View>
371
+
<TextField.LabelText>
372
+
<Trans>List avatar</Trans>
373
+
</TextField.LabelText>
374
+
<View style={[a.align_start]}>
375
+
<EditableUserAvatar
376
+
size={80}
377
+
avatar={listAvatar}
378
+
onSelectNewAvatar={onSelectNewAvatar}
379
+
type="list"
380
+
/>
381
+
</View>
382
+
</View>
383
+
<View>
384
+
<TextField.LabelText>
385
+
<Trans>List name</Trans>
386
+
</TextField.LabelText>
387
+
<TextField.Root isInvalid={displayNameTooLong || displayNameTooShort}>
388
+
<Dialog.Input
389
+
defaultValue={displayName}
390
+
onChangeText={onChangeDisplayName}
391
+
label={_(msg`Name`)}
392
+
placeholder={_(msg`e.g. Great Posters`)}
393
+
testID="editListNameInput"
394
+
/>
395
+
</TextField.Root>
396
+
{(displayNameTooLong || displayNameTooShort) && (
397
+
<Text
398
+
style={[
399
+
a.text_sm,
400
+
a.mt_xs,
401
+
a.font_bold,
402
+
{color: t.palette.negative_400},
403
+
]}>
404
+
{displayNameTooLong ? (
405
+
<Trans>
406
+
List name is too long.{' '}
407
+
<Plural
408
+
value={DISPLAY_NAME_MAX_GRAPHEMES}
409
+
other="The maximum number of characters is #."
410
+
/>
411
+
</Trans>
412
+
) : displayNameTooShort ? (
413
+
<Trans>List must have a name.</Trans>
414
+
) : null}
415
+
</Text>
416
+
)}
417
+
</View>
418
+
419
+
<View>
420
+
<TextField.LabelText>
421
+
<Trans>List description</Trans>
422
+
</TextField.LabelText>
423
+
<TextField.Root isInvalid={descriptionTooLong}>
424
+
<Dialog.Input
425
+
defaultValue={descriptionRt.text}
426
+
onChangeText={onChangeDescription}
427
+
multiline
428
+
label={_(msg`Description`)}
429
+
placeholder={_(msg`e.g. The posters that never miss.`)}
430
+
testID="editProfileDescriptionInput"
431
+
/>
432
+
</TextField.Root>
433
+
{descriptionTooLong && (
434
+
<Text
435
+
style={[
436
+
a.text_sm,
437
+
a.mt_xs,
438
+
a.font_bold,
439
+
{color: t.palette.negative_400},
440
+
]}>
441
+
<Trans>
442
+
List description is too long.{' '}
443
+
<Plural
444
+
value={DESCRIPTION_MAX_GRAPHEMES}
445
+
other="The maximum number of characters is #."
446
+
/>
447
+
</Trans>
448
+
</Text>
449
+
)}
450
+
</View>
451
+
</View>
452
+
</Dialog.ScrollableInner>
453
+
)
454
+
}
+9
-2
src/lib/strings/helpers.ts
+9
-2
src/lib/strings/helpers.ts
···
1
1
import {useCallback, useMemo} from 'react'
2
+
import {type RichText} from '@atproto/api'
2
3
import Graphemer from 'graphemer'
4
+
5
+
import {shortenLinks} from './rich-text-manip'
3
6
4
7
export function enforceLen(
5
8
str: string,
···
45
48
text,
46
49
maxCount,
47
50
}: {
48
-
text: string
51
+
text: string | RichText
49
52
maxCount: number
50
53
}) {
51
54
const splitter = useMemo(() => new Graphemer(), [])
52
55
53
56
return useMemo(() => {
54
-
return splitter.countGraphemes(text) > maxCount
57
+
if (typeof text === 'string') {
58
+
return splitter.countGraphemes(text) > maxCount
59
+
} else {
60
+
return shortenLinks(text).graphemeLength > maxCount
61
+
}
55
62
}, [splitter, maxCount, text])
56
63
}
57
64
+5
-7
src/screens/Profile/Header/EditProfileDialog.tsx
+5
-7
src/screens/Profile/Header/EditProfileDialog.tsx
···
1
1
import {useCallback, useEffect, useState} from 'react'
2
-
import {Dimensions, View} from 'react-native'
2
+
import {useWindowDimensions, View} from 'react-native'
3
3
import {type AppBskyActorDefs} from '@atproto/api'
4
4
import {msg, Plural, Trans} from '@lingui/macro'
5
5
import {useLingui} from '@lingui/react'
···
28
28
const DISPLAY_NAME_MAX_GRAPHEMES = 64
29
29
const DESCRIPTION_MAX_GRAPHEMES = 256
30
30
31
-
const SCREEN_HEIGHT = Dimensions.get('window').height
32
-
33
31
export function EditProfileDialog({
34
32
profile,
35
33
control,
···
42
40
const {_} = useLingui()
43
41
const cancelControl = Dialog.useDialogControl()
44
42
const [dirty, setDirty] = useState(false)
43
+
const {height} = useWindowDimensions()
45
44
46
45
const onPressCancel = useCallback(() => {
47
46
if (dirty) {
···
56
55
control={control}
57
56
nativeOptions={{
58
57
preventDismiss: dirty,
59
-
minHeight: SCREEN_HEIGHT,
58
+
minHeight: height,
60
59
}}
61
60
webOptions={{
62
61
onBackgroundPress: () => {
···
186
185
newUserAvatar,
187
186
newUserBanner,
188
187
})
189
-
onUpdate?.()
190
-
control.close()
188
+
control.close(() => onUpdate?.())
191
189
Toast.show(_(msg({message: 'Profile updated', context: 'toast'})))
192
190
} catch (e: any) {
193
191
logger.error('Failed to update user profile', {message: String(e)})
···
369
367
defaultValue={description}
370
368
onChangeText={setDescription}
371
369
multiline
372
-
label={_(msg`Display name`)}
370
+
label={_(msg`Description`)}
373
371
placeholder={_(msg`Tell us a bit about yourself`)}
374
372
testID="editProfileDescriptionInput"
375
373
/>
+5
-10
src/screens/ProfileList/components/MoreOptionsMenu.tsx
+5
-10
src/screens/ProfileList/components/MoreOptionsMenu.tsx
···
8
8
import {toShareUrl} from '#/lib/strings/url-helpers'
9
9
import {logger} from '#/logger'
10
10
import {isWeb} from '#/platform/detection'
11
-
import {useModalControls} from '#/state/modals'
12
11
import {
13
12
useListBlockMutation,
14
13
useListDeleteMutation,
···
18
17
import {useSession} from '#/state/session'
19
18
import {Button, ButtonIcon} from '#/components/Button'
20
19
import {useDialogControl} from '#/components/Dialog'
20
+
import {CreateOrEditListDialog} from '#/components/dialogs/lists/CreateOrEditListDialog'
21
21
import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ShareIcon} from '#/components/icons/ArrowOutOfBox'
22
22
import {ChainLink_Stroke2_Corner0_Rounded as ChainLink} from '#/components/icons/ChainLink'
23
23
import {DotGrid_Stroke2_Corner0_Rounded as DotGridIcon} from '#/components/icons/DotGrid'
···
44
44
}) {
45
45
const {_} = useLingui()
46
46
const {currentAccount} = useSession()
47
-
const {openModal} = useModalControls()
47
+
const editListDialogControl = useDialogControl()
48
48
const deleteListPromptControl = useDialogControl()
49
49
const reportDialogControl = useReportDialogControl()
50
50
const navigation = useNavigation<NavigationProp>()
···
80
80
}
81
81
}
82
82
83
-
const onPressEdit = () => {
84
-
openModal({
85
-
name: 'create-or-edit-list',
86
-
list,
87
-
})
88
-
}
89
-
90
83
const onPressDelete = async () => {
91
84
await deleteList({uri: list.uri})
92
85
···
201
194
<Menu.Group>
202
195
<Menu.Item
203
196
label={_(msg`Edit list details`)}
204
-
onPress={onPressEdit}>
197
+
onPress={editListDialogControl.open}>
205
198
<Menu.ItemText>
206
199
<Trans>Edit list details</Trans>
207
200
</Menu.ItemText>
···
274
267
)}
275
268
</Menu.Outer>
276
269
</Menu.Root>
270
+
271
+
<CreateOrEditListDialog control={editListDialogControl} list={list} />
277
272
278
273
<Prompt.Basic
279
274
control={deleteListPromptControl}
-9
src/state/modals/index.tsx
-9
src/state/modals/index.tsx
···
1
1
import React from 'react'
2
-
import {type AppBskyGraphDefs} from '@atproto/api'
3
2
4
3
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
5
-
6
-
export interface CreateOrEditListModal {
7
-
name: 'create-or-edit-list'
8
-
purpose?: string
9
-
list?: AppBskyGraphDefs.ListView
10
-
onSave?: (uri: string) => void
11
-
}
12
4
13
5
export interface UserAddRemoveListsModal {
14
6
name: 'user-add-remove-lists'
···
46
38
| ContentLanguagesSettingsModal
47
39
48
40
// Lists
49
-
| CreateOrEditListModal
50
41
| UserAddRemoveListsModal
51
42
52
43
// Bluesky access
+1
-1
src/view/com/composer/photos/EditImageDialog.web.tsx
+1
-1
src/view/com/composer/photos/EditImageDialog.web.tsx
+1
-5
src/view/com/modals/Modal.tsx
+1
-5
src/view/com/modals/Modal.tsx
···
7
7
import {useModalControls, useModals} from '#/state/modals'
8
8
import {FullWindowOverlay} from '#/components/FullWindowOverlay'
9
9
import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
10
-
import * as CreateOrEditListModal from './CreateOrEditList'
11
10
import * as DeleteAccountModal from './DeleteAccount'
12
11
import * as InviteCodesModal from './InviteCodes'
13
12
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
···
44
43
45
44
let snapPoints: (string | number)[] = DEFAULT_SNAPPOINTS
46
45
let element
47
-
if (activeModal?.name === 'create-or-edit-list') {
48
-
snapPoints = CreateOrEditListModal.snapPoints
49
-
element = <CreateOrEditListModal.Component {...activeModal} />
50
-
} else if (activeModal?.name === 'user-add-remove-lists') {
46
+
if (activeModal?.name === 'user-add-remove-lists') {
51
47
snapPoints = UserAddRemoveListsModal.snapPoints
52
48
element = <UserAddRemoveListsModal.Component {...activeModal} />
53
49
} else if (activeModal?.name === 'delete-account') {
+1
-4
src/view/com/modals/Modal.web.tsx
+1
-4
src/view/com/modals/Modal.web.tsx
···
6
6
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
7
7
import {type Modal as ModalIface} from '#/state/modals'
8
8
import {useModalControls, useModals} from '#/state/modals'
9
-
import * as CreateOrEditListModal from './CreateOrEditList'
10
9
import * as DeleteAccountModal from './DeleteAccount'
11
10
import * as InviteCodesModal from './InviteCodes'
12
11
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
···
48
47
}
49
48
50
49
let element
51
-
if (modal.name === 'create-or-edit-list') {
52
-
element = <CreateOrEditListModal.Component {...modal} />
53
-
} else if (modal.name === 'user-add-remove-lists') {
50
+
if (modal.name === 'user-add-remove-lists') {
54
51
element = <UserAddRemoveLists.Component {...modal} />
55
52
} else if (modal.name === 'delete-account') {
56
53
element = <DeleteAccountModal.Component />
+28
-19
src/view/screens/Lists.tsx
+28
-19
src/view/screens/Lists.tsx
···
1
-
import React from 'react'
1
+
import {useCallback} from 'react'
2
2
import {AtUri} from '@atproto/api'
3
3
import {msg, Trans} from '@lingui/macro'
4
4
import {useLingui} from '@lingui/react'
···
10
10
type NativeStackScreenProps,
11
11
} from '#/lib/routes/types'
12
12
import {type NavigationProp} from '#/lib/routes/types'
13
-
import {useModalControls} from '#/state/modals'
14
13
import {useSetMinimalShellMode} from '#/state/shell'
15
14
import {MyLists} from '#/view/com/lists/MyLists'
16
15
import {atoms as a} from '#/alf'
17
16
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
17
+
import {useDialogControl} from '#/components/Dialog'
18
+
import {CreateOrEditListDialog} from '#/components/dialogs/lists/CreateOrEditListDialog'
18
19
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
19
20
import * as Layout from '#/components/Layout'
20
21
···
23
24
const {_} = useLingui()
24
25
const setMinimalShellMode = useSetMinimalShellMode()
25
26
const navigation = useNavigation<NavigationProp>()
26
-
const {openModal} = useModalControls()
27
27
const requireEmailVerification = useRequireEmailVerification()
28
+
const createListDialogControl = useDialogControl()
28
29
29
30
useFocusEffect(
30
-
React.useCallback(() => {
31
+
useCallback(() => {
31
32
setMinimalShellMode(false)
32
33
}, [setMinimalShellMode]),
33
34
)
34
35
35
-
const onPressNewList = React.useCallback(() => {
36
-
openModal({
37
-
name: 'create-or-edit-list',
38
-
purpose: 'app.bsky.graph.defs#curatelist',
39
-
onSave: (uri: string) => {
40
-
try {
41
-
const urip = new AtUri(uri)
42
-
navigation.navigate('ProfileList', {
43
-
name: urip.hostname,
44
-
rkey: urip.rkey,
45
-
})
46
-
} catch {}
47
-
},
48
-
})
49
-
}, [openModal, navigation])
36
+
const onPressNewList = useCallback(() => {
37
+
createListDialogControl.open()
38
+
}, [createListDialogControl])
50
39
51
40
const wrappedOnPressNewList = requireEmailVerification(onPressNewList, {
52
41
instructions: [
···
56
45
],
57
46
})
58
47
48
+
const onCreateList = useCallback(
49
+
(uri: string) => {
50
+
try {
51
+
const urip = new AtUri(uri)
52
+
navigation.navigate('ProfileList', {
53
+
name: urip.hostname,
54
+
rkey: urip.rkey,
55
+
})
56
+
} catch {}
57
+
},
58
+
[navigation],
59
+
)
60
+
59
61
return (
60
62
<Layout.Screen testID="listsScreen">
61
63
<Layout.Header.Outer>
···
78
80
</ButtonText>
79
81
</Button>
80
82
</Layout.Header.Outer>
83
+
81
84
<MyLists filter="curate" style={a.flex_grow} />
85
+
86
+
<CreateOrEditListDialog
87
+
purpose="app.bsky.graph.defs#curatelist"
88
+
control={createListDialogControl}
89
+
onSave={onCreateList}
90
+
/>
82
91
</Layout.Screen>
83
92
)
84
93
}
+28
-19
src/view/screens/ModerationModlists.tsx
+28
-19
src/view/screens/ModerationModlists.tsx
···
1
-
import React from 'react'
1
+
import {useCallback} from 'react'
2
2
import {AtUri} from '@atproto/api'
3
3
import {msg, Trans} from '@lingui/macro'
4
4
import {useLingui} from '@lingui/react'
···
10
10
type NativeStackScreenProps,
11
11
} from '#/lib/routes/types'
12
12
import {type NavigationProp} from '#/lib/routes/types'
13
-
import {useModalControls} from '#/state/modals'
14
13
import {useSetMinimalShellMode} from '#/state/shell'
15
14
import {MyLists} from '#/view/com/lists/MyLists'
16
15
import {atoms as a} from '#/alf'
17
16
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
17
+
import {useDialogControl} from '#/components/Dialog'
18
+
import {CreateOrEditListDialog} from '#/components/dialogs/lists/CreateOrEditListDialog'
18
19
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
19
20
import * as Layout from '#/components/Layout'
20
21
···
23
24
const {_} = useLingui()
24
25
const setMinimalShellMode = useSetMinimalShellMode()
25
26
const navigation = useNavigation<NavigationProp>()
26
-
const {openModal} = useModalControls()
27
27
const requireEmailVerification = useRequireEmailVerification()
28
+
const createListDialogControl = useDialogControl()
28
29
29
30
useFocusEffect(
30
-
React.useCallback(() => {
31
+
useCallback(() => {
31
32
setMinimalShellMode(false)
32
33
}, [setMinimalShellMode]),
33
34
)
34
35
35
-
const onPressNewList = React.useCallback(() => {
36
-
openModal({
37
-
name: 'create-or-edit-list',
38
-
purpose: 'app.bsky.graph.defs#modlist',
39
-
onSave: (uri: string) => {
40
-
try {
41
-
const urip = new AtUri(uri)
42
-
navigation.navigate('ProfileList', {
43
-
name: urip.hostname,
44
-
rkey: urip.rkey,
45
-
})
46
-
} catch {}
47
-
},
48
-
})
49
-
}, [openModal, navigation])
36
+
const onPressNewList = useCallback(() => {
37
+
createListDialogControl.open()
38
+
}, [createListDialogControl])
50
39
51
40
const wrappedOnPressNewList = requireEmailVerification(onPressNewList, {
52
41
instructions: [
···
56
45
],
57
46
})
58
47
48
+
const onCreateList = useCallback(
49
+
(uri: string) => {
50
+
try {
51
+
const urip = new AtUri(uri)
52
+
navigation.navigate('ProfileList', {
53
+
name: urip.hostname,
54
+
rkey: urip.rkey,
55
+
})
56
+
} catch {}
57
+
},
58
+
[navigation],
59
+
)
60
+
59
61
return (
60
62
<Layout.Screen testID="moderationModlistsScreen">
61
63
<Layout.Header.Outer>
···
78
80
</ButtonText>
79
81
</Button>
80
82
</Layout.Header.Outer>
83
+
81
84
<MyLists filter="mod" style={a.flex_grow} />
85
+
86
+
<CreateOrEditListDialog
87
+
purpose="app.bsky.graph.defs#modlist"
88
+
control={createListDialogControl}
89
+
onSave={onCreateList}
90
+
/>
82
91
</Layout.Screen>
83
92
)
84
93
}