diff --git a/src/components/ReportDialog/SelectReportOptionView.tsx b/src/components/ReportDialog/SelectReportOptionView.tsx index b2e6f9f7b3..9fb34c1e5e 100644 --- a/src/components/ReportDialog/SelectReportOptionView.tsx +++ b/src/components/ReportDialog/SelectReportOptionView.tsx @@ -47,6 +47,9 @@ export function SelectReportOptionView({ } else if (props.params.type === 'list') { title = _(msg`Report this list`) description = _(msg`Why should this list be reviewed?`) + } else if (props.params.type === 'feedgen') { + title = _(msg`Report this feed`) + description = _(msg`Why should this feed be reviewed?`) } return { diff --git a/src/components/ReportDialog/types.ts b/src/components/ReportDialog/types.ts index 6d441c7812..0c8a1e0778 100644 --- a/src/components/ReportDialog/types.ts +++ b/src/components/ReportDialog/types.ts @@ -4,7 +4,7 @@ export type ReportDialogProps = { control: Dialog.DialogOuterProps['control'] params: | { - type: 'post' | 'list' | 'other' + type: 'post' | 'list' | 'feedgen' | 'other' uri: string cid: string } diff --git a/src/lib/build-flags.ts b/src/lib/build-flags.ts index f5889e8347..f85cbd9f84 100644 --- a/src/lib/build-flags.ts +++ b/src/lib/build-flags.ts @@ -1,4 +1,3 @@ export const LOGIN_INCLUDE_DEV_SERVERS = true export const PWI_ENABLED = true export const NEW_ONBOARDING_ENABLED = true -export const NEW_REPORT_DIALOG_ENABLED = true diff --git a/src/lib/moderation/useReportOptions.ts b/src/lib/moderation/useReportOptions.ts index 8c1afe4578..df619af8b4 100644 --- a/src/lib/moderation/useReportOptions.ts +++ b/src/lib/moderation/useReportOptions.ts @@ -79,6 +79,14 @@ export function useReportOptions(): ReportOptions { }, ...common, ], + feedgen: [ + { + reason: ComAtprotoModerationDefs.REASONVIOLATION, + title: _(msg`Name or Description Violates Community Standards`), + description: _(msg`Terms used violate community standards`), + }, + ...common, + ], other: common, } }, [_]) diff --git a/src/screens/Profile/Header/DropdownBtn.tsx b/src/screens/Profile/Header/DropdownBtn.tsx index d6b91bc323..ba7b878730 100644 --- a/src/screens/Profile/Header/DropdownBtn.tsx +++ b/src/screens/Profile/Header/DropdownBtn.tsx @@ -19,7 +19,6 @@ import {shareUrl} from 'lib/sharing' import {logger} from '#/logger' import {useSession} from '#/state/session' import {Shadow} from '#/state/cache/types' -import {NEW_REPORT_DIALOG_ENABLED} from '#/lib/build-flags' import {atoms as a, useTheme} from '#/alf' import * as Toast from 'view/com/util/Toast' @@ -40,7 +39,7 @@ export function ProfileHeaderDropdownBtn({ const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile) const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) const queryClient = useQueryClient() - const control = useReportDialogControl() + const reportDialogControl = useReportDialogControl() const invalidateProfileQuery = React.useCallback(() => { queryClient.invalidateQueries({ @@ -137,15 +136,8 @@ export function ProfileHeaderDropdownBtn({ const onPressReportAccount = React.useCallback(() => { track('ProfileHeader:ReportAccountButtonClicked') - if (NEW_REPORT_DIALOG_ENABLED) { - control.open() - } else { - openModal({ - name: 'report', - did: profile.did, - }) - } - }, [track, openModal, profile, control]) + reportDialogControl.open() + }, [track, reportDialogControl]) const isMe = React.useMemo( () => currentAccount?.did === profile.did, @@ -269,7 +261,7 @@ export function ProfileHeaderDropdownBtn({ return dropdownItems?.length ? ( <> diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx index df69ac6a8b..118376ef29 100644 --- a/src/state/modals/index.tsx +++ b/src/state/modals/index.tsx @@ -26,16 +26,6 @@ export interface EditProfileModal { onUpdate?: () => void } -export type ReportModal = { - name: 'report' -} & ( - | { - uri: string - cid: string - } - | {did: string} -) - export interface CreateOrEditListModal { name: 'create-or-edit-list' purpose?: string @@ -181,9 +171,6 @@ export type Modal = | ContentLanguagesSettingsModal | PostLanguagesSettingsModal - // Moderation - | ReportModal - // Lists | CreateOrEditListModal | UserAddRemoveListsModal diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index a9207607b1..1ef5a34e65 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -16,7 +16,6 @@ import * as UserAddRemoveListsModal from './UserAddRemoveLists' import * as ListAddUserModal from './ListAddRemoveUsers' import * as AltImageModal from './AltImage' import * as EditImageModal from './AltImage' -import * as ReportModal from './report/Modal' import * as DeleteAccountModal from './DeleteAccount' import * as ChangeHandleModal from './ChangeHandle' import * as InviteCodesModal from './InviteCodes' @@ -69,9 +68,6 @@ export function ModalsContainer() { } else if (activeModal?.name === 'edit-profile') { snapPoints = EditProfileModal.snapPoints element = - } else if (activeModal?.name === 'report') { - snapPoints = ReportModal.snapPoints - element = } else if (activeModal?.name === 'create-or-edit-list') { snapPoints = CreateOrEditListModal.snapPoints element = diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index 347057062e..f852bb1f8e 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -9,7 +9,6 @@ import {useModals, useModalControls} from '#/state/modals' import type {Modal as ModalIface} from '#/state/modals' import * as ConfirmModal from './Confirm' import * as EditProfileModal from './EditProfile' -import * as ReportModal from './report/Modal' import * as CreateOrEditListModal from './CreateOrEditList' import * as UserAddRemoveLists from './UserAddRemoveLists' import * as ListAddUserModal from './ListAddRemoveUsers' @@ -79,8 +78,6 @@ function Modal({modal}: {modal: ModalIface}) { element = } else if (modal.name === 'edit-profile') { element = - } else if (modal.name === 'report') { - element = } else if (modal.name === 'create-or-edit-list') { element = } else if (modal.name === 'user-add-remove-lists') { diff --git a/src/view/com/modals/report/InputIssueDetails.tsx b/src/view/com/modals/report/InputIssueDetails.tsx deleted file mode 100644 index 2bc86f75e7..0000000000 --- a/src/view/com/modals/report/InputIssueDetails.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React from 'react' -import {View, TouchableOpacity, StyleSheet} from 'react-native' -import {TextInput} from '../util' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {CharProgress} from '../../composer/char-progress/CharProgress' -import {Text} from '../../util/text/Text' -import {usePalette} from 'lib/hooks/usePalette' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {s} from 'lib/styles' -import {SendReportButton} from './SendReportButton' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -export function InputIssueDetails({ - details, - setDetails, - goBack, - submitReport, - isProcessing, -}: { - details: string | undefined - setDetails: (v: string) => void - goBack: () => void - submitReport: () => void - isProcessing: boolean -}) { - const pal = usePalette('default') - const {_} = useLingui() - const {isMobile} = useWebMediaQueries() - - return ( - - - - - {' '} - Back - - - - - - - - - - - - - ) -} - -const styles = StyleSheet.create({ - backBtn: { - flexDirection: 'row', - alignItems: 'center', - }, - detailsInputContainer: { - borderRadius: 8, - }, - detailsInput: { - paddingHorizontal: 12, - paddingTop: 12, - paddingBottom: 12, - borderRadius: 8, - minHeight: 100, - fontSize: 16, - }, - detailsInputBottomBar: { - alignSelf: 'flex-end', - }, - charCounter: { - flexDirection: 'row', - alignItems: 'center', - paddingRight: 10, - paddingBottom: 8, - }, -}) diff --git a/src/view/com/modals/report/Modal.tsx b/src/view/com/modals/report/Modal.tsx deleted file mode 100644 index abbad9b402..0000000000 --- a/src/view/com/modals/report/Modal.tsx +++ /dev/null @@ -1,223 +0,0 @@ -import React, {useState, useMemo} from 'react' -import {Linking, StyleSheet, TouchableOpacity, View} from 'react-native' -import {ScrollView} from 'react-native-gesture-handler' -import {AtUri} from '@atproto/api' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {s} from 'lib/styles' -import {Text} from '../../util/text/Text' -import * as Toast from '../../util/Toast' -import {ErrorMessage} from '../../util/error/ErrorMessage' -import {cleanError} from 'lib/strings/errors' -import {usePalette} from 'lib/hooks/usePalette' -import {SendReportButton} from './SendReportButton' -import {InputIssueDetails} from './InputIssueDetails' -import {ReportReasonOptions} from './ReasonOptions' -import {CollectionId} from './types' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useModalControls} from '#/state/modals' -import {getAgent} from '#/state/session' - -const DMCA_LINK = 'https://bsky.social/about/support/copyright' - -export const snapPoints = [575] - -const CollectionNames = { - [CollectionId.FeedGenerator]: 'Feed', - [CollectionId.Profile]: 'Profile', - [CollectionId.List]: 'List', - [CollectionId.Post]: 'Post', -} - -type ReportComponentProps = - | { - uri: string - cid: string - } - | { - did: string - } - -export function Component(content: ReportComponentProps) { - const {closeModal} = useModalControls() - const pal = usePalette('default') - const {isMobile} = useWebMediaQueries() - const [isProcessing, setIsProcessing] = useState(false) - const [showDetailsInput, setShowDetailsInput] = useState(false) - const [error, setError] = useState('') - const [issue, setIssue] = useState('') - const [details, setDetails] = useState('') - const isAccountReport = 'did' in content - const subjectKey = isAccountReport ? content.did : content.uri - const atUri = useMemo( - () => (!isAccountReport ? new AtUri(subjectKey) : null), - [isAccountReport, subjectKey], - ) - - const submitReport = async () => { - setError('') - if (!issue) { - return - } - setIsProcessing(true) - try { - if (issue === '__copyright__') { - Linking.openURL(DMCA_LINK) - closeModal() - return - } - const $type = !isAccountReport - ? 'com.atproto.repo.strongRef' - : 'com.atproto.admin.defs#repoRef' - await getAgent().createModerationReport({ - reasonType: issue, - subject: { - $type, - ...content, - }, - reason: details, - }) - Toast.show("Thank you for your report! We'll look into it promptly.") - - closeModal() - return - } catch (e: any) { - setError(cleanError(e)) - setIsProcessing(false) - } - } - - const goBack = () => { - setShowDetailsInput(false) - } - - return ( - - - {showDetailsInput ? ( - - ) : ( - - )} - - - ) -} - -// If no atUri is passed, that means the reporting collection is account -const getCollectionNameForReport = (atUri: AtUri | null) => { - if (!atUri) return 'Account' - // Generic fallback for any collection being reported - return CollectionNames[atUri.collection as CollectionId] || 'Content' -} - -const SelectIssue = ({ - error, - setShowDetailsInput, - issue, - setIssue, - submitReport, - isProcessing, - atUri, -}: { - error: string | undefined - setShowDetailsInput: (v: boolean) => void - issue: string | undefined - setIssue: (v: string) => void - submitReport: () => void - isProcessing: boolean - atUri: AtUri | null -}) => { - const pal = usePalette('default') - const {_} = useLingui() - const collectionName = getCollectionNameForReport(atUri) - const onSelectIssue = (v: string) => setIssue(v) - const goToDetails = () => { - if (issue === '__copyright__') { - Linking.openURL(DMCA_LINK) - return - } - setShowDetailsInput(true) - } - - return ( - <> - - Report {collectionName} - - - What is the issue with this {collectionName}? - - - - - {error ? : undefined} - {/* If no atUri is present, the report would be for account in which case, we allow sending without specifying a reason */} - {issue || !atUri ? ( - <> - - - - Add details to report - - - - ) : undefined} - - ) -} - -const styles = StyleSheet.create({ - container: { - paddingHorizontal: 10, - }, - title: { - textAlign: 'center', - fontWeight: 'bold', - fontSize: 24, - marginBottom: 12, - }, - description: { - textAlign: 'center', - fontSize: 17, - paddingHorizontal: 22, - marginBottom: 10, - }, - addDetailsBtn: { - padding: 14, - alignSelf: 'center', - }, -}) diff --git a/src/view/com/modals/report/ReasonOptions.tsx b/src/view/com/modals/report/ReasonOptions.tsx deleted file mode 100644 index 23b49b6648..0000000000 --- a/src/view/com/modals/report/ReasonOptions.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import {View} from 'react-native' -import React, {useMemo} from 'react' -import {AtUri, ComAtprotoModerationDefs} from '@atproto/api' - -import {Text} from '../../util/text/Text' -import {UsePaletteValue, usePalette} from 'lib/hooks/usePalette' -import {RadioGroup, RadioGroupItem} from 'view/com/util/forms/RadioGroup' -import {CollectionId} from './types' - -type ReasonMap = Record -const CommonReasons = { - [ComAtprotoModerationDefs.REASONRUDE]: { - title: 'Anti-Social Behavior', - description: 'Harassment, trolling, or intolerance', - }, - [ComAtprotoModerationDefs.REASONVIOLATION]: { - title: 'Illegal and Urgent', - description: 'Glaring violations of law or terms of service', - }, - [ComAtprotoModerationDefs.REASONOTHER]: { - title: 'Other', - description: 'An issue not included in these options', - }, -} -const CollectionToReasonsMap: Record = { - [CollectionId.Post]: { - [ComAtprotoModerationDefs.REASONSPAM]: { - title: 'Spam', - description: 'Excessive mentions or replies', - }, - [ComAtprotoModerationDefs.REASONSEXUAL]: { - title: 'Unwanted Sexual Content', - description: 'Nudity or pornography not labeled as such', - }, - __copyright__: { - title: 'Copyright Violation', - description: 'Contains copyrighted material', - }, - ...CommonReasons, - }, - [CollectionId.List]: { - ...CommonReasons, - [ComAtprotoModerationDefs.REASONVIOLATION]: { - title: 'Name or Description Violates Community Standards', - description: 'Terms used violate community standards', - }, - }, -} -const AccountReportReasons = { - [ComAtprotoModerationDefs.REASONMISLEADING]: { - title: 'Misleading Account', - description: 'Impersonation or false claims about identity or affiliation', - }, - [ComAtprotoModerationDefs.REASONSPAM]: { - title: 'Frequently Posts Unwanted Content', - description: 'Spam; excessive mentions or replies', - }, - [ComAtprotoModerationDefs.REASONVIOLATION]: { - title: 'Name or Description Violates Community Standards', - description: 'Terms used violate community standards', - }, -} - -const Option = ({ - pal, - title, - description, -}: { - pal: UsePaletteValue - description: string - title: string -}) => { - return ( - - - {title} - - {description} - - ) -} - -// This is mostly just content copy without almost any logic -// so this may grow over time and it makes sense to split it up into its own file -// to keep it separate from the actual reporting modal logic -const useReportRadioOptions = (pal: UsePaletteValue, atUri: AtUri | null) => - useMemo(() => { - let items: ReasonMap = {...CommonReasons} - // If no atUri is passed, that means the reporting collection is account - if (!atUri) { - items = {...AccountReportReasons} - } - - if (atUri?.collection && CollectionToReasonsMap[atUri.collection]) { - items = {...CollectionToReasonsMap[atUri.collection]} - } - - return Object.entries(items).map(([key, {title, description}]) => ({ - key, - label: