Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client

[APP-1750] Add the ability to report livestreams (#9654)

* Add report dialog to LiveStatus dialog

* Mr Worldwide™

* Bug fix bug fix

* Copy update

* Simplify parsing

* Bump api sdk

* update dev-env

* Update yarn.lock

* Bump api sdk

* Refactor useActorStatus, add disablement and appeal dialog

* Fix types

* Guard against chat profile views

* Go live (disabled)

* Fix types

* Status reports should only go to bsky

* Ah yeah let's not override types

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>

authored by

Eric Bailey
Samuel Newman
and committed by
GitHub
edb19dd6 85a616ee

+325 -55
+1 -1
package.json
··· 73 73 "icons:optimize": "svgo -f ./assets/icons" 74 74 }, 75 75 "dependencies": { 76 - "@atproto/api": "^0.18.11", 76 + "@atproto/api": "^0.18.13", 77 77 "@bitdrift/react-native": "^0.6.8", 78 78 "@braintree/sanitize-url": "^6.0.2", 79 79 "@bsky.app/alf": "^0.1.6",
+1
src/components/ProfileHoverCard/index.web.tsx
··· 389 389 {data && moderationOpts ? ( 390 390 status.isActive ? ( 391 391 <LiveStatus 392 + status={status} 392 393 profile={data} 393 394 embed={status.embed} 394 395 padding="lg"
+7
src/components/dialogs/Context.tsx
··· 3 3 import {type AgeAssuranceRedirectDialogState} from '#/components/ageAssurance/AgeAssuranceRedirectDialog' 4 4 import * as Dialog from '#/components/Dialog' 5 5 import {type Screen} from '#/components/dialogs/EmailDialog/types' 6 + import {type ReportSubject} from '#/components/moderation/ReportDialog' 6 7 7 8 type Control = Dialog.DialogControlProps 8 9 ··· 24 25 share?: boolean 25 26 }> 26 27 ageAssuranceRedirectDialogControl: StatefulControl<AgeAssuranceRedirectDialogState> 28 + reportDialogControl: StatefulControl<{subject: ReportSubject}> 27 29 } 28 30 29 31 const ControlsContext = createContext<ControlsContext | null>(null) ··· 51 53 }>() 52 54 const ageAssuranceRedirectDialogControl = 53 55 useStatefulDialogControl<AgeAssuranceRedirectDialogState>() 56 + const reportDialogControl = useStatefulDialogControl<{ 57 + subject: ReportSubject 58 + }>() 54 59 55 60 const ctx = useMemo<ControlsContext>( 56 61 () => ({ ··· 60 65 emailDialogControl, 61 66 linkWarningDialogControl, 62 67 ageAssuranceRedirectDialogControl, 68 + reportDialogControl, 63 69 }), 64 70 [ 65 71 mutedWordsDialogControl, ··· 68 74 emailDialogControl, 69 75 linkWarningDialogControl, 70 76 ageAssuranceRedirectDialogControl, 77 + reportDialogControl, 71 78 ], 72 79 ) 73 80
+147
src/components/live/GoLiveDisabledDialog.tsx
··· 1 + import {useCallback, useState} from 'react' 2 + import {View} from 'react-native' 3 + import {type AppBskyActorDefs, ToolsOzoneReportDefs} from '@atproto/api' 4 + import {msg, Trans} from '@lingui/macro' 5 + import {useLingui} from '@lingui/react' 6 + import {useMutation} from '@tanstack/react-query' 7 + 8 + import {BLUESKY_MOD_SERVICE_HEADERS} from '#/lib/constants' 9 + import {logger} from '#/logger' 10 + import {useAgent} from '#/state/session' 11 + import {atoms as a, web} from '#/alf' 12 + import {Button, ButtonIcon, ButtonText} from '#/components/Button' 13 + import * as Dialog from '#/components/Dialog' 14 + import {Loader} from '#/components/Loader' 15 + import * as Toast from '#/components/Toast' 16 + import {Text} from '#/components/Typography' 17 + 18 + export function GoLiveDisabledDialog({ 19 + control, 20 + status, 21 + }: { 22 + control: Dialog.DialogControlProps 23 + status: AppBskyActorDefs.StatusView 24 + }) { 25 + return ( 26 + <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> 27 + <Dialog.Handle /> 28 + <DialogInner control={control} status={status} /> 29 + </Dialog.Outer> 30 + ) 31 + } 32 + 33 + export function DialogInner({ 34 + control, 35 + status, 36 + }: { 37 + control: Dialog.DialogControlProps 38 + status: AppBskyActorDefs.StatusView 39 + }) { 40 + const {_} = useLingui() 41 + const agent = useAgent() 42 + const [details, setDetails] = useState('') 43 + 44 + const {mutate, isPending} = useMutation({ 45 + mutationFn: async () => { 46 + if (!agent.session?.did) { 47 + throw new Error('Not logged in') 48 + } 49 + if (!status.uri || !status.cid) { 50 + throw new Error('Status is missing uri or cid') 51 + } 52 + 53 + if (__DEV__) { 54 + logger.info('Submitting go live appeal', { 55 + details, 56 + }) 57 + } else { 58 + await agent.createModerationReport( 59 + { 60 + reasonType: ToolsOzoneReportDefs.REASONAPPEAL, 61 + subject: { 62 + $type: 'com.atproto.repo.strongRef', 63 + uri: status.uri, 64 + cid: status.cid, 65 + }, 66 + reason: details, 67 + }, 68 + { 69 + encoding: 'application/json', 70 + headers: BLUESKY_MOD_SERVICE_HEADERS, 71 + }, 72 + ) 73 + } 74 + }, 75 + onError: () => { 76 + Toast.show(_(msg`Failed to submit appeal, please try again.`), { 77 + type: 'error', 78 + }) 79 + }, 80 + onSuccess: () => { 81 + control.close() 82 + Toast.show(_(msg({message: 'Appeal submitted', context: 'toast'})), { 83 + type: 'success', 84 + }) 85 + }, 86 + }) 87 + 88 + const onSubmit = useCallback(() => mutate(), [mutate]) 89 + 90 + return ( 91 + <Dialog.ScrollableInner 92 + label={_(msg`Appeal livestream suspension`)} 93 + style={[web({maxWidth: 400})]}> 94 + <View style={[a.gap_lg]}> 95 + <View style={[a.gap_md]}> 96 + <Text 97 + style={[ 98 + a.flex_1, 99 + a.text_2xl, 100 + a.font_semi_bold, 101 + a.leading_snug, 102 + a.pr_4xl, 103 + ]}> 104 + <Trans>Going live is currently disabled for your account</Trans> 105 + </Text> 106 + <Text style={[a.text_md, a.leading_snug]}> 107 + <Trans> 108 + You are currently blocked from using the Go Live feature. To 109 + appeal this moderation decision, please submit the form below. 110 + </Trans> 111 + </Text> 112 + <Text style={[a.text_md, a.leading_snug]}> 113 + <Trans> 114 + This appeal will be sent to Bluesky's moderation service. 115 + </Trans> 116 + </Text> 117 + </View> 118 + 119 + <View style={[a.gap_md]}> 120 + <Dialog.Input 121 + label={_(msg`Text input field`)} 122 + placeholder={_( 123 + msg`Please explain why you think your Go Live access was incorrectly disabled.`, 124 + )} 125 + value={details} 126 + onChangeText={setDetails} 127 + autoFocus={true} 128 + numberOfLines={3} 129 + multiline 130 + maxLength={300} 131 + /> 132 + <Button 133 + testID="submitBtn" 134 + variant="solid" 135 + color="primary" 136 + size="large" 137 + onPress={onSubmit} 138 + label={_(msg`Submit`)}> 139 + <ButtonText>{_(msg`Submit`)}</ButtonText> 140 + {isPending && <ButtonIcon icon={Loader} />} 141 + </Button> 142 + </View> 143 + </View> 144 + <Dialog.Close /> 145 + </Dialog.ScrollableInner> 146 + ) 147 + }
+52 -8
src/components/live/LiveStatusDialog.tsx
··· 17 17 import {android, atoms as a, platform, tokens, useTheme, web} from '#/alf' 18 18 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 19 19 import * as Dialog from '#/components/Dialog' 20 + import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo' 21 + import {createStaticClick, SimpleInlineLinkText} from '#/components/Link' 22 + import {useGlobalReportDialogControl} from '#/components/moderation/ReportDialog' 20 23 import * as ProfileCard from '#/components/ProfileCard' 21 24 import {Text} from '#/components/Typography' 22 25 import type * as bsky from '#/types/bsky' ··· 28 31 control, 29 32 profile, 30 33 embed, 34 + status, 31 35 }: { 32 36 control: Dialog.DialogControlProps 33 37 profile: bsky.profile.AnyProfileView ··· 38 42 return ( 39 43 <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> 40 44 <Dialog.Handle difference={!!embed.external.thumb} /> 41 - <DialogInner profile={profile} embed={embed} navigation={navigation} /> 45 + <DialogInner 46 + status={status} 47 + profile={profile} 48 + embed={embed} 49 + navigation={navigation} 50 + /> 42 51 </Dialog.Outer> 43 52 ) 44 53 } ··· 47 56 profile, 48 57 embed, 49 58 navigation, 59 + status, 50 60 }: { 51 61 profile: bsky.profile.AnyProfileView 52 62 embed: AppBskyEmbedExternal.View 53 63 navigation: NavigationProp 64 + status: AppBskyActorDefs.StatusView 54 65 }) { 55 66 const {_} = useLingui() 56 67 const control = Dialog.useDialogContext() ··· 69 80 contentContainerStyle={[a.pt_0, a.px_0]} 70 81 style={[web({maxWidth: 420}), a.overflow_hidden]}> 71 82 <LiveStatus 83 + status={status} 72 84 profile={profile} 73 85 embed={embed} 74 86 onPressOpenProfile={onPressOpenProfile} ··· 79 91 } 80 92 81 93 export function LiveStatus({ 94 + status, 82 95 profile, 83 96 embed, 84 97 padding = 'xl', 85 98 onPressOpenProfile, 86 99 }: { 100 + status: AppBskyActorDefs.StatusView 87 101 profile: bsky.profile.AnyProfileView 88 102 embed: AppBskyEmbedExternal.View 89 103 padding?: 'lg' | 'xl' ··· 94 108 const queryClient = useQueryClient() 95 109 const openLink = useOpenLink() 96 110 const moderationOpts = useModerationOpts() 111 + const reportDialogControl = useGlobalReportDialogControl() 112 + const dialogContext = Dialog.useDialogContext() 97 113 98 114 return ( 99 115 <> ··· 205 221 </Button> 206 222 </ProfileCard.Header> 207 223 )} 208 - <Text 224 + <View 209 225 style={[ 210 - a.w_full, 211 - a.text_center, 212 - t.atoms.text_contrast_low, 213 - a.text_sm, 226 + a.flex_row, 227 + a.align_center, 228 + a.justify_between, 229 + a.flex_1, 230 + a.pt_sm, 214 231 ]}> 215 - <Trans>Live feature is in beta testing</Trans> 216 - </Text> 232 + <View style={[a.flex_row, a.align_center, a.gap_xs, a.flex_1]}> 233 + <CircleInfoIcon size="sm" fill={t.atoms.text_contrast_low.color} /> 234 + <Text style={[t.atoms.text_contrast_low, a.text_sm]}> 235 + <Trans>Live feature is in beta</Trans> 236 + </Text> 237 + </View> 238 + {status && ( 239 + <SimpleInlineLinkText 240 + label={_(msg`Report this livestream`)} 241 + {...createStaticClick(() => { 242 + function open() { 243 + reportDialogControl.open({ 244 + subject: { 245 + ...status, 246 + $type: 'app.bsky.actor.defs#statusView', 247 + }, 248 + }) 249 + } 250 + if (dialogContext.isWithinDialog) { 251 + dialogContext.close(open) 252 + } else { 253 + open() 254 + } 255 + })} 256 + style={[a.text_sm, a.underline, t.atoms.text_contrast_medium]}> 257 + <Trans>Report</Trans> 258 + </SimpleInlineLinkText> 259 + )} 260 + </View> 217 261 </View> 218 262 </> 219 263 )
+1
src/components/moderation/ReportDialog/action.ts
··· 70 70 } 71 71 break 72 72 } 73 + case 'status': 73 74 case 'post': 74 75 case 'list': 75 76 case 'feed':
+9
src/components/moderation/ReportDialog/const.ts
··· 3 3 ToolsOzoneReportDefs as OzoneReportDefs, 4 4 } from '@atproto/api' 5 5 6 + import {type ParsedReportSubject} from '#/components/moderation/ReportDialog/types' 7 + 6 8 export const DMCA_LINK = 'https://bsky.social/about/support/copyright' 7 9 export const SUPPORT_PAGE = 'https://bsky.social/about/support' 8 10 ··· 112 114 OzoneReportDefs.REASONCHILDSAFETYOTHER, 113 115 OzoneReportDefs.REASONVIOLENCEEXTREMISTCONTENT, 114 116 ]) 117 + 118 + /** 119 + * Set of _parsed_ subject types that should only be sent to Bluesky's 120 + * moderation service. 121 + */ 122 + export const BSKY_LABELER_ONLY_SUBJECT_TYPES: Set<ParsedReportSubject['type']> = 123 + new Set(['convoMessage', 'status'])
+6
src/components/moderation/ReportDialog/copy.ts
··· 14 14 subtitle: _(msg`Why should this user be reviewed?`), 15 15 } 16 16 } 17 + case 'status': { 18 + return { 19 + title: _(msg`Report this livestream`), 20 + subtitle: _(msg`Why should this livestream be reviewed?`), 21 + } 22 + } 17 23 case 'post': { 18 24 return { 19 25 title: _(msg`Report this post`),
+19 -10
src/components/moderation/ReportDialog/index.tsx
··· 16 16 import * as Admonition from '#/components/Admonition' 17 17 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 18 18 import * as Dialog from '#/components/Dialog' 19 + import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' 19 20 import {useDelayedLoading} from '#/components/hooks/useDelayedLoading' 20 21 import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as Retry} from '#/components/icons/ArrowRotate' 21 22 import { ··· 31 32 import {useSubmitReportMutation} from './action' 32 33 import { 33 34 BSKY_LABELER_ONLY_REPORT_REASONS, 35 + BSKY_LABELER_ONLY_SUBJECT_TYPES, 34 36 NEW_TO_OLD_REASONS_MAP, 35 37 SUPPORT_PAGE, 36 38 } from './const' ··· 44 46 useReportOptions, 45 47 } from './utils/useReportOptions' 46 48 49 + export {type ReportSubject} from './types' 47 50 export {useDialogControl as useReportDialogControl} from '#/components/Dialog' 48 51 52 + export function useGlobalReportDialogControl() { 53 + return useGlobalDialogsControlContext().reportDialogControl 54 + } 55 + 49 56 const logger = Logger.create(Logger.Context.ReportDialog) 50 57 58 + export function GlobalReportDialog() { 59 + const {value, control} = useGlobalReportDialogControl() 60 + return <ReportDialog control={control} subject={value?.subject} /> 61 + } 62 + 51 63 export function ReportDialog( 52 64 props: Omit<ReportDialogProps, 'subject'> & { 53 - subject: ReportSubject 65 + subject?: ReportSubject 54 66 }, 55 67 ) { 56 68 const subject = React.useMemo( 57 - () => parseReportSubject(props.subject), 69 + () => (props.subject ? parseReportSubject(props.subject) : undefined), 58 70 [props.subject], 59 71 ) 60 72 const onClose = React.useCallback(() => { ··· 116 128 const isBskyOnlyReason = state?.selectedOption?.reason 117 129 ? BSKY_LABELER_ONLY_REPORT_REASONS.has(state.selectedOption.reason) 118 130 : false 119 - // some subjects (chats) only go to Bluesky 120 - const isBskyOnlySubject = props.subject.type === 'convoMessage' 131 + // some subjects ONLY go to Bluesky 132 + const isBskyOnlySubject = BSKY_LABELER_ONLY_SUBJECT_TYPES.has( 133 + props.subject.type, 134 + ) 121 135 122 136 /** 123 137 * Labelers that support this `subject` and its NSID collection ··· 824 838 {title} 825 839 </Text> 826 840 <Text 827 - style={[ 828 - a.text_sm, 829 - , 830 - a.leading_snug, 831 - t.atoms.text_contrast_medium, 832 - ]}> 841 + style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}> 833 842 <Trans>By {sanitizeHandle(labeler.creator.handle, '@')}</Trans> 834 843 </Text> 835 844 </View>
+7
src/components/moderation/ReportDialog/types.ts
··· 18 18 | $Typed<AppBskyActorDefs.ProfileViewBasic> 19 19 | $Typed<AppBskyActorDefs.ProfileView> 20 20 | $Typed<AppBskyActorDefs.ProfileViewDetailed> 21 + | $Typed<AppBskyActorDefs.StatusView> 21 22 | $Typed<AppBskyGraphDefs.ListView> 22 23 | $Typed<AppBskyFeedDefs.GeneratorView> 23 24 | $Typed<AppBskyGraphDefs.StarterPackView> ··· 37 38 link: boolean 38 39 quote: boolean 39 40 } 41 + } 42 + | { 43 + type: 'status' 44 + uri: string 45 + cid: string 46 + nsid: string 40 47 } 41 48 | { 42 49 type: 'list'
+8
src/components/moderation/ReportDialog/utils/parseReportSubject.ts
··· 33 33 did: subject.did, 34 34 nsid: 'app.bsky.actor.profile', 35 35 } 36 + } else if (AppBskyActorDefs.isStatusView(subject)) { 37 + if (!subject.uri || !subject.cid) return 38 + return { 39 + type: 'status', 40 + uri: subject.uri, 41 + cid: subject.cid, 42 + nsid: 'app.bsky.actor.status', 43 + } 36 44 } else if (AppBskyGraphDefs.isListView(subject)) { 37 45 return { 38 46 type: 'list',
+21 -8
src/lib/actor-status.ts
··· 19 19 return useMemo(() => { 20 20 tick! // revalidate every minute 21 21 22 - if ( 23 - shadowed && 24 - 'status' in shadowed && 25 - shadowed.status && 26 - validateStatus(shadowed.did, shadowed.status, config) && 27 - isStatusStillActive(shadowed.status.expiresAt) 28 - ) { 22 + if (shadowed && 'status' in shadowed && shadowed.status) { 23 + const isValid = validateStatus(shadowed.did, shadowed.status, config) 24 + const isDisabled = shadowed.status.isDisabled || false 25 + const isActive = isStatusStillActive(shadowed.status.expiresAt) 26 + if (isValid && !isDisabled && isActive) { 27 + return { 28 + uri: shadowed.status.uri, 29 + cid: shadowed.status.cid, 30 + isDisabled: false, 31 + isActive: true, 32 + status: 'app.bsky.actor.status#live', 33 + embed: shadowed.status.embed as $Typed<AppBskyEmbedExternal.View>, // temp_isStatusValid asserts this 34 + expiresAt: shadowed.status.expiresAt!, // isStatusStillActive asserts this 35 + record: shadowed.status.record, 36 + } satisfies AppBskyActorDefs.StatusView 37 + } 29 38 return { 30 - isActive: true, 39 + uri: shadowed.status.uri, 40 + cid: shadowed.status.cid, 41 + isDisabled, 42 + isActive: false, 31 43 status: 'app.bsky.actor.status#live', 32 44 embed: shadowed.status.embed as $Typed<AppBskyEmbedExternal.View>, // temp_isStatusValid asserts this 33 45 expiresAt: shadowed.status.expiresAt!, // isStatusStillActive asserts this ··· 36 48 } else { 37 49 return { 38 50 status: '', 51 + isDisabled: false, 39 52 isActive: false, 40 53 record: {}, 41 54 } satisfies AppBskyActorDefs.StatusView
+22 -8
src/view/com/profile/ProfileMenu.tsx
··· 49 49 import {StarterPack} from '#/components/icons/StarterPack' 50 50 import {EditLiveDialog} from '#/components/live/EditLiveDialog' 51 51 import {GoLiveDialog} from '#/components/live/GoLiveDialog' 52 + import {GoLiveDisabledDialog} from '#/components/live/GoLiveDisabledDialog' 52 53 import * as Menu from '#/components/Menu' 53 54 import { 54 55 ReportDialog, ··· 79 80 const [devModeEnabled] = useDevMode() 80 81 const verification = useFullVerificationState({profile}) 81 82 const canGoLive = useCanGoLive(currentAccount?.did) 83 + const status = useActorStatus(profile) 82 84 83 85 const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile) 84 86 const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) ··· 90 92 const blockPromptControl = Prompt.usePromptControl() 91 93 const loggedOutWarningPromptControl = Prompt.usePromptControl() 92 94 const goLiveDialogControl = useDialogControl() 95 + const goLiveDisabledDialogControl = useDialogControl() 93 96 const addToStarterPacksDialogControl = useDialogControl() 94 97 95 98 const showLoggedOutWarning = React.useMemo(() => { ··· 220 223 return v.issuer === currentAccount?.did 221 224 }) ?? [] 222 225 223 - const status = useActorStatus(profile) 224 - 225 226 return ( 226 227 <EventStopper onKeyDown={false}> 227 228 <Menu.Root> ··· 330 331 <Menu.Item 331 332 testID="profileHeaderDropdownListAddRemoveBtn" 332 333 label={ 333 - status.isActive 334 - ? _(msg`Edit live status`) 335 - : _(msg`Go live`) 334 + status.isDisabled 335 + ? _(msg`Go live (disabled)`) 336 + : status.isActive 337 + ? _(msg`Edit live status`) 338 + : _(msg`Go live`) 336 339 } 337 - onPress={goLiveDialogControl.open}> 340 + onPress={ 341 + status.isDisabled 342 + ? goLiveDisabledDialogControl.open 343 + : goLiveDialogControl.open 344 + }> 338 345 <Menu.ItemText> 339 - {status.isActive ? ( 346 + {status.isDisabled ? ( 347 + <Trans>Go live (disabled)</Trans> 348 + ) : status.isActive ? ( 340 349 <Trans>Edit live status</Trans> 341 350 ) : ( 342 351 <Trans>Go live</Trans> ··· 517 526 verifications={currentAccountVerifications} 518 527 /> 519 528 520 - {status.isActive ? ( 529 + {status.isDisabled ? ( 530 + <GoLiveDisabledDialog 531 + control={goLiveDisabledDialogControl} 532 + status={status} 533 + /> 534 + ) : status.isActive ? ( 521 535 <EditLiveDialog 522 536 control={goLiveDialogControl} 523 537 status={status}
+2
src/view/shell/index.tsx
··· 34 34 import {MutedWordsDialog} from '#/components/dialogs/MutedWords' 35 35 import {NuxDialogs} from '#/components/dialogs/nuxs' 36 36 import {SigninDialog} from '#/components/dialogs/Signin' 37 + import {GlobalReportDialog} from '#/components/moderation/ReportDialog' 37 38 import { 38 39 Outlet as PolicyUpdateOverlayPortalOutlet, 39 40 usePolicyUpdateContext, ··· 117 118 <LinkWarningDialog /> 118 119 <Lightbox /> 119 120 <NuxDialogs /> 121 + <GlobalReportDialog /> 120 122 121 123 {/* Until policy update has been completed by the user, don't render anything that is portaled */} 122 124 {policyUpdateState.completed && (
+2
src/view/shell/index.web.tsx
··· 24 24 import {NuxDialogs} from '#/components/dialogs/nuxs' 25 25 import {SigninDialog} from '#/components/dialogs/Signin' 26 26 import {useWelcomeModal} from '#/components/hooks/useWelcomeModal' 27 + import {GlobalReportDialog} from '#/components/moderation/ReportDialog' 27 28 import { 28 29 Outlet as PolicyUpdateOverlayPortalOutlet, 29 30 usePolicyUpdateContext, ··· 73 74 <LinkWarningDialog /> 74 75 <Lightbox /> 75 76 <NuxDialogs /> 77 + <GlobalReportDialog /> 76 78 77 79 {welcomeModalControl.isOpen && ( 78 80 <WelcomeModal control={welcomeModalControl} />
+20 -20
yarn.lock
··· 82 82 "@atproto/xrpc" "^0.7.6" 83 83 "@atproto/xrpc-server" "^0.10.0" 84 84 85 - "@atproto/api@^0.18.11": 86 - version "0.18.11" 87 - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.18.11.tgz#38b8bacecaae4c24bc29bd98b34f270d8be675b3" 88 - integrity sha512-YhgyL4rOBFOIs+e/8s5S+EjwM3T5q65IKh30ZoIrWcQS/2uteiIzg1xB+iXmexZq4Cwug0NLfRUJDViDK/ut0w== 85 + "@atproto/api@^0.18.13": 86 + version "0.18.13" 87 + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.18.13.tgz#63eee310e6715752eb87748323cf9ab57dd91e4b" 88 + integrity sha512-CULZ01pSJDltLS/Gc9MMrhFzB6OM3ezyZw7KoeLT/sBfwgA1ddA4mWdTh7DIRosPRigXtA05bnoiCutZbQDo+Q== 89 89 dependencies: 90 - "@atproto/common-web" "^0.4.10" 90 + "@atproto/common-web" "^0.4.11" 91 91 "@atproto/lexicon" "^0.6.0" 92 92 "@atproto/syntax" "^0.4.2" 93 93 "@atproto/xrpc" "^0.7.7" ··· 208 208 pino-http "^8.2.1" 209 209 typed-emitter "^2.1.0" 210 210 211 - "@atproto/common-web@^0.4.10": 212 - version "0.4.10" 213 - resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.10.tgz#3d29df4dc3ea8f5c149161209f55e146f27867c1" 214 - integrity sha512-TLDZSgSKzT8ZgOrBrTGK87J1CXve9TEuY6NVVUBRkOMzRRtQzpFb9/ih5WVS/hnaWVvE30CfuyaetRoma+WKNw== 211 + "@atproto/common-web@^0.4.11": 212 + version "0.4.11" 213 + resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.11.tgz#eb41dc02c1ea4221388630e193d181fb098186e0" 214 + integrity sha512-VHejNmSABU8/03VrQ3e36AmT5U3UIeio+qSUqCrO1oNgrJcWfGy1rpj0FVtUugWF8Un29+yzkukzWGZfXL70rQ== 215 215 dependencies: 216 - "@atproto/lex-data" "0.0.6" 217 - "@atproto/lex-json" "0.0.6" 216 + "@atproto/lex-data" "0.0.7" 217 + "@atproto/lex-json" "0.0.7" 218 218 zod "^3.23.8" 219 219 220 220 "@atproto/common-web@^0.4.4", "@atproto/common-web@^0.4.6": ··· 415 415 uint8arrays "3.0.0" 416 416 unicode-segmenter "^0.14.0" 417 417 418 - "@atproto/lex-data@0.0.6": 419 - version "0.0.6" 420 - resolved "https://registry.yarnpkg.com/@atproto/lex-data/-/lex-data-0.0.6.tgz#280d05ec9579ab091dc4a8b696ebbeddcb8ee37d" 421 - integrity sha512-MBNB4ghRJQzuXK1zlUPljpPbQcF1LZ5dzxy274KqPt4p3uPuRw0mHjgcCoWzRUNBQC685WMQR4IN9DHtsnG57A== 418 + "@atproto/lex-data@0.0.7": 419 + version "0.0.7" 420 + resolved "https://registry.yarnpkg.com/@atproto/lex-data/-/lex-data-0.0.7.tgz#6aa87423f6d47849bec8ff3ca0b00ce93964adc8" 421 + integrity sha512-W/Q5o9o7n2Sv3UywckChu01X5lwQUtaiiOkGJLnRsdkQTyC6813nPgY+p2sG7NwwM+82lu+FUV9fE/Ul3VqaJw== 422 422 dependencies: 423 423 "@atproto/syntax" "0.4.2" 424 424 multiformats "^9.9.0" ··· 451 451 "@atproto/lex-data" "0.0.3" 452 452 tslib "^2.8.1" 453 453 454 - "@atproto/lex-json@0.0.6": 455 - version "0.0.6" 456 - resolved "https://registry.yarnpkg.com/@atproto/lex-json/-/lex-json-0.0.6.tgz#50618efb1cc11708b3897de14dcd3bd9c06e064d" 457 - integrity sha512-EILnN5cditPvf+PCNjXt7reMuzjugxAL1fpSzmzJbEMGMUwxOf5pPWxRsaA/M3Boip4NQZ+6DVrPOGUMlnqceg== 454 + "@atproto/lex-json@0.0.7": 455 + version "0.0.7" 456 + resolved "https://registry.yarnpkg.com/@atproto/lex-json/-/lex-json-0.0.7.tgz#c06e1fc3e06d739bbb74694f5d846055bed37866" 457 + integrity sha512-bjNPD5M/MhLfjNM7tcxuls80UgXpHqxdOxDXEUouAtZQV/nIDhGjmNUvKxOmOgnDsiZRnT2g5y3onrnjH3a44g== 458 458 dependencies: 459 - "@atproto/lex-data" "0.0.6" 459 + "@atproto/lex-data" "0.0.7" 460 460 tslib "^2.8.1" 461 461 462 462 "@atproto/lex-resolver@0.0.5":