[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>
This commit is contained in:
Eric Bailey
2026-01-09 10:19:47 -06:00
committed by GitHub
parent 85a616eee0
commit edb19dd6cf
16 changed files with 325 additions and 55 deletions
@@ -0,0 +1,147 @@
import {useCallback, useState} from 'react'
import {View} from 'react-native'
import {type AppBskyActorDefs, ToolsOzoneReportDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useMutation} from '@tanstack/react-query'
import {BLUESKY_MOD_SERVICE_HEADERS} from '#/lib/constants'
import {logger} from '#/logger'
import {useAgent} from '#/state/session'
import {atoms as a, web} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {Loader} from '#/components/Loader'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
export function GoLiveDisabledDialog({
control,
status,
}: {
control: Dialog.DialogControlProps
status: AppBskyActorDefs.StatusView
}) {
return (
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
<Dialog.Handle />
<DialogInner control={control} status={status} />
</Dialog.Outer>
)
}
export function DialogInner({
control,
status,
}: {
control: Dialog.DialogControlProps
status: AppBskyActorDefs.StatusView
}) {
const {_} = useLingui()
const agent = useAgent()
const [details, setDetails] = useState('')
const {mutate, isPending} = useMutation({
mutationFn: async () => {
if (!agent.session?.did) {
throw new Error('Not logged in')
}
if (!status.uri || !status.cid) {
throw new Error('Status is missing uri or cid')
}
if (__DEV__) {
logger.info('Submitting go live appeal', {
details,
})
} else {
await agent.createModerationReport(
{
reasonType: ToolsOzoneReportDefs.REASONAPPEAL,
subject: {
$type: 'com.atproto.repo.strongRef',
uri: status.uri,
cid: status.cid,
},
reason: details,
},
{
encoding: 'application/json',
headers: BLUESKY_MOD_SERVICE_HEADERS,
},
)
}
},
onError: () => {
Toast.show(_(msg`Failed to submit appeal, please try again.`), {
type: 'error',
})
},
onSuccess: () => {
control.close()
Toast.show(_(msg({message: 'Appeal submitted', context: 'toast'})), {
type: 'success',
})
},
})
const onSubmit = useCallback(() => mutate(), [mutate])
return (
<Dialog.ScrollableInner
label={_(msg`Appeal livestream suspension`)}
style={[web({maxWidth: 400})]}>
<View style={[a.gap_lg]}>
<View style={[a.gap_md]}>
<Text
style={[
a.flex_1,
a.text_2xl,
a.font_semi_bold,
a.leading_snug,
a.pr_4xl,
]}>
<Trans>Going live is currently disabled for your account</Trans>
</Text>
<Text style={[a.text_md, a.leading_snug]}>
<Trans>
You are currently blocked from using the Go Live feature. To
appeal this moderation decision, please submit the form below.
</Trans>
</Text>
<Text style={[a.text_md, a.leading_snug]}>
<Trans>
This appeal will be sent to Bluesky's moderation service.
</Trans>
</Text>
</View>
<View style={[a.gap_md]}>
<Dialog.Input
label={_(msg`Text input field`)}
placeholder={_(
msg`Please explain why you think your Go Live access was incorrectly disabled.`,
)}
value={details}
onChangeText={setDetails}
autoFocus={true}
numberOfLines={3}
multiline
maxLength={300}
/>
<Button
testID="submitBtn"
variant="solid"
color="primary"
size="large"
onPress={onSubmit}
label={_(msg`Submit`)}>
<ButtonText>{_(msg`Submit`)}</ButtonText>
{isPending && <ButtonIcon icon={Loader} />}
</Button>
</View>
</View>
<Dialog.Close />
</Dialog.ScrollableInner>
)
}
+52 -8
View File
@@ -17,6 +17,9 @@ import {unstableCacheProfileView} from '#/state/queries/profile'
import {android, atoms as a, platform, tokens, useTheme, web} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo'
import {createStaticClick, SimpleInlineLinkText} from '#/components/Link'
import {useGlobalReportDialogControl} from '#/components/moderation/ReportDialog'
import * as ProfileCard from '#/components/ProfileCard'
import {Text} from '#/components/Typography'
import type * as bsky from '#/types/bsky'
@@ -28,6 +31,7 @@ export function LiveStatusDialog({
control,
profile,
embed,
status,
}: {
control: Dialog.DialogControlProps
profile: bsky.profile.AnyProfileView
@@ -38,7 +42,12 @@ export function LiveStatusDialog({
return (
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
<Dialog.Handle difference={!!embed.external.thumb} />
<DialogInner profile={profile} embed={embed} navigation={navigation} />
<DialogInner
status={status}
profile={profile}
embed={embed}
navigation={navigation}
/>
</Dialog.Outer>
)
}
@@ -47,10 +56,12 @@ function DialogInner({
profile,
embed,
navigation,
status,
}: {
profile: bsky.profile.AnyProfileView
embed: AppBskyEmbedExternal.View
navigation: NavigationProp
status: AppBskyActorDefs.StatusView
}) {
const {_} = useLingui()
const control = Dialog.useDialogContext()
@@ -69,6 +80,7 @@ function DialogInner({
contentContainerStyle={[a.pt_0, a.px_0]}
style={[web({maxWidth: 420}), a.overflow_hidden]}>
<LiveStatus
status={status}
profile={profile}
embed={embed}
onPressOpenProfile={onPressOpenProfile}
@@ -79,11 +91,13 @@ function DialogInner({
}
export function LiveStatus({
status,
profile,
embed,
padding = 'xl',
onPressOpenProfile,
}: {
status: AppBskyActorDefs.StatusView
profile: bsky.profile.AnyProfileView
embed: AppBskyEmbedExternal.View
padding?: 'lg' | 'xl'
@@ -94,6 +108,8 @@ export function LiveStatus({
const queryClient = useQueryClient()
const openLink = useOpenLink()
const moderationOpts = useModerationOpts()
const reportDialogControl = useGlobalReportDialogControl()
const dialogContext = Dialog.useDialogContext()
return (
<>
@@ -205,15 +221,43 @@ export function LiveStatus({
</Button>
</ProfileCard.Header>
)}
<Text
<View
style={[
a.w_full,
a.text_center,
t.atoms.text_contrast_low,
a.text_sm,
a.flex_row,
a.align_center,
a.justify_between,
a.flex_1,
a.pt_sm,
]}>
<Trans>Live feature is in beta testing</Trans>
</Text>
<View style={[a.flex_row, a.align_center, a.gap_xs, a.flex_1]}>
<CircleInfoIcon size="sm" fill={t.atoms.text_contrast_low.color} />
<Text style={[t.atoms.text_contrast_low, a.text_sm]}>
<Trans>Live feature is in beta</Trans>
</Text>
</View>
{status && (
<SimpleInlineLinkText
label={_(msg`Report this livestream`)}
{...createStaticClick(() => {
function open() {
reportDialogControl.open({
subject: {
...status,
$type: 'app.bsky.actor.defs#statusView',
},
})
}
if (dialogContext.isWithinDialog) {
dialogContext.close(open)
} else {
open()
}
})}
style={[a.text_sm, a.underline, t.atoms.text_contrast_medium]}>
<Trans>Report</Trans>
</SimpleInlineLinkText>
)}
</View>
</View>
</>
)