about summary refs log tree commit diff
path: root/src/view/com/modals/report
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/modals/report')
-rw-r--r--src/view/com/modals/report/Modal.tsx (renamed from src/view/com/modals/report/ReportPost.tsx)165
-rw-r--r--src/view/com/modals/report/ReasonOptions.tsx123
-rw-r--r--src/view/com/modals/report/ReportAccount.tsx197
-rw-r--r--src/view/com/modals/report/types.ts8
4 files changed, 189 insertions, 304 deletions
diff --git a/src/view/com/modals/report/ReportPost.tsx b/src/view/com/modals/report/Modal.tsx
index 34ec8c2f2..f386b110d 100644
--- a/src/view/com/modals/report/ReportPost.tsx
+++ b/src/view/com/modals/report/Modal.tsx
@@ -1,10 +1,9 @@
 import React, {useState, useMemo} from 'react'
 import {Linking, StyleSheet, TouchableOpacity, View} from 'react-native'
 import {ScrollView} from 'react-native-gesture-handler'
-import {ComAtprotoModerationDefs} from '@atproto/api'
+import {AtUri} from '@atproto/api'
 import {useStores} from 'state/index'
 import {s} from 'lib/styles'
-import {RadioGroup, RadioGroupItem} from '../../util/forms/RadioGroup'
 import {Text} from '../../util/text/Text'
 import * as Toast from '../../util/Toast'
 import {ErrorMessage} from '../../util/error/ErrorMessage'
@@ -12,25 +11,43 @@ 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'
 
 const DMCA_LINK = 'https://bsky.app/support/copyright'
 
 export const snapPoints = [575]
 
-export function Component({
-  postUri,
-  postCid,
-}: {
-  postUri: string
-  postCid: string
-}) {
+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 store = useStores()
   const pal = usePalette('default')
   const [isProcessing, setIsProcessing] = useState(false)
-  const [showTextInput, setShowTextInput] = useState(false)
+  const [showDetailsInput, setShowDetailsInput] = useState(false)
   const [error, setError] = useState<string>()
   const [issue, setIssue] = useState<string>()
   const [details, setDetails] = useState<string>()
+  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('')
@@ -43,12 +60,14 @@ export function Component({
         Linking.openURL(DMCA_LINK)
         return
       }
+      const $type = !isAccountReport
+        ? 'com.atproto.repo.strongRef'
+        : 'com.atproto.admin.defs#repoRef'
       await store.agent.createModerationReport({
         reasonType: issue,
         subject: {
-          $type: 'com.atproto.repo.strongRef',
-          uri: postUri,
-          cid: postCid,
+          $type,
+          ...content,
         },
         reason: details,
       })
@@ -63,13 +82,13 @@ export function Component({
   }
 
   const goBack = () => {
-    setShowTextInput(false)
+    setShowDetailsInput(false)
   }
 
   return (
-    <ScrollView testID="reportPostModal" style={[s.flex1, pal.view]}>
+    <ScrollView testID="reportModal" style={[s.flex1, pal.view]}>
       <View style={styles.container}>
-        {showTextInput ? (
+        {showDetailsInput ? (
           <InputIssueDetails
             details={details}
             setDetails={setDetails}
@@ -79,12 +98,13 @@ export function Component({
           />
         ) : (
           <SelectIssue
-            setShowTextInput={setShowTextInput}
+            setShowDetailsInput={setShowDetailsInput}
             error={error}
             issue={issue}
             setIssue={setIssue}
             submitReport={submitReport}
             isProcessing={isProcessing}
+            atUri={atUri}
           />
         )}
       </View>
@@ -92,128 +112,59 @@ export function Component({
   )
 }
 
+// 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,
-  setShowTextInput,
+  setShowDetailsInput,
   issue,
   setIssue,
   submitReport,
   isProcessing,
+  atUri,
 }: {
   error: string | undefined
-  setShowTextInput: (v: boolean) => void
+  setShowDetailsInput: (v: boolean) => void
   issue: string | undefined
   setIssue: (v: string) => void
   submitReport: () => void
   isProcessing: boolean
+  atUri: AtUri | null
 }) => {
   const pal = usePalette('default')
-  const ITEMS: RadioGroupItem[] = useMemo(
-    () => [
-      {
-        key: ComAtprotoModerationDefs.REASONSPAM,
-        label: (
-          <View>
-            <Text style={pal.text} type="md-bold">
-              Spam
-            </Text>
-            <Text style={pal.textLight}>Excessive mentions or replies</Text>
-          </View>
-        ),
-      },
-      {
-        key: ComAtprotoModerationDefs.REASONSEXUAL,
-        label: (
-          <View>
-            <Text style={pal.text} type="md-bold">
-              Unwanted Sexual Content
-            </Text>
-            <Text style={pal.textLight}>
-              Nudity or pornography not labeled as such
-            </Text>
-          </View>
-        ),
-      },
-      {
-        key: '__copyright__',
-        label: (
-          <View>
-            <Text style={pal.text} type="md-bold">
-              Copyright Violation
-            </Text>
-            <Text style={pal.textLight}>Contains copyrighted material</Text>
-          </View>
-        ),
-      },
-      {
-        key: ComAtprotoModerationDefs.REASONRUDE,
-        label: (
-          <View>
-            <Text style={pal.text} type="md-bold">
-              Anti-Social Behavior
-            </Text>
-            <Text style={pal.textLight}>
-              Harassment, trolling, or intolerance
-            </Text>
-          </View>
-        ),
-      },
-      {
-        key: ComAtprotoModerationDefs.REASONVIOLATION,
-        label: (
-          <View>
-            <Text style={pal.text} type="md-bold">
-              Illegal and Urgent
-            </Text>
-            <Text style={pal.textLight}>
-              Glaring violations of law or terms of service
-            </Text>
-          </View>
-        ),
-      },
-      {
-        key: ComAtprotoModerationDefs.REASONOTHER,
-        label: (
-          <View>
-            <Text style={pal.text} type="md-bold">
-              Other
-            </Text>
-            <Text style={pal.textLight}>
-              An issue not included in these options
-            </Text>
-          </View>
-        ),
-      },
-    ],
-    [pal],
-  )
-
+  const collectionName = getCollectionNameForReport(atUri)
   const onSelectIssue = (v: string) => setIssue(v)
   const goToDetails = () => {
     if (issue === '__copyright__') {
       Linking.openURL(DMCA_LINK)
       return
     }
-    setShowTextInput(true)
+    setShowDetailsInput(true)
   }
 
   return (
     <>
-      <Text style={[pal.text, styles.title]}>Report post</Text>
+      <Text style={[pal.text, styles.title]}>Report {collectionName}</Text>
       <Text style={[pal.textLight, styles.description]}>
-        What is the issue with this post?
+        What is the issue with this {collectionName}?
       </Text>
-      <RadioGroup
-        testID="reportPostRadios"
-        items={ITEMS}
-        onSelect={onSelectIssue}
+      <ReportReasonOptions
+        atUri={atUri}
+        selectedIssue={issue}
+        onSelectIssue={onSelectIssue}
       />
       {error ? (
         <View style={s.mt10}>
           <ErrorMessage message={error} />
         </View>
       ) : undefined}
-      {issue ? (
+      {/* If no atUri is present, the report would be for account in which case, we allow sending without specifying a reason */}
+      {issue || !atUri ? (
         <>
           <SendReportButton
             onPress={submitReport}
diff --git a/src/view/com/modals/report/ReasonOptions.tsx b/src/view/com/modals/report/ReasonOptions.tsx
new file mode 100644
index 000000000..23b49b664
--- /dev/null
+++ b/src/view/com/modals/report/ReasonOptions.tsx
@@ -0,0 +1,123 @@
+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<string, {title: string; description: string}>
+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<string, ReasonMap> = {
+  [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 (
+    <View>
+      <Text style={pal.text} type="md-bold">
+        {title}
+      </Text>
+      <Text style={pal.textLight}>{description}</Text>
+    </View>
+  )
+}
+
+// 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: <Option pal={pal} title={title} description={description} />,
+    }))
+  }, [pal, atUri])
+
+export const ReportReasonOptions = ({
+  atUri,
+  selectedIssue,
+  onSelectIssue,
+}: {
+  atUri: AtUri | null
+  selectedIssue?: string
+  onSelectIssue: (key: string) => void
+}) => {
+  const pal = usePalette('default')
+  const ITEMS: RadioGroupItem[] = useReportRadioOptions(pal, atUri)
+  return (
+    <RadioGroup
+      items={ITEMS}
+      onSelect={onSelectIssue}
+      testID="reportReasonRadios"
+      initialSelection={selectedIssue}
+    />
+  )
+}
diff --git a/src/view/com/modals/report/ReportAccount.tsx b/src/view/com/modals/report/ReportAccount.tsx
deleted file mode 100644
index b53c54caa..000000000
--- a/src/view/com/modals/report/ReportAccount.tsx
+++ /dev/null
@@ -1,197 +0,0 @@
-import React, {useState, useMemo} from 'react'
-import {TouchableOpacity, StyleSheet, View} from 'react-native'
-import {ScrollView} from 'react-native-gesture-handler'
-import {ComAtprotoModerationDefs} from '@atproto/api'
-import {useStores} from 'state/index'
-import {s} from 'lib/styles'
-import {RadioGroup, RadioGroupItem} from '../../util/forms/RadioGroup'
-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 {isDesktopWeb} from 'platform/detection'
-import {SendReportButton} from './SendReportButton'
-import {InputIssueDetails} from './InputIssueDetails'
-
-export const snapPoints = [500]
-
-export function Component({did}: {did: string}) {
-  const store = useStores()
-  const pal = usePalette('default')
-  const [isProcessing, setIsProcessing] = useState(false)
-  const [error, setError] = useState<string>()
-  const [issue, setIssue] = useState<string>()
-  const onSelectIssue = (v: string) => setIssue(v)
-  const [details, setDetails] = useState<string>()
-  const [showDetailsInput, setShowDetailsInput] = useState(false)
-
-  const onPress = async () => {
-    setError('')
-    if (!issue) {
-      return
-    }
-    setIsProcessing(true)
-    try {
-      await store.agent.com.atproto.moderation.createReport({
-        reasonType: issue,
-        subject: {
-          $type: 'com.atproto.admin.defs#repoRef',
-          did,
-        },
-        reason: details,
-      })
-      Toast.show("Thank you for your report! We'll look into it promptly.")
-      store.shell.closeModal()
-      return
-    } catch (e: any) {
-      setError(cleanError(e))
-      setIsProcessing(false)
-    }
-  }
-  const goBack = () => {
-    setShowDetailsInput(false)
-  }
-  const goToDetails = () => {
-    setShowDetailsInput(true)
-  }
-
-  return (
-    <ScrollView
-      testID="reportAccountModal"
-      style={[styles.container, pal.view]}>
-      {showDetailsInput ? (
-        <InputIssueDetails
-          submitReport={onPress}
-          setDetails={setDetails}
-          details={details}
-          isProcessing={isProcessing}
-          goBack={goBack}
-        />
-      ) : (
-        <SelectIssue
-          onPress={onPress}
-          onSelectIssue={onSelectIssue}
-          error={error}
-          isProcessing={isProcessing}
-          goToDetails={goToDetails}
-        />
-      )}
-    </ScrollView>
-  )
-}
-
-const SelectIssue = ({
-  onPress,
-  onSelectIssue,
-  error,
-  isProcessing,
-  goToDetails,
-}: {
-  onPress: () => void
-  onSelectIssue: (v: string) => void
-  error: string | undefined
-  isProcessing: boolean
-  goToDetails: () => void
-}) => {
-  const pal = usePalette('default')
-  const ITEMS: RadioGroupItem[] = useMemo(
-    () => [
-      {
-        key: ComAtprotoModerationDefs.REASONMISLEADING,
-        label: (
-          <View>
-            <Text style={pal.text} type="md-bold">
-              Misleading Account
-            </Text>
-            <Text style={pal.textLight}>
-              Impersonation or false claims about identity or affiliation
-            </Text>
-          </View>
-        ),
-      },
-      {
-        key: ComAtprotoModerationDefs.REASONSPAM,
-        label: (
-          <View>
-            <Text style={pal.text} type="md-bold">
-              Frequently Posts Unwanted Content
-            </Text>
-            <Text style={pal.textLight}>
-              Spam; excessive mentions or replies
-            </Text>
-          </View>
-        ),
-      },
-      {
-        key: ComAtprotoModerationDefs.REASONVIOLATION,
-        label: (
-          <View>
-            <Text style={pal.text} type="md-bold">
-              Name or Description Violates Community Standards
-            </Text>
-            <Text style={pal.textLight}>
-              Terms used violate community standards
-            </Text>
-          </View>
-        ),
-      },
-    ],
-    [pal],
-  )
-  return (
-    <>
-      <Text type="title-xl" style={[pal.text, styles.title]}>
-        Report Account
-      </Text>
-      <Text type="xl" style={[pal.text, styles.description]}>
-        What is the issue with this account?
-      </Text>
-      <RadioGroup
-        testID="reportAccountRadios"
-        items={ITEMS}
-        onSelect={onSelectIssue}
-      />
-      <Text type="sm" style={[pal.text, styles.description, s.pt10]}>
-        For other issues, please report specific posts.
-      </Text>
-      {error ? (
-        <View style={s.mt10}>
-          <ErrorMessage message={error} />
-        </View>
-      ) : undefined}
-      <SendReportButton onPress={onPress} isProcessing={isProcessing} />
-      <TouchableOpacity
-        testID="addDetailsBtn"
-        style={styles.addDetailsBtn}
-        onPress={goToDetails}
-        accessibilityRole="button"
-        accessibilityLabel="Add details"
-        accessibilityHint="Add more details to your report">
-        <Text style={[s.f18, pal.link]}>Add details to report</Text>
-      </TouchableOpacity>
-    </>
-  )
-}
-
-const styles = StyleSheet.create({
-  container: {
-    flex: 1,
-    paddingHorizontal: isDesktopWeb ? 0 : 10,
-  },
-  title: {
-    textAlign: 'center',
-    fontWeight: 'bold',
-    marginBottom: 12,
-  },
-  description: {
-    textAlign: 'center',
-    paddingHorizontal: 22,
-    marginBottom: 10,
-  },
-  addDetailsBtn: {
-    padding: 14,
-    alignSelf: 'center',
-    marginBottom: 40,
-  },
-})
diff --git a/src/view/com/modals/report/types.ts b/src/view/com/modals/report/types.ts
new file mode 100644
index 000000000..ca947ecbd
--- /dev/null
+++ b/src/view/com/modals/report/types.ts
@@ -0,0 +1,8 @@
+// TODO: ATM, @atproto/api does not export ids but it does have these listed at @atproto/api/client/lexicons
+// once we start exporting the ids from the @atproto/ap package, replace these hardcoded ones
+export enum CollectionId {
+  FeedGenerator = 'app.bsky.feed.generator',
+  Profile = 'app.bsky.actor.profile',
+  List = 'app.bsky.graph.list',
+  Post = 'app.bsky.feed.post',
+}