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 ? (