deer social fork for personal usage. but you might see a use idk. github mirror

Modernise list create/edit dialog (#8223)

authored by samuel.fm and committed by GitHub e90cfdc8 3bc906b9

Changed files
+536 -77
src
components
Dialog
dialogs
lib
strings
screens
Profile
ProfileList
components
state
modals
view
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 19 19 20 20 export function EditImageDialog(props: EditImageDialogProps) { 21 21 return ( 22 - <Dialog.Outer control={props.control}> 22 + <Dialog.Outer control={props.control} webOptions={{alignCenter: true}}> 23 23 <Dialog.Handle /> 24 24 <DialogInner {...props} /> 25 25 </Dialog.Outer>
+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
··· 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
··· 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
··· 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 }