mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at remove-hackfix 393 lines 11 kB view raw
1import {useCallback, useEffect, useState} from 'react' 2import {useWindowDimensions, View} from 'react-native' 3import {type AppBskyActorDefs} from '@atproto/api' 4import {msg, Plural, Trans} from '@lingui/macro' 5import {useLingui} from '@lingui/react' 6 7import {urls} from '#/lib/constants' 8import {cleanError} from '#/lib/strings/errors' 9import {useWarnMaxGraphemeCount} from '#/lib/strings/helpers' 10import {logger} from '#/logger' 11import {type ImageMeta} from '#/state/gallery' 12import {useProfileUpdateMutation} from '#/state/queries/profile' 13import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 14import * as Toast from '#/view/com/util/Toast' 15import {EditableUserAvatar} from '#/view/com/util/UserAvatar' 16import {UserBanner} from '#/view/com/util/UserBanner' 17import {atoms as a, useTheme} from '#/alf' 18import {Admonition} from '#/components/Admonition' 19import {Button, ButtonIcon, ButtonText} from '#/components/Button' 20import * as Dialog from '#/components/Dialog' 21import * as TextField from '#/components/forms/TextField' 22import {InlineLinkText} from '#/components/Link' 23import {Loader} from '#/components/Loader' 24import * as Prompt from '#/components/Prompt' 25import {Text} from '#/components/Typography' 26import {useSimpleVerificationState} from '#/components/verification' 27 28const DISPLAY_NAME_MAX_GRAPHEMES = 64 29const DESCRIPTION_MAX_GRAPHEMES = 256 30 31export function EditProfileDialog({ 32 profile, 33 control, 34 onUpdate, 35}: { 36 profile: AppBskyActorDefs.ProfileViewDetailed 37 control: Dialog.DialogControlProps 38 onUpdate?: () => void 39}) { 40 const {_} = useLingui() 41 const cancelControl = Dialog.useDialogControl() 42 const [dirty, setDirty] = useState(false) 43 const {height} = useWindowDimensions() 44 45 const onPressCancel = useCallback(() => { 46 if (dirty) { 47 cancelControl.open() 48 } else { 49 control.close() 50 } 51 }, [dirty, control, cancelControl]) 52 53 return ( 54 <Dialog.Outer 55 control={control} 56 nativeOptions={{ 57 preventDismiss: dirty, 58 minHeight: height, 59 }} 60 webOptions={{ 61 onBackgroundPress: () => { 62 if (dirty) { 63 cancelControl.open() 64 } else { 65 control.close() 66 } 67 }, 68 }} 69 testID="editProfileModal"> 70 <DialogInner 71 profile={profile} 72 onUpdate={onUpdate} 73 setDirty={setDirty} 74 onPressCancel={onPressCancel} 75 /> 76 77 <Prompt.Basic 78 control={cancelControl} 79 title={_(msg`Discard changes?`)} 80 description={_(msg`Are you sure you want to discard your changes?`)} 81 onConfirm={() => control.close()} 82 confirmButtonCta={_(msg`Discard`)} 83 confirmButtonColor="negative" 84 /> 85 </Dialog.Outer> 86 ) 87} 88 89function DialogInner({ 90 profile, 91 onUpdate, 92 setDirty, 93 onPressCancel, 94}: { 95 profile: AppBskyActorDefs.ProfileViewDetailed 96 onUpdate?: () => void 97 setDirty: (dirty: boolean) => void 98 onPressCancel: () => void 99}) { 100 const {_} = useLingui() 101 const t = useTheme() 102 const control = Dialog.useDialogContext() 103 const verification = useSimpleVerificationState({ 104 profile, 105 }) 106 const { 107 mutateAsync: updateProfileMutation, 108 error: updateProfileError, 109 isError: isUpdateProfileError, 110 isPending: isUpdatingProfile, 111 } = useProfileUpdateMutation() 112 const [imageError, setImageError] = useState('') 113 const initialDisplayName = profile.displayName || '' 114 const [displayName, setDisplayName] = useState(initialDisplayName) 115 const initialDescription = profile.description || '' 116 const [description, setDescription] = useState(initialDescription) 117 const [userBanner, setUserBanner] = useState<string | undefined | null>( 118 profile.banner, 119 ) 120 const [userAvatar, setUserAvatar] = useState<string | undefined | null>( 121 profile.avatar, 122 ) 123 const [newUserBanner, setNewUserBanner] = useState< 124 ImageMeta | undefined | null 125 >() 126 const [newUserAvatar, setNewUserAvatar] = useState< 127 ImageMeta | undefined | null 128 >() 129 130 const dirty = 131 displayName !== initialDisplayName || 132 description !== initialDescription || 133 userAvatar !== profile.avatar || 134 userBanner !== profile.banner 135 136 useEffect(() => { 137 setDirty(dirty) 138 }, [dirty, setDirty]) 139 140 const onSelectNewAvatar = useCallback( 141 (img: ImageMeta | null) => { 142 setImageError('') 143 if (img === null) { 144 setNewUserAvatar(null) 145 setUserAvatar(null) 146 return 147 } 148 try { 149 setNewUserAvatar(img) 150 setUserAvatar(img.path) 151 } catch (e: any) { 152 setImageError(cleanError(e)) 153 } 154 }, 155 [setNewUserAvatar, setUserAvatar, setImageError], 156 ) 157 158 const onSelectNewBanner = useCallback( 159 (img: ImageMeta | null) => { 160 setImageError('') 161 if (!img) { 162 setNewUserBanner(null) 163 setUserBanner(null) 164 return 165 } 166 try { 167 setNewUserBanner(img) 168 setUserBanner(img.path) 169 } catch (e: any) { 170 setImageError(cleanError(e)) 171 } 172 }, 173 [setNewUserBanner, setUserBanner, setImageError], 174 ) 175 176 const onPressSave = useCallback(async () => { 177 setImageError('') 178 try { 179 await updateProfileMutation({ 180 profile, 181 updates: { 182 displayName: displayName.trimEnd(), 183 description: description.trimEnd(), 184 }, 185 newUserAvatar, 186 newUserBanner, 187 }) 188 control.close(() => onUpdate?.()) 189 Toast.show(_(msg({message: 'Profile updated', context: 'toast'}))) 190 } catch (e: any) { 191 logger.error('Failed to update user profile', {message: String(e)}) 192 } 193 }, [ 194 updateProfileMutation, 195 profile, 196 onUpdate, 197 control, 198 displayName, 199 description, 200 newUserAvatar, 201 newUserBanner, 202 setImageError, 203 _, 204 ]) 205 206 const displayNameTooLong = useWarnMaxGraphemeCount({ 207 text: displayName, 208 maxCount: DISPLAY_NAME_MAX_GRAPHEMES, 209 }) 210 const descriptionTooLong = useWarnMaxGraphemeCount({ 211 text: description, 212 maxCount: DESCRIPTION_MAX_GRAPHEMES, 213 }) 214 215 const cancelButton = useCallback( 216 () => ( 217 <Button 218 label={_(msg`Cancel`)} 219 onPress={onPressCancel} 220 size="small" 221 color="primary" 222 variant="ghost" 223 style={[a.rounded_full]} 224 testID="editProfileCancelBtn"> 225 <ButtonText style={[a.text_md]}> 226 <Trans>Cancel</Trans> 227 </ButtonText> 228 </Button> 229 ), 230 [onPressCancel, _], 231 ) 232 233 const saveButton = useCallback( 234 () => ( 235 <Button 236 label={_(msg`Save`)} 237 onPress={onPressSave} 238 disabled={ 239 !dirty || 240 isUpdatingProfile || 241 displayNameTooLong || 242 descriptionTooLong 243 } 244 size="small" 245 color="primary" 246 variant="ghost" 247 style={[a.rounded_full]} 248 testID="editProfileSaveBtn"> 249 <ButtonText style={[a.text_md, !dirty && t.atoms.text_contrast_low]}> 250 <Trans>Save</Trans> 251 </ButtonText> 252 {isUpdatingProfile && <ButtonIcon icon={Loader} />} 253 </Button> 254 ), 255 [ 256 _, 257 t, 258 dirty, 259 onPressSave, 260 isUpdatingProfile, 261 displayNameTooLong, 262 descriptionTooLong, 263 ], 264 ) 265 266 return ( 267 <Dialog.ScrollableInner 268 label={_(msg`Edit profile`)} 269 style={[a.overflow_hidden]} 270 contentContainerStyle={[a.px_0, a.pt_0]} 271 header={ 272 <Dialog.Header renderLeft={cancelButton} renderRight={saveButton}> 273 <Dialog.HeaderText> 274 <Trans>Edit profile</Trans> 275 </Dialog.HeaderText> 276 </Dialog.Header> 277 }> 278 <View style={[a.relative]}> 279 <UserBanner banner={userBanner} onSelectNewBanner={onSelectNewBanner} /> 280 <View 281 style={[ 282 a.absolute, 283 { 284 top: 80, 285 left: 20, 286 width: 84, 287 height: 84, 288 borderWidth: 2, 289 borderRadius: 42, 290 borderColor: t.atoms.bg.backgroundColor, 291 }, 292 ]}> 293 <EditableUserAvatar 294 size={80} 295 avatar={userAvatar} 296 onSelectNewAvatar={onSelectNewAvatar} 297 /> 298 </View> 299 </View> 300 {isUpdateProfileError && ( 301 <View style={[a.mt_xl]}> 302 <ErrorMessage message={cleanError(updateProfileError)} /> 303 </View> 304 )} 305 {imageError !== '' && ( 306 <View style={[a.mt_xl]}> 307 <ErrorMessage message={imageError} /> 308 </View> 309 )} 310 <View style={[a.mt_4xl, a.px_xl, a.gap_xl]}> 311 <View> 312 <TextField.LabelText> 313 <Trans>Display name</Trans> 314 </TextField.LabelText> 315 <TextField.Root isInvalid={displayNameTooLong}> 316 <Dialog.Input 317 defaultValue={displayName} 318 onChangeText={setDisplayName} 319 label={_(msg`Display name`)} 320 placeholder={_(msg`e.g. Alice Lastname`)} 321 testID="editProfileDisplayNameInput" 322 /> 323 </TextField.Root> 324 {displayNameTooLong && ( 325 <Text 326 style={[ 327 a.text_sm, 328 a.mt_xs, 329 a.font_semi_bold, 330 {color: t.palette.negative_400}, 331 ]}> 332 <Plural 333 value={DISPLAY_NAME_MAX_GRAPHEMES} 334 other="Display name is too long. The maximum number of characters is #." 335 /> 336 </Text> 337 )} 338 </View> 339 340 {verification.isVerified && 341 verification.role === 'default' && 342 displayName !== initialDisplayName && ( 343 <Admonition type="error"> 344 <Trans> 345 You are verified. You will lose your verification status if you 346 change your display name.{' '} 347 <InlineLinkText 348 label={_( 349 msg({ 350 message: `Learn more`, 351 context: `english-only-resource`, 352 }), 353 )} 354 to={urls.website.blog.initialVerificationAnnouncement}> 355 <Trans context="english-only-resource">Learn more.</Trans> 356 </InlineLinkText> 357 </Trans> 358 </Admonition> 359 )} 360 361 <View> 362 <TextField.LabelText> 363 <Trans>Description</Trans> 364 </TextField.LabelText> 365 <TextField.Root isInvalid={descriptionTooLong}> 366 <Dialog.Input 367 defaultValue={description} 368 onChangeText={setDescription} 369 multiline 370 label={_(msg`Description`)} 371 placeholder={_(msg`Tell us a bit about yourself`)} 372 testID="editProfileDescriptionInput" 373 /> 374 </TextField.Root> 375 {descriptionTooLong && ( 376 <Text 377 style={[ 378 a.text_sm, 379 a.mt_xs, 380 a.font_semi_bold, 381 {color: t.palette.negative_400}, 382 ]}> 383 <Plural 384 value={DESCRIPTION_MAX_GRAPHEMES} 385 other="Description is too long. The maximum number of characters is #." 386 /> 387 </Text> 388 )} 389 </View> 390 </View> 391 </Dialog.ScrollableInner> 392 ) 393}