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 "icons:optimize": "svgo -f ./assets/icons" 74 }, 75 "dependencies": { 76 - "@atproto/api": "^0.18.11", 77 "@bitdrift/react-native": "^0.6.8", 78 "@braintree/sanitize-url": "^6.0.2", 79 "@bsky.app/alf": "^0.1.6",
··· 73 "icons:optimize": "svgo -f ./assets/icons" 74 }, 75 "dependencies": { 76 + "@atproto/api": "^0.18.13", 77 "@bitdrift/react-native": "^0.6.8", 78 "@braintree/sanitize-url": "^6.0.2", 79 "@bsky.app/alf": "^0.1.6",
+1
src/components/ProfileHoverCard/index.web.tsx
··· 389 {data && moderationOpts ? ( 390 status.isActive ? ( 391 <LiveStatus 392 profile={data} 393 embed={status.embed} 394 padding="lg"
··· 389 {data && moderationOpts ? ( 390 status.isActive ? ( 391 <LiveStatus 392 + status={status} 393 profile={data} 394 embed={status.embed} 395 padding="lg"
+7
src/components/dialogs/Context.tsx
··· 3 import {type AgeAssuranceRedirectDialogState} from '#/components/ageAssurance/AgeAssuranceRedirectDialog' 4 import * as Dialog from '#/components/Dialog' 5 import {type Screen} from '#/components/dialogs/EmailDialog/types' 6 7 type Control = Dialog.DialogControlProps 8 ··· 24 share?: boolean 25 }> 26 ageAssuranceRedirectDialogControl: StatefulControl<AgeAssuranceRedirectDialogState> 27 } 28 29 const ControlsContext = createContext<ControlsContext | null>(null) ··· 51 }>() 52 const ageAssuranceRedirectDialogControl = 53 useStatefulDialogControl<AgeAssuranceRedirectDialogState>() 54 55 const ctx = useMemo<ControlsContext>( 56 () => ({ ··· 60 emailDialogControl, 61 linkWarningDialogControl, 62 ageAssuranceRedirectDialogControl, 63 }), 64 [ 65 mutedWordsDialogControl, ··· 68 emailDialogControl, 69 linkWarningDialogControl, 70 ageAssuranceRedirectDialogControl, 71 ], 72 ) 73
··· 3 import {type AgeAssuranceRedirectDialogState} from '#/components/ageAssurance/AgeAssuranceRedirectDialog' 4 import * as Dialog from '#/components/Dialog' 5 import {type Screen} from '#/components/dialogs/EmailDialog/types' 6 + import {type ReportSubject} from '#/components/moderation/ReportDialog' 7 8 type Control = Dialog.DialogControlProps 9 ··· 25 share?: boolean 26 }> 27 ageAssuranceRedirectDialogControl: StatefulControl<AgeAssuranceRedirectDialogState> 28 + reportDialogControl: StatefulControl<{subject: ReportSubject}> 29 } 30 31 const ControlsContext = createContext<ControlsContext | null>(null) ··· 53 }>() 54 const ageAssuranceRedirectDialogControl = 55 useStatefulDialogControl<AgeAssuranceRedirectDialogState>() 56 + const reportDialogControl = useStatefulDialogControl<{ 57 + subject: ReportSubject 58 + }>() 59 60 const ctx = useMemo<ControlsContext>( 61 () => ({ ··· 65 emailDialogControl, 66 linkWarningDialogControl, 67 ageAssuranceRedirectDialogControl, 68 + reportDialogControl, 69 }), 70 [ 71 mutedWordsDialogControl, ··· 74 emailDialogControl, 75 linkWarningDialogControl, 76 ageAssuranceRedirectDialogControl, 77 + reportDialogControl, 78 ], 79 ) 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 import {android, atoms as a, platform, tokens, useTheme, web} from '#/alf' 18 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 19 import * as Dialog from '#/components/Dialog' 20 import * as ProfileCard from '#/components/ProfileCard' 21 import {Text} from '#/components/Typography' 22 import type * as bsky from '#/types/bsky' ··· 28 control, 29 profile, 30 embed, 31 }: { 32 control: Dialog.DialogControlProps 33 profile: bsky.profile.AnyProfileView ··· 38 return ( 39 <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> 40 <Dialog.Handle difference={!!embed.external.thumb} /> 41 - <DialogInner profile={profile} embed={embed} navigation={navigation} /> 42 </Dialog.Outer> 43 ) 44 } ··· 47 profile, 48 embed, 49 navigation, 50 }: { 51 profile: bsky.profile.AnyProfileView 52 embed: AppBskyEmbedExternal.View 53 navigation: NavigationProp 54 }) { 55 const {_} = useLingui() 56 const control = Dialog.useDialogContext() ··· 69 contentContainerStyle={[a.pt_0, a.px_0]} 70 style={[web({maxWidth: 420}), a.overflow_hidden]}> 71 <LiveStatus 72 profile={profile} 73 embed={embed} 74 onPressOpenProfile={onPressOpenProfile} ··· 79 } 80 81 export function LiveStatus({ 82 profile, 83 embed, 84 padding = 'xl', 85 onPressOpenProfile, 86 }: { 87 profile: bsky.profile.AnyProfileView 88 embed: AppBskyEmbedExternal.View 89 padding?: 'lg' | 'xl' ··· 94 const queryClient = useQueryClient() 95 const openLink = useOpenLink() 96 const moderationOpts = useModerationOpts() 97 98 return ( 99 <> ··· 205 </Button> 206 </ProfileCard.Header> 207 )} 208 - <Text 209 style={[ 210 - a.w_full, 211 - a.text_center, 212 - t.atoms.text_contrast_low, 213 - a.text_sm, 214 ]}> 215 - <Trans>Live feature is in beta testing</Trans> 216 - </Text> 217 </View> 218 </> 219 )
··· 17 import {android, atoms as a, platform, tokens, useTheme, web} from '#/alf' 18 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 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' 23 import * as ProfileCard from '#/components/ProfileCard' 24 import {Text} from '#/components/Typography' 25 import type * as bsky from '#/types/bsky' ··· 31 control, 32 profile, 33 embed, 34 + status, 35 }: { 36 control: Dialog.DialogControlProps 37 profile: bsky.profile.AnyProfileView ··· 42 return ( 43 <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> 44 <Dialog.Handle difference={!!embed.external.thumb} /> 45 + <DialogInner 46 + status={status} 47 + profile={profile} 48 + embed={embed} 49 + navigation={navigation} 50 + /> 51 </Dialog.Outer> 52 ) 53 } ··· 56 profile, 57 embed, 58 navigation, 59 + status, 60 }: { 61 profile: bsky.profile.AnyProfileView 62 embed: AppBskyEmbedExternal.View 63 navigation: NavigationProp 64 + status: AppBskyActorDefs.StatusView 65 }) { 66 const {_} = useLingui() 67 const control = Dialog.useDialogContext() ··· 80 contentContainerStyle={[a.pt_0, a.px_0]} 81 style={[web({maxWidth: 420}), a.overflow_hidden]}> 82 <LiveStatus 83 + status={status} 84 profile={profile} 85 embed={embed} 86 onPressOpenProfile={onPressOpenProfile} ··· 91 } 92 93 export function LiveStatus({ 94 + status, 95 profile, 96 embed, 97 padding = 'xl', 98 onPressOpenProfile, 99 }: { 100 + status: AppBskyActorDefs.StatusView 101 profile: bsky.profile.AnyProfileView 102 embed: AppBskyEmbedExternal.View 103 padding?: 'lg' | 'xl' ··· 108 const queryClient = useQueryClient() 109 const openLink = useOpenLink() 110 const moderationOpts = useModerationOpts() 111 + const reportDialogControl = useGlobalReportDialogControl() 112 + const dialogContext = Dialog.useDialogContext() 113 114 return ( 115 <> ··· 221 </Button> 222 </ProfileCard.Header> 223 )} 224 + <View 225 style={[ 226 + a.flex_row, 227 + a.align_center, 228 + a.justify_between, 229 + a.flex_1, 230 + a.pt_sm, 231 ]}> 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> 261 </View> 262 </> 263 )
+1
src/components/moderation/ReportDialog/action.ts
··· 70 } 71 break 72 } 73 case 'post': 74 case 'list': 75 case 'feed':
··· 70 } 71 break 72 } 73 + case 'status': 74 case 'post': 75 case 'list': 76 case 'feed':
+9
src/components/moderation/ReportDialog/const.ts
··· 3 ToolsOzoneReportDefs as OzoneReportDefs, 4 } from '@atproto/api' 5 6 export const DMCA_LINK = 'https://bsky.social/about/support/copyright' 7 export const SUPPORT_PAGE = 'https://bsky.social/about/support' 8 ··· 112 OzoneReportDefs.REASONCHILDSAFETYOTHER, 113 OzoneReportDefs.REASONVIOLENCEEXTREMISTCONTENT, 114 ])
··· 3 ToolsOzoneReportDefs as OzoneReportDefs, 4 } from '@atproto/api' 5 6 + import {type ParsedReportSubject} from '#/components/moderation/ReportDialog/types' 7 + 8 export const DMCA_LINK = 'https://bsky.social/about/support/copyright' 9 export const SUPPORT_PAGE = 'https://bsky.social/about/support' 10 ··· 114 OzoneReportDefs.REASONCHILDSAFETYOTHER, 115 OzoneReportDefs.REASONVIOLENCEEXTREMISTCONTENT, 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 subtitle: _(msg`Why should this user be reviewed?`), 15 } 16 } 17 case 'post': { 18 return { 19 title: _(msg`Report this post`),
··· 14 subtitle: _(msg`Why should this user be reviewed?`), 15 } 16 } 17 + case 'status': { 18 + return { 19 + title: _(msg`Report this livestream`), 20 + subtitle: _(msg`Why should this livestream be reviewed?`), 21 + } 22 + } 23 case 'post': { 24 return { 25 title: _(msg`Report this post`),
+19 -10
src/components/moderation/ReportDialog/index.tsx
··· 16 import * as Admonition from '#/components/Admonition' 17 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 18 import * as Dialog from '#/components/Dialog' 19 import {useDelayedLoading} from '#/components/hooks/useDelayedLoading' 20 import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as Retry} from '#/components/icons/ArrowRotate' 21 import { ··· 31 import {useSubmitReportMutation} from './action' 32 import { 33 BSKY_LABELER_ONLY_REPORT_REASONS, 34 NEW_TO_OLD_REASONS_MAP, 35 SUPPORT_PAGE, 36 } from './const' ··· 44 useReportOptions, 45 } from './utils/useReportOptions' 46 47 export {useDialogControl as useReportDialogControl} from '#/components/Dialog' 48 49 const logger = Logger.create(Logger.Context.ReportDialog) 50 51 export function ReportDialog( 52 props: Omit<ReportDialogProps, 'subject'> & { 53 - subject: ReportSubject 54 }, 55 ) { 56 const subject = React.useMemo( 57 - () => parseReportSubject(props.subject), 58 [props.subject], 59 ) 60 const onClose = React.useCallback(() => { ··· 116 const isBskyOnlyReason = state?.selectedOption?.reason 117 ? BSKY_LABELER_ONLY_REPORT_REASONS.has(state.selectedOption.reason) 118 : false 119 - // some subjects (chats) only go to Bluesky 120 - const isBskyOnlySubject = props.subject.type === 'convoMessage' 121 122 /** 123 * Labelers that support this `subject` and its NSID collection ··· 824 {title} 825 </Text> 826 <Text 827 - style={[ 828 - a.text_sm, 829 - , 830 - a.leading_snug, 831 - t.atoms.text_contrast_medium, 832 - ]}> 833 <Trans>By {sanitizeHandle(labeler.creator.handle, '@')}</Trans> 834 </Text> 835 </View>
··· 16 import * as Admonition from '#/components/Admonition' 17 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 18 import * as Dialog from '#/components/Dialog' 19 + import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' 20 import {useDelayedLoading} from '#/components/hooks/useDelayedLoading' 21 import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as Retry} from '#/components/icons/ArrowRotate' 22 import { ··· 32 import {useSubmitReportMutation} from './action' 33 import { 34 BSKY_LABELER_ONLY_REPORT_REASONS, 35 + BSKY_LABELER_ONLY_SUBJECT_TYPES, 36 NEW_TO_OLD_REASONS_MAP, 37 SUPPORT_PAGE, 38 } from './const' ··· 46 useReportOptions, 47 } from './utils/useReportOptions' 48 49 + export {type ReportSubject} from './types' 50 export {useDialogControl as useReportDialogControl} from '#/components/Dialog' 51 52 + export function useGlobalReportDialogControl() { 53 + return useGlobalDialogsControlContext().reportDialogControl 54 + } 55 + 56 const logger = Logger.create(Logger.Context.ReportDialog) 57 58 + export function GlobalReportDialog() { 59 + const {value, control} = useGlobalReportDialogControl() 60 + return <ReportDialog control={control} subject={value?.subject} /> 61 + } 62 + 63 export function ReportDialog( 64 props: Omit<ReportDialogProps, 'subject'> & { 65 + subject?: ReportSubject 66 }, 67 ) { 68 const subject = React.useMemo( 69 + () => (props.subject ? parseReportSubject(props.subject) : undefined), 70 [props.subject], 71 ) 72 const onClose = React.useCallback(() => { ··· 128 const isBskyOnlyReason = state?.selectedOption?.reason 129 ? BSKY_LABELER_ONLY_REPORT_REASONS.has(state.selectedOption.reason) 130 : false 131 + // some subjects ONLY go to Bluesky 132 + const isBskyOnlySubject = BSKY_LABELER_ONLY_SUBJECT_TYPES.has( 133 + props.subject.type, 134 + ) 135 136 /** 137 * Labelers that support this `subject` and its NSID collection ··· 838 {title} 839 </Text> 840 <Text 841 + style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}> 842 <Trans>By {sanitizeHandle(labeler.creator.handle, '@')}</Trans> 843 </Text> 844 </View>
+7
src/components/moderation/ReportDialog/types.ts
··· 18 | $Typed<AppBskyActorDefs.ProfileViewBasic> 19 | $Typed<AppBskyActorDefs.ProfileView> 20 | $Typed<AppBskyActorDefs.ProfileViewDetailed> 21 | $Typed<AppBskyGraphDefs.ListView> 22 | $Typed<AppBskyFeedDefs.GeneratorView> 23 | $Typed<AppBskyGraphDefs.StarterPackView> ··· 37 link: boolean 38 quote: boolean 39 } 40 } 41 | { 42 type: 'list'
··· 18 | $Typed<AppBskyActorDefs.ProfileViewBasic> 19 | $Typed<AppBskyActorDefs.ProfileView> 20 | $Typed<AppBskyActorDefs.ProfileViewDetailed> 21 + | $Typed<AppBskyActorDefs.StatusView> 22 | $Typed<AppBskyGraphDefs.ListView> 23 | $Typed<AppBskyFeedDefs.GeneratorView> 24 | $Typed<AppBskyGraphDefs.StarterPackView> ··· 38 link: boolean 39 quote: boolean 40 } 41 + } 42 + | { 43 + type: 'status' 44 + uri: string 45 + cid: string 46 + nsid: string 47 } 48 | { 49 type: 'list'
+8
src/components/moderation/ReportDialog/utils/parseReportSubject.ts
··· 33 did: subject.did, 34 nsid: 'app.bsky.actor.profile', 35 } 36 } else if (AppBskyGraphDefs.isListView(subject)) { 37 return { 38 type: 'list',
··· 33 did: subject.did, 34 nsid: 'app.bsky.actor.profile', 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 + } 44 } else if (AppBskyGraphDefs.isListView(subject)) { 45 return { 46 type: 'list',
+21 -8
src/lib/actor-status.ts
··· 19 return useMemo(() => { 20 tick! // revalidate every minute 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 - ) { 29 return { 30 - isActive: true, 31 status: 'app.bsky.actor.status#live', 32 embed: shadowed.status.embed as $Typed<AppBskyEmbedExternal.View>, // temp_isStatusValid asserts this 33 expiresAt: shadowed.status.expiresAt!, // isStatusStillActive asserts this ··· 36 } else { 37 return { 38 status: '', 39 isActive: false, 40 record: {}, 41 } satisfies AppBskyActorDefs.StatusView
··· 19 return useMemo(() => { 20 tick! // revalidate every minute 21 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 + } 38 return { 39 + uri: shadowed.status.uri, 40 + cid: shadowed.status.cid, 41 + isDisabled, 42 + isActive: false, 43 status: 'app.bsky.actor.status#live', 44 embed: shadowed.status.embed as $Typed<AppBskyEmbedExternal.View>, // temp_isStatusValid asserts this 45 expiresAt: shadowed.status.expiresAt!, // isStatusStillActive asserts this ··· 48 } else { 49 return { 50 status: '', 51 + isDisabled: false, 52 isActive: false, 53 record: {}, 54 } satisfies AppBskyActorDefs.StatusView
+22 -8
src/view/com/profile/ProfileMenu.tsx
··· 49 import {StarterPack} from '#/components/icons/StarterPack' 50 import {EditLiveDialog} from '#/components/live/EditLiveDialog' 51 import {GoLiveDialog} from '#/components/live/GoLiveDialog' 52 import * as Menu from '#/components/Menu' 53 import { 54 ReportDialog, ··· 79 const [devModeEnabled] = useDevMode() 80 const verification = useFullVerificationState({profile}) 81 const canGoLive = useCanGoLive(currentAccount?.did) 82 83 const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile) 84 const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) ··· 90 const blockPromptControl = Prompt.usePromptControl() 91 const loggedOutWarningPromptControl = Prompt.usePromptControl() 92 const goLiveDialogControl = useDialogControl() 93 const addToStarterPacksDialogControl = useDialogControl() 94 95 const showLoggedOutWarning = React.useMemo(() => { ··· 220 return v.issuer === currentAccount?.did 221 }) ?? [] 222 223 - const status = useActorStatus(profile) 224 - 225 return ( 226 <EventStopper onKeyDown={false}> 227 <Menu.Root> ··· 330 <Menu.Item 331 testID="profileHeaderDropdownListAddRemoveBtn" 332 label={ 333 - status.isActive 334 - ? _(msg`Edit live status`) 335 - : _(msg`Go live`) 336 } 337 - onPress={goLiveDialogControl.open}> 338 <Menu.ItemText> 339 - {status.isActive ? ( 340 <Trans>Edit live status</Trans> 341 ) : ( 342 <Trans>Go live</Trans> ··· 517 verifications={currentAccountVerifications} 518 /> 519 520 - {status.isActive ? ( 521 <EditLiveDialog 522 control={goLiveDialogControl} 523 status={status}
··· 49 import {StarterPack} from '#/components/icons/StarterPack' 50 import {EditLiveDialog} from '#/components/live/EditLiveDialog' 51 import {GoLiveDialog} from '#/components/live/GoLiveDialog' 52 + import {GoLiveDisabledDialog} from '#/components/live/GoLiveDisabledDialog' 53 import * as Menu from '#/components/Menu' 54 import { 55 ReportDialog, ··· 80 const [devModeEnabled] = useDevMode() 81 const verification = useFullVerificationState({profile}) 82 const canGoLive = useCanGoLive(currentAccount?.did) 83 + const status = useActorStatus(profile) 84 85 const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile) 86 const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) ··· 92 const blockPromptControl = Prompt.usePromptControl() 93 const loggedOutWarningPromptControl = Prompt.usePromptControl() 94 const goLiveDialogControl = useDialogControl() 95 + const goLiveDisabledDialogControl = useDialogControl() 96 const addToStarterPacksDialogControl = useDialogControl() 97 98 const showLoggedOutWarning = React.useMemo(() => { ··· 223 return v.issuer === currentAccount?.did 224 }) ?? [] 225 226 return ( 227 <EventStopper onKeyDown={false}> 228 <Menu.Root> ··· 331 <Menu.Item 332 testID="profileHeaderDropdownListAddRemoveBtn" 333 label={ 334 + status.isDisabled 335 + ? _(msg`Go live (disabled)`) 336 + : status.isActive 337 + ? _(msg`Edit live status`) 338 + : _(msg`Go live`) 339 } 340 + onPress={ 341 + status.isDisabled 342 + ? goLiveDisabledDialogControl.open 343 + : goLiveDialogControl.open 344 + }> 345 <Menu.ItemText> 346 + {status.isDisabled ? ( 347 + <Trans>Go live (disabled)</Trans> 348 + ) : status.isActive ? ( 349 <Trans>Edit live status</Trans> 350 ) : ( 351 <Trans>Go live</Trans> ··· 526 verifications={currentAccountVerifications} 527 /> 528 529 + {status.isDisabled ? ( 530 + <GoLiveDisabledDialog 531 + control={goLiveDisabledDialogControl} 532 + status={status} 533 + /> 534 + ) : status.isActive ? ( 535 <EditLiveDialog 536 control={goLiveDialogControl} 537 status={status}
+2
src/view/shell/index.tsx
··· 34 import {MutedWordsDialog} from '#/components/dialogs/MutedWords' 35 import {NuxDialogs} from '#/components/dialogs/nuxs' 36 import {SigninDialog} from '#/components/dialogs/Signin' 37 import { 38 Outlet as PolicyUpdateOverlayPortalOutlet, 39 usePolicyUpdateContext, ··· 117 <LinkWarningDialog /> 118 <Lightbox /> 119 <NuxDialogs /> 120 121 {/* Until policy update has been completed by the user, don't render anything that is portaled */} 122 {policyUpdateState.completed && (
··· 34 import {MutedWordsDialog} from '#/components/dialogs/MutedWords' 35 import {NuxDialogs} from '#/components/dialogs/nuxs' 36 import {SigninDialog} from '#/components/dialogs/Signin' 37 + import {GlobalReportDialog} from '#/components/moderation/ReportDialog' 38 import { 39 Outlet as PolicyUpdateOverlayPortalOutlet, 40 usePolicyUpdateContext, ··· 118 <LinkWarningDialog /> 119 <Lightbox /> 120 <NuxDialogs /> 121 + <GlobalReportDialog /> 122 123 {/* Until policy update has been completed by the user, don't render anything that is portaled */} 124 {policyUpdateState.completed && (
+2
src/view/shell/index.web.tsx
··· 24 import {NuxDialogs} from '#/components/dialogs/nuxs' 25 import {SigninDialog} from '#/components/dialogs/Signin' 26 import {useWelcomeModal} from '#/components/hooks/useWelcomeModal' 27 import { 28 Outlet as PolicyUpdateOverlayPortalOutlet, 29 usePolicyUpdateContext, ··· 73 <LinkWarningDialog /> 74 <Lightbox /> 75 <NuxDialogs /> 76 77 {welcomeModalControl.isOpen && ( 78 <WelcomeModal control={welcomeModalControl} />
··· 24 import {NuxDialogs} from '#/components/dialogs/nuxs' 25 import {SigninDialog} from '#/components/dialogs/Signin' 26 import {useWelcomeModal} from '#/components/hooks/useWelcomeModal' 27 + import {GlobalReportDialog} from '#/components/moderation/ReportDialog' 28 import { 29 Outlet as PolicyUpdateOverlayPortalOutlet, 30 usePolicyUpdateContext, ··· 74 <LinkWarningDialog /> 75 <Lightbox /> 76 <NuxDialogs /> 77 + <GlobalReportDialog /> 78 79 {welcomeModalControl.isOpen && ( 80 <WelcomeModal control={welcomeModalControl} />
+20 -20
yarn.lock
··· 82 "@atproto/xrpc" "^0.7.6" 83 "@atproto/xrpc-server" "^0.10.0" 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== 89 dependencies: 90 - "@atproto/common-web" "^0.4.10" 91 "@atproto/lexicon" "^0.6.0" 92 "@atproto/syntax" "^0.4.2" 93 "@atproto/xrpc" "^0.7.7" ··· 208 pino-http "^8.2.1" 209 typed-emitter "^2.1.0" 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== 215 dependencies: 216 - "@atproto/lex-data" "0.0.6" 217 - "@atproto/lex-json" "0.0.6" 218 zod "^3.23.8" 219 220 "@atproto/common-web@^0.4.4", "@atproto/common-web@^0.4.6": ··· 415 uint8arrays "3.0.0" 416 unicode-segmenter "^0.14.0" 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== 422 dependencies: 423 "@atproto/syntax" "0.4.2" 424 multiformats "^9.9.0" ··· 451 "@atproto/lex-data" "0.0.3" 452 tslib "^2.8.1" 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== 458 dependencies: 459 - "@atproto/lex-data" "0.0.6" 460 tslib "^2.8.1" 461 462 "@atproto/lex-resolver@0.0.5":
··· 82 "@atproto/xrpc" "^0.7.6" 83 "@atproto/xrpc-server" "^0.10.0" 84 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 dependencies: 90 + "@atproto/common-web" "^0.4.11" 91 "@atproto/lexicon" "^0.6.0" 92 "@atproto/syntax" "^0.4.2" 93 "@atproto/xrpc" "^0.7.7" ··· 208 pino-http "^8.2.1" 209 typed-emitter "^2.1.0" 210 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 dependencies: 216 + "@atproto/lex-data" "0.0.7" 217 + "@atproto/lex-json" "0.0.7" 218 zod "^3.23.8" 219 220 "@atproto/common-web@^0.4.4", "@atproto/common-web@^0.4.6": ··· 415 uint8arrays "3.0.0" 416 unicode-segmenter "^0.14.0" 417 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 dependencies: 423 "@atproto/syntax" "0.4.2" 424 multiformats "^9.9.0" ··· 451 "@atproto/lex-data" "0.0.3" 452 tslib "^2.8.1" 453 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 dependencies: 459 + "@atproto/lex-data" "0.0.7" 460 tslib "^2.8.1" 461 462 "@atproto/lex-resolver@0.0.5":