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

Merge branch 'profilemetadata'

aylac.top 3be82cf6 90f7231b

verified
Changed files
+356 -56
src
+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
··· 40 40 a.getDate() === b.getDate() 41 41 ) 42 42 } 43 + 44 + export function formatJoinDate(date: number | string | Date): string { 45 + const d = new Date(date) 46 + return d.toLocaleDateString('en-US', { 47 + month: 'short', 48 + year: 'numeric', 49 + }) 50 + }
+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
··· 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
··· 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
··· 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
··· 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
··· 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":