+13
src/lib/strings/pronouns.ts
+13
src/lib/strings/pronouns.ts
···
1
+
import {forceLTR} from './bidi'
2
+
3
+
export function sanitizePronouns(
4
+
pronouns: string,
5
+
forceLeftToRight = true,
6
+
): string {
7
+
if (!pronouns || pronouns.trim() === '') {
8
+
return ''
9
+
}
10
+
11
+
const trimmed = pronouns.trim().toLowerCase()
12
+
return forceLeftToRight ? forceLTR(trimmed) : trimmed
13
+
}
+8
src/lib/strings/time.ts
+8
src/lib/strings/time.ts
+44
src/lib/strings/website.ts
+44
src/lib/strings/website.ts
···
1
+
export function sanitizeWebsiteForDisplay(website: string): string {
2
+
return website.replace(/^https?:\/\//i, '').replace(/\/$/, '')
3
+
}
4
+
5
+
export function sanitizeWebsiteForLink(website: string): string {
6
+
const normalized = website.toLowerCase()
7
+
return normalized.startsWith('https')
8
+
? normalized
9
+
: `https://${website.toLowerCase()}`
10
+
}
11
+
12
+
export function isValidWebsiteFormat(website: string): boolean {
13
+
const trimmedWebsite = website?.trim() || ''
14
+
15
+
if (!trimmedWebsite || trimmedWebsite.length === 0) {
16
+
return true
17
+
}
18
+
19
+
const normalizedWebsite = trimmedWebsite.toLowerCase()
20
+
21
+
if ('https://'.startsWith(normalizedWebsite)) {
22
+
return true
23
+
}
24
+
25
+
if (!normalizedWebsite.match(/^https:\/\/.+/)) {
26
+
return false
27
+
}
28
+
29
+
const domainMatch = normalizedWebsite.match(/^https:\/\/([^/\s]+)/)
30
+
if (!domainMatch) {
31
+
return false
32
+
}
33
+
34
+
const domain = domainMatch[1]
35
+
36
+
// Check for valid domain structure:
37
+
// - Must contain at least one dot
38
+
// - Must have a valid TLD (at least 2 characters after the last dot)
39
+
// - Cannot be just a single word without extension
40
+
const domainPattern =
41
+
/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$/
42
+
43
+
return domainPattern.test(domain)
44
+
}
+162
-4
src/screens/Profile/Header/EditProfileDialog.tsx
+162
-4
src/screens/Profile/Header/EditProfileDialog.tsx
···
1
-
import {useCallback, useEffect, useState} from 'react'
2
-
import {useWindowDimensions, View} from 'react-native'
1
+
import {useCallback, useEffect, useRef, useState} from 'react'
2
+
import {Pressable, 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'
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
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'
···
26
30
import {useSimpleVerificationState} from '#/components/verification'
27
31
28
32
const DISPLAY_NAME_MAX_GRAPHEMES = 64
33
+
const PRONOUNS_MAX_GRAPHEMES = 20
34
+
const WEBSITE_MAX_GRAPHEMES = 28
29
35
const DESCRIPTION_MAX_GRAPHEMES = 256
30
36
31
37
export function EditProfileDialog({
···
114
120
const [displayName, setDisplayName] = useState(initialDisplayName)
115
121
const initialDescription = profile.description || ''
116
122
const [description, setDescription] = useState(initialDescription)
123
+
const initialPronouns = profile.pronouns || ''
124
+
const [pronouns, setPronouns] = useState(initialPronouns)
125
+
const initialWebsite = profile.website || ''
126
+
const [website, setWebsite] = useState(initialWebsite)
127
+
const websiteInputRef = useRef<any>(null)
117
128
const [userBanner, setUserBanner] = useState<string | undefined | null>(
118
129
profile.banner,
119
130
)
···
130
141
const dirty =
131
142
displayName !== initialDisplayName ||
132
143
description !== initialDescription ||
144
+
pronouns !== initialPronouns ||
145
+
website !== initialWebsite ||
133
146
userAvatar !== profile.avatar ||
134
147
userBanner !== profile.banner
135
148
···
173
186
[setNewUserBanner, setUserBanner, setImageError],
174
187
)
175
188
189
+
const onClearWebsite = useCallback(() => {
190
+
setWebsite('')
191
+
if (websiteInputRef.current) {
192
+
websiteInputRef.current.clear()
193
+
}
194
+
}, [setWebsite])
195
+
176
196
const onPressSave = useCallback(async () => {
177
197
setImageError('')
178
198
try {
···
181
201
updates: {
182
202
displayName: displayName.trimEnd(),
183
203
description: description.trimEnd(),
204
+
pronouns: pronouns.trimEnd().toLowerCase(),
205
+
website: website.trimEnd().toLowerCase(),
184
206
},
185
207
newUserAvatar,
186
208
newUserBanner,
···
197
219
control,
198
220
displayName,
199
221
description,
222
+
pronouns,
223
+
website,
200
224
newUserAvatar,
201
225
newUserBanner,
202
226
setImageError,
···
207
231
text: displayName,
208
232
maxCount: DISPLAY_NAME_MAX_GRAPHEMES,
209
233
})
234
+
const pronounsTooLong = useWarnMaxGraphemeCount({
235
+
text: pronouns,
236
+
maxCount: PRONOUNS_MAX_GRAPHEMES,
237
+
})
238
+
const websiteTooLong = useWarnMaxGraphemeCount({
239
+
text: website,
240
+
maxCount: WEBSITE_MAX_GRAPHEMES,
241
+
})
242
+
const websiteInvalidFormat = !isValidWebsiteFormat(website)
210
243
const descriptionTooLong = useWarnMaxGraphemeCount({
211
244
text: description,
212
245
maxCount: DESCRIPTION_MAX_GRAPHEMES,
···
239
272
!dirty ||
240
273
isUpdatingProfile ||
241
274
displayNameTooLong ||
242
-
descriptionTooLong
275
+
descriptionTooLong ||
276
+
pronounsTooLong ||
277
+
websiteTooLong ||
278
+
websiteInvalidFormat
243
279
}
244
280
size="small"
245
281
color="primary"
···
260
296
isUpdatingProfile,
261
297
displayNameTooLong,
262
298
descriptionTooLong,
299
+
pronounsTooLong,
300
+
websiteTooLong,
301
+
websiteInvalidFormat,
263
302
],
264
303
)
265
304
···
384
423
value={DESCRIPTION_MAX_GRAPHEMES}
385
424
other="Description is too long. The maximum number of characters is #."
386
425
/>
426
+
</Text>
427
+
)}
428
+
</View>
429
+
430
+
<View>
431
+
<TextField.LabelText>
432
+
<Trans>Pronouns</Trans>
433
+
</TextField.LabelText>
434
+
<TextField.Root isInvalid={pronounsTooLong}>
435
+
<Dialog.Input
436
+
defaultValue={pronouns}
437
+
onChangeText={setPronouns}
438
+
label={_(msg`Pronouns`)}
439
+
placeholder={_(msg`Pronouns`)}
440
+
testID="editProfilePronounsInput"
441
+
/>
442
+
</TextField.Root>
443
+
{pronounsTooLong && (
444
+
<Text
445
+
style={[
446
+
a.text_sm,
447
+
a.mt_xs,
448
+
a.font_bold,
449
+
{color: t.palette.negative_400},
450
+
]}>
451
+
<Plural
452
+
value={PRONOUNS_MAX_GRAPHEMES}
453
+
other="The maximum number of characters is #."
454
+
/>
455
+
</Text>
456
+
)}
457
+
</View>
458
+
459
+
<View>
460
+
<TextField.LabelText>
461
+
<Trans>Website</Trans>
462
+
</TextField.LabelText>
463
+
<View style={[a.w_full, a.relative]}>
464
+
<TextField.Root isInvalid={websiteTooLong || websiteInvalidFormat}>
465
+
{website && <TextField.Icon icon={Globe} />}
466
+
<Dialog.Input
467
+
inputRef={websiteInputRef}
468
+
defaultValue={website}
469
+
onChangeText={setWebsite}
470
+
label={_(msg`EditWebsite`)}
471
+
placeholder={_(msg`URL`)}
472
+
testID="editProfileWebsiteInput"
473
+
autoCapitalize="none"
474
+
keyboardType="url"
475
+
style={[
476
+
website
477
+
? {
478
+
paddingRight: tokens.space._5xl,
479
+
}
480
+
: {},
481
+
]}
482
+
/>
483
+
</TextField.Root>
484
+
485
+
{website && (
486
+
<View
487
+
style={[
488
+
a.absolute,
489
+
a.z_10,
490
+
a.my_auto,
491
+
a.inset_0,
492
+
a.justify_center,
493
+
a.pr_sm,
494
+
{left: 'auto'},
495
+
]}>
496
+
<Pressable
497
+
testID="clearWebsiteBtn"
498
+
onPress={onClearWebsite}
499
+
accessibilityLabel={_(msg`Clear website`)}
500
+
accessibilityHint={_(msg`Removes the website URL`)}
501
+
hitSlop={HITSLOP_10}
502
+
style={[
503
+
a.flex_row,
504
+
a.align_center,
505
+
a.justify_center,
506
+
{
507
+
width: tokens.space._2xl,
508
+
height: tokens.space._2xl,
509
+
},
510
+
a.rounded_full,
511
+
]}>
512
+
<CircleX
513
+
width={tokens.space.lg}
514
+
style={{color: t.palette.contrast_600}}
515
+
/>
516
+
</Pressable>
517
+
</View>
518
+
)}
519
+
</View>
520
+
{websiteTooLong && (
521
+
<Text
522
+
style={[
523
+
a.text_sm,
524
+
a.mt_xs,
525
+
a.font_bold,
526
+
{color: t.palette.negative_400},
527
+
]}>
528
+
<Plural
529
+
value={WEBSITE_MAX_GRAPHEMES}
530
+
other="Website is too long. The maximum number of characters is #."
531
+
/>
532
+
</Text>
533
+
)}
534
+
{websiteInvalidFormat && (
535
+
<Text
536
+
style={[
537
+
a.text_sm,
538
+
a.mt_xs,
539
+
a.font_bold,
540
+
{color: t.palette.negative_400},
541
+
]}>
542
+
<Trans>
543
+
Website must be a valid URL (e.g., https://bsky.app)
544
+
</Trans>
387
545
</Text>
388
546
)}
389
547
</View>
+50
-34
src/screens/Profile/Header/Handle.tsx
+50
-34
src/screens/Profile/Header/Handle.tsx
···
4
4
import {useLingui} from '@lingui/react'
5
5
6
6
import {isInvalidHandle, sanitizeHandle} from '#/lib/strings/handles'
7
+
import {sanitizePronouns} from '#/lib/strings/pronouns'
7
8
import {isIOS, isNative} from '#/platform/detection'
8
9
import {type Shadow} from '#/state/cache/types'
9
10
import {useShowLinkInHandle} from '#/state/preferences/show-link-in-handle.tsx'
···
22
23
const t = useTheme()
23
24
const {_} = useLingui()
24
25
const invalidHandle = isInvalidHandle(profile.handle)
26
+
const pronouns = profile.pronouns
25
27
const blockHide = profile.viewer?.blocking || profile.viewer?.blockedBy
26
28
const isBskySocialHandle = profile.handle.endsWith('.bsky.social')
27
29
const showProfileInHandle = useShowLinkInHandle()
···
33
35
)
34
36
return (
35
37
<View
36
-
style={[a.flex_row, a.gap_sm, a.align_center, {maxWidth: '100%'}]}
38
+
style={[a.flex_col, a.gap_sm, a.align_start, {maxWidth: '100%'}]}
37
39
pointerEvents={disableTaps ? 'none' : isIOS ? 'auto' : 'box-none'}>
38
40
<NewskieDialog profile={profile} disabled={disableTaps} />
39
41
{profile.viewer?.followedBy && !blockHide ? (
···
43
45
</Text>
44
46
</View>
45
47
) : undefined}
46
-
<Text
47
-
emoji
48
-
numberOfLines={1}
49
-
style={[
50
-
invalidHandle
51
-
? [
52
-
a.border,
53
-
a.text_xs,
54
-
a.px_sm,
55
-
a.py_xs,
56
-
a.rounded_xs,
57
-
{borderColor: t.palette.contrast_200},
58
-
]
59
-
: [a.text_md, a.leading_snug, t.atoms.text_contrast_medium],
60
-
web({
61
-
wordBreak: 'break-all',
62
-
direction: 'ltr',
63
-
unicodeBidi: 'isolate',
64
-
}),
65
-
]}>
66
-
{invalidHandle ? (
67
-
_(msg`⚠Invalid Handle`)
68
-
) : showProfileInHandle && !isBskySocialHandle ? (
69
-
<InlineLinkText
70
-
to={`https://${profile.handle}`}
71
-
label={profile.handle}>
72
-
<Text style={[a.text_md, {color: t.palette.primary_500}]}>
73
-
{sanitized}
74
-
</Text>
75
-
</InlineLinkText>
76
-
) : (
77
-
sanitized
48
+
49
+
<View style={[a.flex_row, a.flex_wrap, {gap: 6}]}>
50
+
<Text
51
+
emoji
52
+
numberOfLines={1}
53
+
style={[
54
+
invalidHandle
55
+
? [
56
+
a.border,
57
+
a.text_xs,
58
+
a.px_sm,
59
+
a.py_xs,
60
+
a.rounded_xs,
61
+
{borderColor: t.palette.contrast_200},
62
+
]
63
+
: [a.text_md, a.leading_snug, t.atoms.text_contrast_medium],
64
+
web({
65
+
wordBreak: 'break-all',
66
+
direction: 'ltr',
67
+
unicodeBidi: 'isolate',
68
+
}),
69
+
]}>
70
+
{invalidHandle ? (
71
+
_(msg`⚠Invalid Handle`)
72
+
) : showProfileInHandle && !isBskySocialHandle ? (
73
+
<InlineLinkText
74
+
to={`https://${profile.handle}`}
75
+
label={profile.handle}>
76
+
<Text style={[a.text_md, {color: t.palette.primary_500}]}>
77
+
{sanitized}
78
+
</Text>
79
+
</InlineLinkText>
80
+
) : (
81
+
sanitized
82
+
)}
83
+
</Text>
84
+
{pronouns && (
85
+
<Text
86
+
style={[
87
+
t.atoms.text_contrast_low,
88
+
a.text_md,
89
+
a.leading_snug,
90
+
a.pb_sm,
91
+
]}>
92
+
{sanitizePronouns(pronouns, isNative)}
93
+
</Text>
78
94
)}
79
-
</Text>
95
+
</View>
80
96
</View>
81
97
)
82
98
}
+43
-1
src/screens/Profile/Header/ProfileHeaderStandard.tsx
+43
-1
src/screens/Profile/Header/ProfileHeaderStandard.tsx
···
12
12
import {useActorStatus} from '#/lib/actor-status'
13
13
import {sanitizeDisplayName} from '#/lib/strings/display-names'
14
14
import {sanitizeHandle} from '#/lib/strings/handles'
15
+
import {formatJoinDate} from '#/lib/strings/time'
16
+
import {
17
+
sanitizeWebsiteForDisplay,
18
+
sanitizeWebsiteForLink,
19
+
} from '#/lib/strings/website'
15
20
import {logger} from '#/logger'
16
21
import {isIOS} from '#/platform/detection'
17
22
import {useProfileShadow} from '#/state/cache/profile-shadow'
···
22
27
import {useRequireAuth, useSession} from '#/state/session'
23
28
import {ProfileMenu} from '#/view/com/profile/ProfileMenu'
24
29
import * as Toast from '#/view/com/util/Toast'
25
-
import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf'
30
+
import {atoms as a, platform, tokens, useBreakpoints, useTheme} from '#/alf'
26
31
import {SubscribeProfileButton} from '#/components/activity-notifications/SubscribeProfileButton'
27
32
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
28
33
import {useDialogControl} from '#/components/Dialog'
29
34
import {MessageProfileButton} from '#/components/dms/MessageProfileButton'
35
+
import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'
36
+
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
30
37
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
31
38
import {
32
39
KnownFollowers,
33
40
shouldShowKnownFollowers,
34
41
} from '#/components/KnownFollowers'
42
+
import {Link} from '#/components/Link'
35
43
import * as Prompt from '#/components/Prompt'
36
44
import {RichText} from '#/components/RichText'
37
45
import {Text} from '#/components/Typography'
···
79
87
profile.viewer?.blocking ||
80
88
profile.viewer?.blockedBy ||
81
89
profile.viewer?.blockingByList
90
+
91
+
const website = profile.website
92
+
const websiteFormatted = sanitizeWebsiteForDisplay(website ?? '')
93
+
94
+
const dateJoined = useMemo(() => {
95
+
if (!profile.createdAt) return ''
96
+
return formatJoinDate(profile.createdAt)
97
+
}, [profile.createdAt])
82
98
83
99
const editProfileControl = useDialogControl()
84
100
···
315
331
)}
316
332
</View>
317
333
)}
334
+
335
+
<View style={[a.flex_row, a.flex_wrap, {gap: 10}, a.pt_md]}>
336
+
{websiteFormatted && (
337
+
<Link
338
+
to={sanitizeWebsiteForLink(websiteFormatted)}
339
+
label={_(msg({message: `Visit ${websiteFormatted}`}))}
340
+
style={[a.flex_row, a.align_center, a.gap_xs]}>
341
+
<Globe
342
+
width={tokens.space.lg}
343
+
style={{color: t.palette.primary_500}}
344
+
/>
345
+
<Text style={[{color: t.palette.primary_500}]}>
346
+
{websiteFormatted}
347
+
</Text>
348
+
</Link>
349
+
)}
350
+
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
351
+
<CalendarDays
352
+
width={tokens.space.lg}
353
+
style={{color: t.atoms.text_contrast_medium.color}}
354
+
/>
355
+
<Text style={[t.atoms.text_contrast_medium]}>
356
+
<Trans>Joined {dateJoined}</Trans>
357
+
</Text>
358
+
</View>
359
+
</View>
318
360
</View>
319
361
320
362
<Prompt.Basic
+9
-1
src/state/queries/profile.ts
+9
-1
src/state/queries/profile.ts
···
177
177
if ('pinnedPost' in updates) {
178
178
next.pinnedPost = updates.pinnedPost
179
179
}
180
+
if ('pronouns' in updates) {
181
+
next.pronouns = updates.pronouns
182
+
}
183
+
if ('website' in updates) {
184
+
next.website = updates.website
185
+
}
180
186
}
181
187
if (newUserAvatarPromise) {
182
188
const res = await newUserAvatarPromise
···
220
226
}
221
227
return (
222
228
res.data.displayName === updates.displayName &&
223
-
res.data.description === updates.description
229
+
res.data.description === updates.description &&
230
+
res.data.pronouns === updates.pronouns &&
231
+
res.data.website === updates.website
224
232
)
225
233
}),
226
234
)
+27
-16
yarn.lock
+27
-16
yarn.lock
···
64
64
"@atproto/xrpc-server" "^0.9.4"
65
65
66
66
"@atproto/api@^0.16.7":
67
-
version "0.16.7"
68
-
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.16.7.tgz#eb0c520dbdaf74ba6f5ad7f9c6afe2d1389b8a0a"
69
-
integrity sha512-EdVWkEgaEQm1LEiiP1fW/XXXpMNmtvT5c9+cZVRiwYc4rTB66WIJJWqmaMT/tB7nccMkFjr6FtwObq5LewWfgw==
67
+
version "0.16.10"
68
+
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.16.10.tgz#79c383272f9aba0cda787d0c352454579f0664f0"
69
+
integrity sha512-PPWCk73+9IcbadUFBMI86RIV4UQSozRZ1K2XNB+0+CIhUtOjt8jfGDwU/VxGA+HZrDbjOCpvhy5KbOrtoLgGVQ==
70
70
dependencies:
71
-
"@atproto/common-web" "^0.4.2"
72
-
"@atproto/lexicon" "^0.5.0"
71
+
"@atproto/common-web" "^0.4.3"
72
+
"@atproto/lexicon" "^0.5.1"
73
73
"@atproto/syntax" "^0.4.1"
74
-
"@atproto/xrpc" "^0.7.4"
74
+
"@atproto/xrpc" "^0.7.5"
75
75
await-lock "^2.2.2"
76
76
multiformats "^9.9.0"
77
77
tlds "^1.234.0"
···
160
160
pino-http "^8.2.1"
161
161
typed-emitter "^2.1.0"
162
162
163
-
"@atproto/common-web@^0.4.2":
164
-
version "0.4.2"
165
-
resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.2.tgz#6e3add6939da93d3dfbc8f87e26dc4f57fad7259"
166
-
integrity sha512-vrXwGNoFGogodjQvJDxAeP3QbGtawgZute2ed1XdRO0wMixLk3qewtikZm06H259QDJVu6voKC5mubml+WgQUw==
163
+
"@atproto/common-web@^0.4.2", "@atproto/common-web@^0.4.3":
164
+
version "0.4.3"
165
+
resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.3.tgz#b4480220b5682db09da45f4ef906eb7619c838b5"
166
+
integrity sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg==
167
167
dependencies:
168
168
graphemer "^1.4.0"
169
169
multiformats "^9.9.0"
···
292
292
"@atproto/xrpc" "^0.7.4"
293
293
multiformats "^9.9.0"
294
294
295
-
"@atproto/lexicon@0.5.0", "@atproto/lexicon@^0.5.0":
295
+
"@atproto/lexicon@0.5.0":
296
296
version "0.5.0"
297
297
resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.5.0.tgz#4d2be425361f9ac7f9754b8a1ccba29ddf0b9460"
298
298
integrity sha512-3aAzEAy9EAPs3CxznzMhEcqDd7m3vz1eze/ya9/ThbB7yleqJIhz5GY2q76tCCwHPhn5qDDMhlA9kKV6fG23gA==
···
303
303
multiformats "^9.9.0"
304
304
zod "^3.23.8"
305
305
306
+
"@atproto/lexicon@^0.5.0", "@atproto/lexicon@^0.5.1":
307
+
version "0.5.1"
308
+
resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.5.1.tgz#e9b7d5c70dc5a38518a8069cd80fea77ab526947"
309
+
integrity sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A==
310
+
dependencies:
311
+
"@atproto/common-web" "^0.4.3"
312
+
"@atproto/syntax" "^0.4.1"
313
+
iso-datestring-validator "^2.2.2"
314
+
multiformats "^9.9.0"
315
+
zod "^3.23.8"
316
+
306
317
"@atproto/oauth-provider-api@0.3.0":
307
318
version "0.3.0"
308
319
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-api/-/oauth-provider-api-0.3.0.tgz#c53a6f2584e6e53746b6cdf233be591fdf7d4355"
···
508
519
ws "^8.12.0"
509
520
zod "^3.23.8"
510
521
511
-
"@atproto/xrpc@^0.7.4":
512
-
version "0.7.4"
513
-
resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.7.4.tgz#030342548797c1f344968c457a8659dbb60a2d60"
514
-
integrity sha512-sDi68+QE1XHegTaNAndlX41Gp827pouSzSs8CyAwhrqZdsJUxE3P7TMtrA0z+zAjvxVyvzscRc0TsN/fGUGrhw==
522
+
"@atproto/xrpc@^0.7.4", "@atproto/xrpc@^0.7.5":
523
+
version "0.7.5"
524
+
resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.7.5.tgz#40cef1a657b5f28af8ebec9e3dac5872e58e88ea"
525
+
integrity sha512-MUYNn5d2hv8yVegRL0ccHvTHAVj5JSnW07bkbiaz96UH45lvYNRVwt44z+yYVnb0/mvBzyD3/ZQ55TRGt7fHkA==
515
526
dependencies:
516
-
"@atproto/lexicon" "^0.5.0"
527
+
"@atproto/lexicon" "^0.5.1"
517
528
zod "^3.23.8"
518
529
519
530
"@aws-crypto/crc32@3.0.0":