diff --git a/src/components/live/GoLiveDisabledDialog.tsx b/src/components/live/GoLiveDisabledDialog.tsx new file mode 100644 index 0000000000..9a3c04286d --- /dev/null +++ b/src/components/live/GoLiveDisabledDialog.tsx @@ -0,0 +1,144 @@ +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 ( + + + + + ) +} + +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 (__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 ( + + + + + Going live is currently disabled for your account + + + + You are currently blocked from using the Go Live feature. To + appeal this moderation decision, please submit the form below. + + + + + This appeal will be sent to Bluesky's moderation service. + + + + + + + + + + + + ) +} diff --git a/src/lib/actor-status.ts b/src/lib/actor-status.ts index 7e023be44e..31e532eaa0 100644 --- a/src/lib/actor-status.ts +++ b/src/lib/actor-status.ts @@ -19,15 +19,27 @@ export function useActorStatus(actor?: bsky.profile.AnyProfileView) { return useMemo(() => { tick! // revalidate every minute - if ( - shadowed && - 'status' in shadowed && - shadowed.status && - validateStatus(shadowed.did, shadowed.status, config) && - isStatusStillActive(shadowed.status.expiresAt) - ) { + if (shadowed && 'status' in shadowed && shadowed.status) { + const isValid = validateStatus(shadowed.did, shadowed.status, config) + const isDisabled = shadowed.status.isDisabled || false + const isActive = isStatusStillActive(shadowed.status.expiresAt) + if (isValid && !isDisabled && isActive) { + return { + uri: shadowed.status.uri, + cid: shadowed.status.cid, + isDisabled: false, + isActive: true, + status: 'app.bsky.actor.status#live', + embed: shadowed.status.embed as $Typed, // temp_isStatusValid asserts this + expiresAt: shadowed.status.expiresAt!, // isStatusStillActive asserts this + record: shadowed.status.record, + } satisfies AppBskyActorDefs.StatusView + } return { - isActive: true, + uri: shadowed.status.uri, + cid: shadowed.status.cid, + isDisabled, + isActive: false, status: 'app.bsky.actor.status#live', embed: shadowed.status.embed as $Typed, // temp_isStatusValid asserts this expiresAt: shadowed.status.expiresAt!, // isStatusStillActive asserts this @@ -36,6 +48,7 @@ export function useActorStatus(actor?: bsky.profile.AnyProfileView) { } else { return { status: '', + isDisabled: false, isActive: false, record: {}, } satisfies AppBskyActorDefs.StatusView diff --git a/src/view/com/profile/ProfileMenu.tsx b/src/view/com/profile/ProfileMenu.tsx index 5fc976ecc2..287b81f400 100644 --- a/src/view/com/profile/ProfileMenu.tsx +++ b/src/view/com/profile/ProfileMenu.tsx @@ -49,6 +49,7 @@ import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/ import {StarterPack} from '#/components/icons/StarterPack' import {EditLiveDialog} from '#/components/live/EditLiveDialog' import {GoLiveDialog} from '#/components/live/GoLiveDialog' +import {GoLiveDisabledDialog} from '#/components/live/GoLiveDisabledDialog' import * as Menu from '#/components/Menu' import { ReportDialog, @@ -79,6 +80,7 @@ let ProfileMenu = ({ const [devModeEnabled] = useDevMode() const verification = useFullVerificationState({profile}) const canGoLive = useCanGoLive(currentAccount?.did) + const status = useActorStatus(profile) const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile) const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) @@ -90,6 +92,7 @@ let ProfileMenu = ({ const blockPromptControl = Prompt.usePromptControl() const loggedOutWarningPromptControl = Prompt.usePromptControl() const goLiveDialogControl = useDialogControl() + const goLiveDisabledDialogControl = useDialogControl() const addToStarterPacksDialogControl = useDialogControl() const showLoggedOutWarning = React.useMemo(() => { @@ -220,8 +223,6 @@ let ProfileMenu = ({ return v.issuer === currentAccount?.did }) ?? [] - const status = useActorStatus(profile) - return ( @@ -330,13 +331,21 @@ let ProfileMenu = ({ + onPress={ + status.isDisabled + ? goLiveDisabledDialogControl.open + : goLiveDialogControl.open + }> - {status.isActive ? ( + {status.isDisabled ? ( + Go live is disabled + ) : status.isActive ? ( Edit live status ) : ( Go live @@ -517,7 +526,12 @@ let ProfileMenu = ({ verifications={currentAccountVerifications} /> - {status.isActive ? ( + {status.isDisabled ? ( + + ) : status.isActive ? (