1
- import { useCallback , useEffect , useState } from 'react'
2
- import { Dimensions , View } from 'react-native'
1
+ import { useCallback , useEffect , useRef , useState } from 'react'
2
+ import { Dimensions , Pressable , 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'
6
6
7
- import { urls } from '#/lib/constants'
7
+ import { HITSLOP_10 , urls } from '#/lib/constants'
8
8
import { cleanError } from '#/lib/strings/errors'
9
9
import { useWarnMaxGraphemeCount } from '#/lib/strings/helpers'
10
+ import { isValidWebsiteFormat } from '#/lib/strings/website'
10
11
import { logger } from '#/logger'
11
12
import { type ImageMeta } from '#/state/gallery'
12
13
import { useProfileUpdateMutation } from '#/state/queries/profile'
@@ -15,19 +16,22 @@ import * as Toast from '#/view/com/util/Toast'
15
16
import { EditableUserAvatar } from '#/view/com/util/UserAvatar'
16
17
import { UserBanner } from '#/view/com/util/UserBanner'
17
18
import { atoms as a , useTheme } from '#/alf'
19
+ import * as tokens from '#/alf/tokens'
18
20
import { Admonition } from '#/components/Admonition'
19
21
import { Button , ButtonIcon , ButtonText } from '#/components/Button'
20
22
import * as Dialog from '#/components/Dialog'
21
23
import * as TextField from '#/components/forms/TextField'
24
+ import { CircleX_Stroke2_Corner0_Rounded as CircleX } from '#/components/icons/CircleX'
25
+ import { Globe_Stroke2_Corner0_Rounded as Globe } from '#/components/icons/Globe'
22
26
import { InlineLinkText } from '#/components/Link'
23
27
import { Loader } from '#/components/Loader'
24
28
import * as Prompt from '#/components/Prompt'
25
29
import { Text } from '#/components/Typography'
26
30
import { useSimpleVerificationState } from '#/components/verification'
27
31
28
32
const DISPLAY_NAME_MAX_GRAPHEMES = 64
29
- const PRONOUNS_MAX_GRAPHEMES = 50
30
- const WEBSITE_MAX_GRAPHEMES = 300
33
+ const PRONOUNS_MAX_GRAPHEMES = 20
34
+ const WEBSITE_MAX_GRAPHEMES = 28
31
35
const DESCRIPTION_MAX_GRAPHEMES = 256
32
36
33
37
const SCREEN_HEIGHT = Dimensions . get ( 'window' ) . height
@@ -117,10 +121,11 @@ function DialogInner({
117
121
const [ displayName , setDisplayName ] = useState ( initialDisplayName )
118
122
const initialDescription = profile . description || ''
119
123
const [ description , setDescription ] = useState ( initialDescription )
120
- const initialPronouns = ''
124
+ const initialPronouns = profile . pronouns || ''
121
125
const [ pronouns , setPronouns ] = useState ( initialPronouns )
122
- const initialWebsite = ''
126
+ const initialWebsite = profile . website || ''
123
127
const [ website , setWebsite ] = useState ( initialWebsite )
128
+ const websiteInputRef = useRef < any > ( null )
124
129
const [ userBanner , setUserBanner ] = useState < string | undefined | null > (
125
130
profile . banner ,
126
131
)
@@ -182,15 +187,26 @@ function DialogInner({
182
187
[ setNewUserBanner , setUserBanner , setImageError ] ,
183
188
)
184
189
190
+ const onClearWebsite = useCallback ( ( ) => {
191
+ setWebsite ( '' )
192
+ if ( websiteInputRef . current ) {
193
+ websiteInputRef . current . clear ( )
194
+ }
195
+ } , [ setWebsite ] )
196
+
185
197
const onPressSave = useCallback ( async ( ) => {
186
198
setImageError ( '' )
187
199
try {
200
+ const trimmedWebsite = website . trimEnd ( ) . toLowerCase ( )
201
+ const websiteToSave = trimmedWebsite || undefined
202
+
188
203
await updateProfileMutation ( {
189
204
profile,
190
205
updates : {
191
206
displayName : displayName . trimEnd ( ) ,
192
207
description : description . trimEnd ( ) ,
193
- pronouns : pronouns . trimEnd ( ) ,
208
+ pronouns : pronouns . trimEnd ( ) . toLowerCase ( ) ,
209
+ website : websiteToSave ,
194
210
} ,
195
211
newUserAvatar,
196
212
newUserBanner,
@@ -209,6 +225,7 @@ function DialogInner({
209
225
displayName ,
210
226
description ,
211
227
pronouns ,
228
+ website ,
212
229
newUserAvatar ,
213
230
newUserBanner ,
214
231
setImageError ,
@@ -227,7 +244,7 @@ function DialogInner({
227
244
text : website ,
228
245
maxCount : WEBSITE_MAX_GRAPHEMES ,
229
246
} )
230
- const websiteInvalidFormat = ! ! ( website && ! website . match ( / ^ h t t p s ? : \/ \/ . + / ) )
247
+ const websiteInvalidFormat = ! isValidWebsiteFormat ( website )
231
248
const descriptionTooLong = useWarnMaxGraphemeCount ( {
232
249
text : description ,
233
250
maxCount : DESCRIPTION_MAX_GRAPHEMES ,
@@ -448,15 +465,63 @@ function DialogInner({
448
465
< TextField . LabelText >
449
466
< Trans > Website</ Trans >
450
467
</ TextField . LabelText >
451
- < TextField . Root isInvalid = { websiteTooLong || websiteInvalidFormat } >
452
- < Dialog . Input
453
- defaultValue = { website }
454
- onChangeText = { setWebsite }
455
- label = { _ ( msg `Website` ) }
456
- placeholder = { _ ( msg `URL` ) }
457
- testID = "editProfileWebsiteInput"
458
- />
459
- </ TextField . Root >
468
+ < View style = { [ a . w_full , a . relative ] } >
469
+ < TextField . Root isInvalid = { websiteTooLong || websiteInvalidFormat } >
470
+ { website && < TextField . Icon icon = { Globe } /> }
471
+ < Dialog . Input
472
+ inputRef = { websiteInputRef }
473
+ defaultValue = { website }
474
+ onChangeText = { setWebsite }
475
+ label = { _ ( msg `EditWebsite` ) }
476
+ placeholder = { _ ( msg `URL` ) }
477
+ testID = "editProfileWebsiteInput"
478
+ autoCapitalize = "none"
479
+ keyboardType = "url"
480
+ style = { [
481
+ website
482
+ ? {
483
+ paddingRight : tokens . space . _5xl ,
484
+ }
485
+ : { } ,
486
+ ] }
487
+ />
488
+ </ TextField . Root >
489
+
490
+ { website && (
491
+ < View
492
+ style = { [
493
+ a . absolute ,
494
+ a . z_10 ,
495
+ a . my_auto ,
496
+ a . inset_0 ,
497
+ a . justify_center ,
498
+ a . pr_sm ,
499
+ { left : 'auto' } ,
500
+ ] } >
501
+ < Pressable
502
+ testID = "clearWebsiteBtn"
503
+ onPress = { onClearWebsite }
504
+ accessibilityLabel = { _ ( msg `Clear website` ) }
505
+ accessibilityHint = { _ ( msg `Removes the website URL` ) }
506
+ hitSlop = { HITSLOP_10 }
507
+ style = { [
508
+ a . flex_row ,
509
+ a . align_center ,
510
+ a . justify_center ,
511
+ {
512
+ width : tokens . space . _2xl ,
513
+ height : tokens . space . _2xl ,
514
+ } ,
515
+ a . rounded_full ,
516
+ ] } >
517
+ < CircleX
518
+ width = { tokens . space . lg }
519
+ style = { { color : t . palette . contrast_600 } }
520
+ />
521
+ </ Pressable >
522
+ </ View >
523
+ ) }
524
+ </ View >
460
525
{ websiteTooLong && (
461
526
< Text
462
527
style = { [
@@ -479,7 +544,9 @@ function DialogInner({
479
544
a . font_bold ,
480
545
{ color : t . palette . negative_400 } ,
481
546
] } >
482
- < Trans > Website must start with http:// or https://</ Trans >
547
+ < Trans >
548
+ Website must be a valid URL (e.g., https://bsky.app)
549
+ </ Trans >
483
550
</ Text >
484
551
) }
485
552
</ View >
0 commit comments