Files
bsky-social-app/src/components/moderation/ReportDialog/action.ts
T
Oleksii Bulenok cb3245e8e8 rework imports
2026-08-24 18:53:36 +02:00

170 lines
5.1 KiB
TypeScript

import {type AtUriString, type DidString} from '@atproto/syntax'
import {api} from '@bsky/sdk'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {useMutation} from '@tanstack/react-query'
import {logger} from '#/logger'
import {useAppviewClient} from '#/state/session'
import * as ComAtprotoModerationCreateReport from '#/lexicons/com/atproto/moderation/createReport'
import {NEW_TO_OLD_REASONS_MAP, REPORT_MOD_TOOL_NAME} from './const'
import {type ReportState} from './state'
import {type ParsedReportSubject} from './types'
type ReportInput = ComAtprotoModerationCreateReport.$InputBody
export function useSubmitReportMutation() {
const {_} = useLingui()
const client = useAppviewClient()
return useMutation({
async mutationFn({
subject,
state,
videoTimestampSeconds,
}: {
subject: ParsedReportSubject
state: ReportState
/**
* How far the viewer watched when the dialog opened, if the subject is a
* post with a video.
*/
videoTimestampSeconds?: number
}) {
if (!state.selectedOption) {
throw new Error(_(msg`Please select a reason for this report`))
}
if (!state.selectedLabeler) {
throw new Error(_(msg`Please select a moderation service`))
}
const labeler = state.selectedLabeler
const labelerSupportedReasonTypes = labeler.reasonTypes || []
let reasonType = state.selectedOption.reason
const backwardsCompatibleReasonType = NEW_TO_OLD_REASONS_MAP[reasonType]
const supportsNewReasonType =
labelerSupportedReasonTypes.includes(reasonType)
const supportsOldReasonType = labelerSupportedReasonTypes.includes(
backwardsCompatibleReasonType,
)
/*
* Only fall back for backwards compatibility if the labeler
* does not support the new reason type. If the labeler does not declare
* supported reason types, send the new version.
*/
if (supportsOldReasonType && !supportsNewReasonType) {
reasonType = backwardsCompatibleReasonType
}
let report: ReportInput
switch (subject.type) {
case 'account': {
report = {
reasonType,
reason: state.details,
subject: {
$type: 'com.atproto.admin.defs#repoRef',
// the parsed subject holds the did as a plain string
did: subject.did as DidString,
},
}
break
}
case 'status':
case 'post':
case 'list':
case 'feed':
case 'starterPack': {
report = {
reasonType,
reason: state.details,
subject: {
$type: 'com.atproto.repo.strongRef',
// the parsed subject carries an at-uri read off a view
uri: subject.uri as AtUriString,
cid: subject.cid,
},
}
break
}
case 'convoMessage': {
report = {
reasonType,
reason: state.details,
subject: toOpenSubject({
$type: 'chat.bsky.convo.defs#messageRef',
messageId: subject.message.id,
convoId: subject.convoId,
did: subject.message.sender.did,
}),
}
break
}
case 'convo': {
report = {
reasonType,
reason: state.details,
subject: toOpenSubject({
$type: 'chat.bsky.convo.defs#convoRef',
convoId: subject.convoId,
did: subject.did,
}),
}
break
}
}
const modToolMeta =
state.includeVideoTimestamp &&
videoTimestampSeconds != null &&
subject.type === 'post' &&
labeler.creator.did === api.moderation.did
? {videoTimestampSeconds}
: undefined
if (modToolMeta) {
report.modTool = {
name: REPORT_MOD_TOOL_NAME,
meta: modToolMeta,
}
}
if (__DEV__) {
logger.info('Submitting report (dry run)', {
labeler: {
handle: labeler.creator.handle,
},
report,
})
} else {
/*
* Reports go to the labeler the user selected rather than Bluesky's, so
* the proxy target is built per call from that labeler's creator did.
*/
await client.call(ComAtprotoModerationCreateReport, report, {
service: `${labeler.creator.did}#atproto_labeler`,
})
}
},
})
}
/**
* Widen a chat convo ref into `createReport`'s subject union.
*
* The lexicon declares only `com.atproto.admin.defs#repoRef` and
* `com.atproto.repo.strongRef`, but leaves the union OPEN, and the chat service
* accepts its own refs there. An open union types its unknown arm as
* `{$type: Unknown$Type}`, which a concrete chat ref does not structurally
* satisfy, so the widening is asserted here once rather than at each call site.
*/
function toOpenSubject(ref: {
$type: string
[key: string]: unknown
}): ReportInput['subject'] {
return ref as unknown as ReportInput['subject']
}