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,6 +187,13 @@ 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 {
@@ -190,7 +202,8 @@ function DialogInner({
190
202
updates : {
191
203
displayName : displayName . trimEnd ( ) ,
192
204
description : description . trimEnd ( ) ,
193
- pronouns : pronouns . trimEnd ( ) ,
205
+ pronouns : pronouns . trimEnd ( ) . toLowerCase ( ) ,
206
+ website : website . trimEnd ( ) . toLowerCase ( ) ,
194
207
} ,
195
208
newUserAvatar,
196
209
newUserBanner,
@@ -209,6 +222,7 @@ function DialogInner({
209
222
displayName ,
210
223
description ,
211
224
pronouns ,
225
+ website ,
212
226
newUserAvatar ,
213
227
newUserBanner ,
214
228
setImageError ,
@@ -227,7 +241,7 @@ function DialogInner({
227
241
text : website ,
228
242
maxCount : WEBSITE_MAX_GRAPHEMES ,
229
243
} )
230
- const websiteInvalidFormat = ! ! ( website && ! website . match ( / ^ h t t p s ? : \/ \/ . + / ) )
244
+ const websiteInvalidFormat = ! isValidWebsiteFormat ( website )
231
245
const descriptionTooLong = useWarnMaxGraphemeCount ( {
232
246
text : description ,
233
247
maxCount : DESCRIPTION_MAX_GRAPHEMES ,
@@ -448,15 +462,63 @@ function DialogInner({
448
462
< TextField . LabelText >
449
463
< Trans > Website</ Trans >
450
464
</ 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 >
465
+ < View style = { [ a . w_full , a . relative ] } >
466
+ < TextField . Root isInvalid = { websiteTooLong || websiteInvalidFormat } >
467
+ { website && < TextField . Icon icon = { Globe } /> }
468
+ < Dialog . Input
469
+ inputRef = { websiteInputRef }
470
+ defaultValue = { website }
471
+ onChangeText = { setWebsite }
472
+ label = { _ ( msg `EditWebsite` ) }
473
+ placeholder = { _ ( msg `URL` ) }
474
+ testID = "editProfileWebsiteInput"
475
+ autoCapitalize = "none"
476
+ keyboardType = "url"
477
+ style = { [
478
+ website
479
+ ? {
480
+ paddingRight : tokens . space . _5xl ,
481
+ }
482
+ : { } ,
483
+ ] }
484
+ />
485
+ </ TextField . Root >
486
+
487
+ { website && (
488
+ < View
489
+ style = { [
490
+ a . absolute ,
491
+ a . z_10 ,
492
+ a . my_auto ,
493
+ a . inset_0 ,
494
+ a . justify_center ,
495
+ a . pr_sm ,
496
+ { left : 'auto' } ,
497
+ ] } >
498
+ < Pressable
499
+ testID = "clearWebsiteBtn"
500
+ onPress = { onClearWebsite }
501
+ accessibilityLabel = { _ ( msg `Clear website` ) }
502
+ accessibilityHint = { _ ( msg `Removes the website URL` ) }
503
+ hitSlop = { HITSLOP_10 }
504
+ style = { [
505
+ a . flex_row ,
506
+ a . align_center ,
507
+ a . justify_center ,
508
+ {
509
+ width : tokens . space . _2xl ,
510
+ height : tokens . space . _2xl ,
511
+ } ,
512
+ a . rounded_full ,
513
+ ] } >
514
+ < CircleX
515
+ width = { tokens . space . lg }
516
+ style = { { color : t . palette . contrast_600 } }
517
+ />
518
+ </ Pressable >
519
+ </ View >
520
+ ) }
521
+ </ View >
460
522
{ websiteTooLong && (
461
523
< Text
462
524
style = { [
@@ -479,7 +541,9 @@ function DialogInner({
479
541
a . font_bold ,
480
542
{ color : t . palette . negative_400 } ,
481
543
] } >
482
- < Trans > Website must start with http:// or https://</ Trans >
544
+ < Trans >
545
+ Website must be a valid URL (e.g., https://bsky.app)
546
+ </ Trans >
483
547
</ Text >
484
548
) }
485
549
</ View >
0 commit comments