[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:
+1
-1
@@ -73,7 +73,7 @@
|
|||||||
"icons:optimize": "svgo -f ./assets/icons"
|
"icons:optimize": "svgo -f ./assets/icons"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atproto/api": "^0.18.11",
|
"@atproto/api": "^0.18.13",
|
||||||
"@bitdrift/react-native": "^0.6.8",
|
"@bitdrift/react-native": "^0.6.8",
|
||||||
"@braintree/sanitize-url": "^6.0.2",
|
"@braintree/sanitize-url": "^6.0.2",
|
||||||
"@bsky.app/alf": "^0.1.6",
|
"@bsky.app/alf": "^0.1.6",
|
||||||
|
|||||||
@@ -389,6 +389,7 @@ let Card = ({
|
|||||||
{data && moderationOpts ? (
|
{data && moderationOpts ? (
|
||||||
status.isActive ? (
|
status.isActive ? (
|
||||||
<LiveStatus
|
<LiveStatus
|
||||||
|
status={status}
|
||||||
profile={data}
|
profile={data}
|
||||||
embed={status.embed}
|
embed={status.embed}
|
||||||
padding="lg"
|
padding="lg"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {createContext, useContext, useMemo, useState} from 'react'
|
|||||||
import {type AgeAssuranceRedirectDialogState} from '#/components/ageAssurance/AgeAssuranceRedirectDialog'
|
import {type AgeAssuranceRedirectDialogState} from '#/components/ageAssurance/AgeAssuranceRedirectDialog'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {type Screen} from '#/components/dialogs/EmailDialog/types'
|
import {type Screen} from '#/components/dialogs/EmailDialog/types'
|
||||||
|
import {type ReportSubject} from '#/components/moderation/ReportDialog'
|
||||||
|
|
||||||
type Control = Dialog.DialogControlProps
|
type Control = Dialog.DialogControlProps
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ type ControlsContext = {
|
|||||||
share?: boolean
|
share?: boolean
|
||||||
}>
|
}>
|
||||||
ageAssuranceRedirectDialogControl: StatefulControl<AgeAssuranceRedirectDialogState>
|
ageAssuranceRedirectDialogControl: StatefulControl<AgeAssuranceRedirectDialogState>
|
||||||
|
reportDialogControl: StatefulControl<{subject: ReportSubject}>
|
||||||
}
|
}
|
||||||
|
|
||||||
const ControlsContext = createContext<ControlsContext | null>(null)
|
const ControlsContext = createContext<ControlsContext | null>(null)
|
||||||
@@ -51,6 +53,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
}>()
|
}>()
|
||||||
const ageAssuranceRedirectDialogControl =
|
const ageAssuranceRedirectDialogControl =
|
||||||
useStatefulDialogControl<AgeAssuranceRedirectDialogState>()
|
useStatefulDialogControl<AgeAssuranceRedirectDialogState>()
|
||||||
|
const reportDialogControl = useStatefulDialogControl<{
|
||||||
|
subject: ReportSubject
|
||||||
|
}>()
|
||||||
|
|
||||||
const ctx = useMemo<ControlsContext>(
|
const ctx = useMemo<ControlsContext>(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -60,6 +65,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
emailDialogControl,
|
emailDialogControl,
|
||||||
linkWarningDialogControl,
|
linkWarningDialogControl,
|
||||||
ageAssuranceRedirectDialogControl,
|
ageAssuranceRedirectDialogControl,
|
||||||
|
reportDialogControl,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
mutedWordsDialogControl,
|
mutedWordsDialogControl,
|
||||||
@@ -68,6 +74,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
emailDialogControl,
|
emailDialogControl,
|
||||||
linkWarningDialogControl,
|
linkWarningDialogControl,
|
||||||
ageAssuranceRedirectDialogControl,
|
ageAssuranceRedirectDialogControl,
|
||||||
|
reportDialogControl,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -17,6 +17,9 @@ import {unstableCacheProfileView} from '#/state/queries/profile'
|
|||||||
import {android, atoms as a, platform, tokens, useTheme, web} from '#/alf'
|
import {android, atoms as a, platform, tokens, useTheme, web} from '#/alf'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
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 * as ProfileCard from '#/components/ProfileCard'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
@@ -28,6 +31,7 @@ export function LiveStatusDialog({
|
|||||||
control,
|
control,
|
||||||
profile,
|
profile,
|
||||||
embed,
|
embed,
|
||||||
|
status,
|
||||||
}: {
|
}: {
|
||||||
control: Dialog.DialogControlProps
|
control: Dialog.DialogControlProps
|
||||||
profile: bsky.profile.AnyProfileView
|
profile: bsky.profile.AnyProfileView
|
||||||
@@ -38,7 +42,12 @@ export function LiveStatusDialog({
|
|||||||
return (
|
return (
|
||||||
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
|
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
|
||||||
<Dialog.Handle difference={!!embed.external.thumb} />
|
<Dialog.Handle difference={!!embed.external.thumb} />
|
||||||
<DialogInner profile={profile} embed={embed} navigation={navigation} />
|
<DialogInner
|
||||||
|
status={status}
|
||||||
|
profile={profile}
|
||||||
|
embed={embed}
|
||||||
|
navigation={navigation}
|
||||||
|
/>
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -47,10 +56,12 @@ function DialogInner({
|
|||||||
profile,
|
profile,
|
||||||
embed,
|
embed,
|
||||||
navigation,
|
navigation,
|
||||||
|
status,
|
||||||
}: {
|
}: {
|
||||||
profile: bsky.profile.AnyProfileView
|
profile: bsky.profile.AnyProfileView
|
||||||
embed: AppBskyEmbedExternal.View
|
embed: AppBskyEmbedExternal.View
|
||||||
navigation: NavigationProp
|
navigation: NavigationProp
|
||||||
|
status: AppBskyActorDefs.StatusView
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const control = Dialog.useDialogContext()
|
const control = Dialog.useDialogContext()
|
||||||
@@ -69,6 +80,7 @@ function DialogInner({
|
|||||||
contentContainerStyle={[a.pt_0, a.px_0]}
|
contentContainerStyle={[a.pt_0, a.px_0]}
|
||||||
style={[web({maxWidth: 420}), a.overflow_hidden]}>
|
style={[web({maxWidth: 420}), a.overflow_hidden]}>
|
||||||
<LiveStatus
|
<LiveStatus
|
||||||
|
status={status}
|
||||||
profile={profile}
|
profile={profile}
|
||||||
embed={embed}
|
embed={embed}
|
||||||
onPressOpenProfile={onPressOpenProfile}
|
onPressOpenProfile={onPressOpenProfile}
|
||||||
@@ -79,11 +91,13 @@ function DialogInner({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function LiveStatus({
|
export function LiveStatus({
|
||||||
|
status,
|
||||||
profile,
|
profile,
|
||||||
embed,
|
embed,
|
||||||
padding = 'xl',
|
padding = 'xl',
|
||||||
onPressOpenProfile,
|
onPressOpenProfile,
|
||||||
}: {
|
}: {
|
||||||
|
status: AppBskyActorDefs.StatusView
|
||||||
profile: bsky.profile.AnyProfileView
|
profile: bsky.profile.AnyProfileView
|
||||||
embed: AppBskyEmbedExternal.View
|
embed: AppBskyEmbedExternal.View
|
||||||
padding?: 'lg' | 'xl'
|
padding?: 'lg' | 'xl'
|
||||||
@@ -94,6 +108,8 @@ export function LiveStatus({
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const openLink = useOpenLink()
|
const openLink = useOpenLink()
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
|
const reportDialogControl = useGlobalReportDialogControl()
|
||||||
|
const dialogContext = Dialog.useDialogContext()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -205,16 +221,44 @@ export function LiveStatus({
|
|||||||
</Button>
|
</Button>
|
||||||
</ProfileCard.Header>
|
</ProfileCard.Header>
|
||||||
)}
|
)}
|
||||||
<Text
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.w_full,
|
a.flex_row,
|
||||||
a.text_center,
|
a.align_center,
|
||||||
t.atoms.text_contrast_low,
|
a.justify_between,
|
||||||
a.text_sm,
|
a.flex_1,
|
||||||
|
a.pt_sm,
|
||||||
]}>
|
]}>
|
||||||
<Trans>Live feature is in beta testing</Trans>
|
<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>
|
</Text>
|
||||||
</View>
|
</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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export function useSubmitReportMutation() {
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case 'status':
|
||||||
case 'post':
|
case 'post':
|
||||||
case 'list':
|
case 'list':
|
||||||
case 'feed':
|
case 'feed':
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import {
|
|||||||
ToolsOzoneReportDefs as OzoneReportDefs,
|
ToolsOzoneReportDefs as OzoneReportDefs,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
|
|
||||||
|
import {type ParsedReportSubject} from '#/components/moderation/ReportDialog/types'
|
||||||
|
|
||||||
export const DMCA_LINK = 'https://bsky.social/about/support/copyright'
|
export const DMCA_LINK = 'https://bsky.social/about/support/copyright'
|
||||||
export const SUPPORT_PAGE = 'https://bsky.social/about/support'
|
export const SUPPORT_PAGE = 'https://bsky.social/about/support'
|
||||||
|
|
||||||
@@ -112,3 +114,10 @@ export const BSKY_LABELER_ONLY_REPORT_REASONS: Set<OzoneReportDefs.ReasonType> =
|
|||||||
OzoneReportDefs.REASONCHILDSAFETYOTHER,
|
OzoneReportDefs.REASONCHILDSAFETYOTHER,
|
||||||
OzoneReportDefs.REASONVIOLENCEEXTREMISTCONTENT,
|
OzoneReportDefs.REASONVIOLENCEEXTREMISTCONTENT,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set of _parsed_ subject types that should only be sent to Bluesky's
|
||||||
|
* moderation service.
|
||||||
|
*/
|
||||||
|
export const BSKY_LABELER_ONLY_SUBJECT_TYPES: Set<ParsedReportSubject['type']> =
|
||||||
|
new Set(['convoMessage', 'status'])
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ export function useCopyForSubject(subject: ParsedReportSubject) {
|
|||||||
subtitle: _(msg`Why should this user be reviewed?`),
|
subtitle: _(msg`Why should this user be reviewed?`),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case 'status': {
|
||||||
|
return {
|
||||||
|
title: _(msg`Report this livestream`),
|
||||||
|
subtitle: _(msg`Why should this livestream be reviewed?`),
|
||||||
|
}
|
||||||
|
}
|
||||||
case 'post': {
|
case 'post': {
|
||||||
return {
|
return {
|
||||||
title: _(msg`Report this post`),
|
title: _(msg`Report this post`),
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {atoms as a, useGutters, useTheme} from '#/alf'
|
|||||||
import * as Admonition from '#/components/Admonition'
|
import * as Admonition from '#/components/Admonition'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
|
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
|
||||||
import {useDelayedLoading} from '#/components/hooks/useDelayedLoading'
|
import {useDelayedLoading} from '#/components/hooks/useDelayedLoading'
|
||||||
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as Retry} from '#/components/icons/ArrowRotate'
|
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as Retry} from '#/components/icons/ArrowRotate'
|
||||||
import {
|
import {
|
||||||
@@ -31,6 +32,7 @@ import {Text} from '#/components/Typography'
|
|||||||
import {useSubmitReportMutation} from './action'
|
import {useSubmitReportMutation} from './action'
|
||||||
import {
|
import {
|
||||||
BSKY_LABELER_ONLY_REPORT_REASONS,
|
BSKY_LABELER_ONLY_REPORT_REASONS,
|
||||||
|
BSKY_LABELER_ONLY_SUBJECT_TYPES,
|
||||||
NEW_TO_OLD_REASONS_MAP,
|
NEW_TO_OLD_REASONS_MAP,
|
||||||
SUPPORT_PAGE,
|
SUPPORT_PAGE,
|
||||||
} from './const'
|
} from './const'
|
||||||
@@ -44,17 +46,27 @@ import {
|
|||||||
useReportOptions,
|
useReportOptions,
|
||||||
} from './utils/useReportOptions'
|
} from './utils/useReportOptions'
|
||||||
|
|
||||||
|
export {type ReportSubject} from './types'
|
||||||
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
|
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
|
||||||
|
|
||||||
|
export function useGlobalReportDialogControl() {
|
||||||
|
return useGlobalDialogsControlContext().reportDialogControl
|
||||||
|
}
|
||||||
|
|
||||||
const logger = Logger.create(Logger.Context.ReportDialog)
|
const logger = Logger.create(Logger.Context.ReportDialog)
|
||||||
|
|
||||||
|
export function GlobalReportDialog() {
|
||||||
|
const {value, control} = useGlobalReportDialogControl()
|
||||||
|
return <ReportDialog control={control} subject={value?.subject} />
|
||||||
|
}
|
||||||
|
|
||||||
export function ReportDialog(
|
export function ReportDialog(
|
||||||
props: Omit<ReportDialogProps, 'subject'> & {
|
props: Omit<ReportDialogProps, 'subject'> & {
|
||||||
subject: ReportSubject
|
subject?: ReportSubject
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const subject = React.useMemo(
|
const subject = React.useMemo(
|
||||||
() => parseReportSubject(props.subject),
|
() => (props.subject ? parseReportSubject(props.subject) : undefined),
|
||||||
[props.subject],
|
[props.subject],
|
||||||
)
|
)
|
||||||
const onClose = React.useCallback(() => {
|
const onClose = React.useCallback(() => {
|
||||||
@@ -116,8 +128,10 @@ function Inner(props: ReportDialogProps) {
|
|||||||
const isBskyOnlyReason = state?.selectedOption?.reason
|
const isBskyOnlyReason = state?.selectedOption?.reason
|
||||||
? BSKY_LABELER_ONLY_REPORT_REASONS.has(state.selectedOption.reason)
|
? BSKY_LABELER_ONLY_REPORT_REASONS.has(state.selectedOption.reason)
|
||||||
: false
|
: false
|
||||||
// some subjects (chats) only go to Bluesky
|
// some subjects ONLY go to Bluesky
|
||||||
const isBskyOnlySubject = props.subject.type === 'convoMessage'
|
const isBskyOnlySubject = BSKY_LABELER_ONLY_SUBJECT_TYPES.has(
|
||||||
|
props.subject.type,
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Labelers that support this `subject` and its NSID collection
|
* Labelers that support this `subject` and its NSID collection
|
||||||
@@ -824,12 +838,7 @@ function LabelerCard({
|
|||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||||
a.text_sm,
|
|
||||||
,
|
|
||||||
a.leading_snug,
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
]}>
|
|
||||||
<Trans>By {sanitizeHandle(labeler.creator.handle, '@')}</Trans>
|
<Trans>By {sanitizeHandle(labeler.creator.handle, '@')}</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export type ReportSubject =
|
|||||||
| $Typed<AppBskyActorDefs.ProfileViewBasic>
|
| $Typed<AppBskyActorDefs.ProfileViewBasic>
|
||||||
| $Typed<AppBskyActorDefs.ProfileView>
|
| $Typed<AppBskyActorDefs.ProfileView>
|
||||||
| $Typed<AppBskyActorDefs.ProfileViewDetailed>
|
| $Typed<AppBskyActorDefs.ProfileViewDetailed>
|
||||||
|
| $Typed<AppBskyActorDefs.StatusView>
|
||||||
| $Typed<AppBskyGraphDefs.ListView>
|
| $Typed<AppBskyGraphDefs.ListView>
|
||||||
| $Typed<AppBskyFeedDefs.GeneratorView>
|
| $Typed<AppBskyFeedDefs.GeneratorView>
|
||||||
| $Typed<AppBskyGraphDefs.StarterPackView>
|
| $Typed<AppBskyGraphDefs.StarterPackView>
|
||||||
@@ -38,6 +39,12 @@ export type ParsedReportSubject =
|
|||||||
quote: boolean
|
quote: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'status'
|
||||||
|
uri: string
|
||||||
|
cid: string
|
||||||
|
nsid: string
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: 'list'
|
type: 'list'
|
||||||
uri: string
|
uri: string
|
||||||
|
|||||||
@@ -33,6 +33,14 @@ export function parseReportSubject(
|
|||||||
did: subject.did,
|
did: subject.did,
|
||||||
nsid: 'app.bsky.actor.profile',
|
nsid: 'app.bsky.actor.profile',
|
||||||
}
|
}
|
||||||
|
} else if (AppBskyActorDefs.isStatusView(subject)) {
|
||||||
|
if (!subject.uri || !subject.cid) return
|
||||||
|
return {
|
||||||
|
type: 'status',
|
||||||
|
uri: subject.uri,
|
||||||
|
cid: subject.cid,
|
||||||
|
nsid: 'app.bsky.actor.status',
|
||||||
|
}
|
||||||
} else if (AppBskyGraphDefs.isListView(subject)) {
|
} else if (AppBskyGraphDefs.isListView(subject)) {
|
||||||
return {
|
return {
|
||||||
type: 'list',
|
type: 'list',
|
||||||
|
|||||||
+20
-7
@@ -19,23 +19,36 @@ export function useActorStatus(actor?: bsky.profile.AnyProfileView) {
|
|||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
tick! // revalidate every minute
|
tick! // revalidate every minute
|
||||||
|
|
||||||
if (
|
if (shadowed && 'status' in shadowed && shadowed.status) {
|
||||||
shadowed &&
|
const isValid = validateStatus(shadowed.did, shadowed.status, config)
|
||||||
'status' in shadowed &&
|
const isDisabled = shadowed.status.isDisabled || false
|
||||||
shadowed.status &&
|
const isActive = isStatusStillActive(shadowed.status.expiresAt)
|
||||||
validateStatus(shadowed.did, shadowed.status, config) &&
|
if (isValid && !isDisabled && isActive) {
|
||||||
isStatusStillActive(shadowed.status.expiresAt)
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
|
uri: shadowed.status.uri,
|
||||||
|
cid: shadowed.status.cid,
|
||||||
|
isDisabled: false,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
status: 'app.bsky.actor.status#live',
|
status: 'app.bsky.actor.status#live',
|
||||||
embed: shadowed.status.embed as $Typed<AppBskyEmbedExternal.View>, // temp_isStatusValid asserts this
|
embed: shadowed.status.embed as $Typed<AppBskyEmbedExternal.View>, // temp_isStatusValid asserts this
|
||||||
expiresAt: shadowed.status.expiresAt!, // isStatusStillActive asserts this
|
expiresAt: shadowed.status.expiresAt!, // isStatusStillActive asserts this
|
||||||
record: shadowed.status.record,
|
record: shadowed.status.record,
|
||||||
} satisfies AppBskyActorDefs.StatusView
|
} satisfies AppBskyActorDefs.StatusView
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
uri: shadowed.status.uri,
|
||||||
|
cid: shadowed.status.cid,
|
||||||
|
isDisabled,
|
||||||
|
isActive: false,
|
||||||
|
status: 'app.bsky.actor.status#live',
|
||||||
|
embed: shadowed.status.embed as $Typed<AppBskyEmbedExternal.View>, // temp_isStatusValid asserts this
|
||||||
|
expiresAt: shadowed.status.expiresAt!, // isStatusStillActive asserts this
|
||||||
|
record: shadowed.status.record,
|
||||||
|
} satisfies AppBskyActorDefs.StatusView
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
status: '',
|
status: '',
|
||||||
|
isDisabled: false,
|
||||||
isActive: false,
|
isActive: false,
|
||||||
record: {},
|
record: {},
|
||||||
} satisfies AppBskyActorDefs.StatusView
|
} satisfies AppBskyActorDefs.StatusView
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/
|
|||||||
import {StarterPack} from '#/components/icons/StarterPack'
|
import {StarterPack} from '#/components/icons/StarterPack'
|
||||||
import {EditLiveDialog} from '#/components/live/EditLiveDialog'
|
import {EditLiveDialog} from '#/components/live/EditLiveDialog'
|
||||||
import {GoLiveDialog} from '#/components/live/GoLiveDialog'
|
import {GoLiveDialog} from '#/components/live/GoLiveDialog'
|
||||||
|
import {GoLiveDisabledDialog} from '#/components/live/GoLiveDisabledDialog'
|
||||||
import * as Menu from '#/components/Menu'
|
import * as Menu from '#/components/Menu'
|
||||||
import {
|
import {
|
||||||
ReportDialog,
|
ReportDialog,
|
||||||
@@ -79,6 +80,7 @@ let ProfileMenu = ({
|
|||||||
const [devModeEnabled] = useDevMode()
|
const [devModeEnabled] = useDevMode()
|
||||||
const verification = useFullVerificationState({profile})
|
const verification = useFullVerificationState({profile})
|
||||||
const canGoLive = useCanGoLive(currentAccount?.did)
|
const canGoLive = useCanGoLive(currentAccount?.did)
|
||||||
|
const status = useActorStatus(profile)
|
||||||
|
|
||||||
const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
|
const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
|
||||||
const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
|
const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
|
||||||
@@ -90,6 +92,7 @@ let ProfileMenu = ({
|
|||||||
const blockPromptControl = Prompt.usePromptControl()
|
const blockPromptControl = Prompt.usePromptControl()
|
||||||
const loggedOutWarningPromptControl = Prompt.usePromptControl()
|
const loggedOutWarningPromptControl = Prompt.usePromptControl()
|
||||||
const goLiveDialogControl = useDialogControl()
|
const goLiveDialogControl = useDialogControl()
|
||||||
|
const goLiveDisabledDialogControl = useDialogControl()
|
||||||
const addToStarterPacksDialogControl = useDialogControl()
|
const addToStarterPacksDialogControl = useDialogControl()
|
||||||
|
|
||||||
const showLoggedOutWarning = React.useMemo(() => {
|
const showLoggedOutWarning = React.useMemo(() => {
|
||||||
@@ -220,8 +223,6 @@ let ProfileMenu = ({
|
|||||||
return v.issuer === currentAccount?.did
|
return v.issuer === currentAccount?.did
|
||||||
}) ?? []
|
}) ?? []
|
||||||
|
|
||||||
const status = useActorStatus(profile)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EventStopper onKeyDown={false}>
|
<EventStopper onKeyDown={false}>
|
||||||
<Menu.Root>
|
<Menu.Root>
|
||||||
@@ -330,13 +331,21 @@ let ProfileMenu = ({
|
|||||||
<Menu.Item
|
<Menu.Item
|
||||||
testID="profileHeaderDropdownListAddRemoveBtn"
|
testID="profileHeaderDropdownListAddRemoveBtn"
|
||||||
label={
|
label={
|
||||||
status.isActive
|
status.isDisabled
|
||||||
|
? _(msg`Go live (disabled)`)
|
||||||
|
: status.isActive
|
||||||
? _(msg`Edit live status`)
|
? _(msg`Edit live status`)
|
||||||
: _(msg`Go live`)
|
: _(msg`Go live`)
|
||||||
}
|
}
|
||||||
onPress={goLiveDialogControl.open}>
|
onPress={
|
||||||
|
status.isDisabled
|
||||||
|
? goLiveDisabledDialogControl.open
|
||||||
|
: goLiveDialogControl.open
|
||||||
|
}>
|
||||||
<Menu.ItemText>
|
<Menu.ItemText>
|
||||||
{status.isActive ? (
|
{status.isDisabled ? (
|
||||||
|
<Trans>Go live (disabled)</Trans>
|
||||||
|
) : status.isActive ? (
|
||||||
<Trans>Edit live status</Trans>
|
<Trans>Edit live status</Trans>
|
||||||
) : (
|
) : (
|
||||||
<Trans>Go live</Trans>
|
<Trans>Go live</Trans>
|
||||||
@@ -517,7 +526,12 @@ let ProfileMenu = ({
|
|||||||
verifications={currentAccountVerifications}
|
verifications={currentAccountVerifications}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{status.isActive ? (
|
{status.isDisabled ? (
|
||||||
|
<GoLiveDisabledDialog
|
||||||
|
control={goLiveDisabledDialogControl}
|
||||||
|
status={status}
|
||||||
|
/>
|
||||||
|
) : status.isActive ? (
|
||||||
<EditLiveDialog
|
<EditLiveDialog
|
||||||
control={goLiveDialogControl}
|
control={goLiveDialogControl}
|
||||||
status={status}
|
status={status}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {LinkWarningDialog} from '#/components/dialogs/LinkWarning'
|
|||||||
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
|
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
|
||||||
import {NuxDialogs} from '#/components/dialogs/nuxs'
|
import {NuxDialogs} from '#/components/dialogs/nuxs'
|
||||||
import {SigninDialog} from '#/components/dialogs/Signin'
|
import {SigninDialog} from '#/components/dialogs/Signin'
|
||||||
|
import {GlobalReportDialog} from '#/components/moderation/ReportDialog'
|
||||||
import {
|
import {
|
||||||
Outlet as PolicyUpdateOverlayPortalOutlet,
|
Outlet as PolicyUpdateOverlayPortalOutlet,
|
||||||
usePolicyUpdateContext,
|
usePolicyUpdateContext,
|
||||||
@@ -117,6 +118,7 @@ function ShellInner() {
|
|||||||
<LinkWarningDialog />
|
<LinkWarningDialog />
|
||||||
<Lightbox />
|
<Lightbox />
|
||||||
<NuxDialogs />
|
<NuxDialogs />
|
||||||
|
<GlobalReportDialog />
|
||||||
|
|
||||||
{/* Until policy update has been completed by the user, don't render anything that is portaled */}
|
{/* Until policy update has been completed by the user, don't render anything that is portaled */}
|
||||||
{policyUpdateState.completed && (
|
{policyUpdateState.completed && (
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
|
|||||||
import {NuxDialogs} from '#/components/dialogs/nuxs'
|
import {NuxDialogs} from '#/components/dialogs/nuxs'
|
||||||
import {SigninDialog} from '#/components/dialogs/Signin'
|
import {SigninDialog} from '#/components/dialogs/Signin'
|
||||||
import {useWelcomeModal} from '#/components/hooks/useWelcomeModal'
|
import {useWelcomeModal} from '#/components/hooks/useWelcomeModal'
|
||||||
|
import {GlobalReportDialog} from '#/components/moderation/ReportDialog'
|
||||||
import {
|
import {
|
||||||
Outlet as PolicyUpdateOverlayPortalOutlet,
|
Outlet as PolicyUpdateOverlayPortalOutlet,
|
||||||
usePolicyUpdateContext,
|
usePolicyUpdateContext,
|
||||||
@@ -73,6 +74,7 @@ function ShellInner() {
|
|||||||
<LinkWarningDialog />
|
<LinkWarningDialog />
|
||||||
<Lightbox />
|
<Lightbox />
|
||||||
<NuxDialogs />
|
<NuxDialogs />
|
||||||
|
<GlobalReportDialog />
|
||||||
|
|
||||||
{welcomeModalControl.isOpen && (
|
{welcomeModalControl.isOpen && (
|
||||||
<WelcomeModal control={welcomeModalControl} />
|
<WelcomeModal control={welcomeModalControl} />
|
||||||
|
|||||||
@@ -82,12 +82,12 @@
|
|||||||
"@atproto/xrpc" "^0.7.6"
|
"@atproto/xrpc" "^0.7.6"
|
||||||
"@atproto/xrpc-server" "^0.10.0"
|
"@atproto/xrpc-server" "^0.10.0"
|
||||||
|
|
||||||
"@atproto/api@^0.18.11":
|
"@atproto/api@^0.18.13":
|
||||||
version "0.18.11"
|
version "0.18.13"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.18.11.tgz#38b8bacecaae4c24bc29bd98b34f270d8be675b3"
|
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.18.13.tgz#63eee310e6715752eb87748323cf9ab57dd91e4b"
|
||||||
integrity sha512-YhgyL4rOBFOIs+e/8s5S+EjwM3T5q65IKh30ZoIrWcQS/2uteiIzg1xB+iXmexZq4Cwug0NLfRUJDViDK/ut0w==
|
integrity sha512-CULZ01pSJDltLS/Gc9MMrhFzB6OM3ezyZw7KoeLT/sBfwgA1ddA4mWdTh7DIRosPRigXtA05bnoiCutZbQDo+Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@atproto/common-web" "^0.4.10"
|
"@atproto/common-web" "^0.4.11"
|
||||||
"@atproto/lexicon" "^0.6.0"
|
"@atproto/lexicon" "^0.6.0"
|
||||||
"@atproto/syntax" "^0.4.2"
|
"@atproto/syntax" "^0.4.2"
|
||||||
"@atproto/xrpc" "^0.7.7"
|
"@atproto/xrpc" "^0.7.7"
|
||||||
@@ -208,13 +208,13 @@
|
|||||||
pino-http "^8.2.1"
|
pino-http "^8.2.1"
|
||||||
typed-emitter "^2.1.0"
|
typed-emitter "^2.1.0"
|
||||||
|
|
||||||
"@atproto/common-web@^0.4.10":
|
"@atproto/common-web@^0.4.11":
|
||||||
version "0.4.10"
|
version "0.4.11"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.10.tgz#3d29df4dc3ea8f5c149161209f55e146f27867c1"
|
resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.11.tgz#eb41dc02c1ea4221388630e193d181fb098186e0"
|
||||||
integrity sha512-TLDZSgSKzT8ZgOrBrTGK87J1CXve9TEuY6NVVUBRkOMzRRtQzpFb9/ih5WVS/hnaWVvE30CfuyaetRoma+WKNw==
|
integrity sha512-VHejNmSABU8/03VrQ3e36AmT5U3UIeio+qSUqCrO1oNgrJcWfGy1rpj0FVtUugWF8Un29+yzkukzWGZfXL70rQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@atproto/lex-data" "0.0.6"
|
"@atproto/lex-data" "0.0.7"
|
||||||
"@atproto/lex-json" "0.0.6"
|
"@atproto/lex-json" "0.0.7"
|
||||||
zod "^3.23.8"
|
zod "^3.23.8"
|
||||||
|
|
||||||
"@atproto/common-web@^0.4.4", "@atproto/common-web@^0.4.6":
|
"@atproto/common-web@^0.4.4", "@atproto/common-web@^0.4.6":
|
||||||
@@ -415,10 +415,10 @@
|
|||||||
uint8arrays "3.0.0"
|
uint8arrays "3.0.0"
|
||||||
unicode-segmenter "^0.14.0"
|
unicode-segmenter "^0.14.0"
|
||||||
|
|
||||||
"@atproto/lex-data@0.0.6":
|
"@atproto/lex-data@0.0.7":
|
||||||
version "0.0.6"
|
version "0.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/lex-data/-/lex-data-0.0.6.tgz#280d05ec9579ab091dc4a8b696ebbeddcb8ee37d"
|
resolved "https://registry.yarnpkg.com/@atproto/lex-data/-/lex-data-0.0.7.tgz#6aa87423f6d47849bec8ff3ca0b00ce93964adc8"
|
||||||
integrity sha512-MBNB4ghRJQzuXK1zlUPljpPbQcF1LZ5dzxy274KqPt4p3uPuRw0mHjgcCoWzRUNBQC685WMQR4IN9DHtsnG57A==
|
integrity sha512-W/Q5o9o7n2Sv3UywckChu01X5lwQUtaiiOkGJLnRsdkQTyC6813nPgY+p2sG7NwwM+82lu+FUV9fE/Ul3VqaJw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@atproto/syntax" "0.4.2"
|
"@atproto/syntax" "0.4.2"
|
||||||
multiformats "^9.9.0"
|
multiformats "^9.9.0"
|
||||||
@@ -451,12 +451,12 @@
|
|||||||
"@atproto/lex-data" "0.0.3"
|
"@atproto/lex-data" "0.0.3"
|
||||||
tslib "^2.8.1"
|
tslib "^2.8.1"
|
||||||
|
|
||||||
"@atproto/lex-json@0.0.6":
|
"@atproto/lex-json@0.0.7":
|
||||||
version "0.0.6"
|
version "0.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/lex-json/-/lex-json-0.0.6.tgz#50618efb1cc11708b3897de14dcd3bd9c06e064d"
|
resolved "https://registry.yarnpkg.com/@atproto/lex-json/-/lex-json-0.0.7.tgz#c06e1fc3e06d739bbb74694f5d846055bed37866"
|
||||||
integrity sha512-EILnN5cditPvf+PCNjXt7reMuzjugxAL1fpSzmzJbEMGMUwxOf5pPWxRsaA/M3Boip4NQZ+6DVrPOGUMlnqceg==
|
integrity sha512-bjNPD5M/MhLfjNM7tcxuls80UgXpHqxdOxDXEUouAtZQV/nIDhGjmNUvKxOmOgnDsiZRnT2g5y3onrnjH3a44g==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@atproto/lex-data" "0.0.6"
|
"@atproto/lex-data" "0.0.7"
|
||||||
tslib "^2.8.1"
|
tslib "^2.8.1"
|
||||||
|
|
||||||
"@atproto/lex-resolver@0.0.5":
|
"@atproto/lex-resolver@0.0.5":
|
||||||
|
|||||||
Reference in New Issue
Block a user