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/InputIssueDetails.tsx93
-rw-r--r--src/view/com/modals/report/ReportAccount.tsx180
-rw-r--r--src/view/com/modals/report/ReportPost.tsx251
-rw-r--r--src/view/com/modals/report/SendReportButton.tsx57
4 files changed, 581 insertions, 0 deletions
diff --git a/src/view/com/modals/report/InputIssueDetails.tsx b/src/view/com/modals/report/InputIssueDetails.tsx
new file mode 100644
index 000000000..a2e5069a8
--- /dev/null
+++ b/src/view/com/modals/report/InputIssueDetails.tsx
@@ -0,0 +1,93 @@
+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 {s} from 'lib/styles'
+import {SendReportButton} from './SendReportButton'
+import {isDesktopWeb} from 'platform/detection'
+
+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')
+
+  return (
+    <View style={[styles.detailsContainer]}>
+      <TouchableOpacity
+        testID="addDetailsBtn"
+        style={[s.mb10, styles.backBtn]}
+        onPress={goBack}
+        accessibilityRole="button"
+        accessibilityLabel="Add details"
+        accessibilityHint="Add more details to your report">
+        <FontAwesomeIcon size={18} icon="angle-left" style={[pal.link]} />
+        <Text style={[pal.text, s.f18, pal.link]}> Back</Text>
+      </TouchableOpacity>
+      <View style={[pal.btn, styles.detailsInputContainer]}>
+        <TextInput
+          accessibilityLabel="Text input field"
+          accessibilityHint="Enter a reason for reporting this post."
+          placeholder="Enter a reason or any other details here."
+          placeholderTextColor={pal.textLight.color}
+          value={details}
+          onChangeText={setDetails}
+          autoFocus={true}
+          numberOfLines={3}
+          multiline={true}
+          textAlignVertical="top"
+          maxLength={300}
+          style={[styles.detailsInput, pal.text]}
+        />
+        <View style={styles.detailsInputBottomBar}>
+          <View style={styles.charCounter}>
+            <CharProgress count={details?.length || 0} />
+          </View>
+        </View>
+      </View>
+      <SendReportButton onPress={submitReport} isProcessing={isProcessing} />
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  detailsContainer: {
+    marginTop: isDesktopWeb ? 0 : 12,
+  },
+  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/ReportAccount.tsx b/src/view/com/modals/report/ReportAccount.tsx
new file mode 100644
index 000000000..3ea221a8b
--- /dev/null
+++ b/src/view/com/modals/report/ReportAccount.tsx
@@ -0,0 +1,180 @@
+import React, {useState, useMemo} from 'react'
+import {TouchableOpacity, StyleSheet, View} from 'react-native'
+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 = [400]
+
+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 (
+    <View 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}
+        />
+      )}
+    </View>
+  )
+}
+
+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>
+        ),
+      },
+    ],
+    [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',
+  },
+})
diff --git a/src/view/com/modals/report/ReportPost.tsx b/src/view/com/modals/report/ReportPost.tsx
new file mode 100644
index 000000000..fe2a5bca4
--- /dev/null
+++ b/src/view/com/modals/report/ReportPost.tsx
@@ -0,0 +1,251 @@
+import React, {useState, useMemo} from 'react'
+import {Linking, StyleSheet, TouchableOpacity, View} from 'react-native'
+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 {SendReportButton} from './SendReportButton'
+import {InputIssueDetails} from './InputIssueDetails'
+
+const DMCA_LINK = 'https://bsky.app/support/copyright'
+
+export const snapPoints = [575]
+
+export function Component({
+  postUri,
+  postCid,
+}: {
+  postUri: string
+  postCid: string
+}) {
+  const store = useStores()
+  const pal = usePalette('default')
+  const [isProcessing, setIsProcessing] = useState(false)
+  const [showTextInput, setShowTextInput] = useState(false)
+  const [error, setError] = useState<string>()
+  const [issue, setIssue] = useState<string>()
+  const [details, setDetails] = useState<string>()
+
+  const submitReport = async () => {
+    setError('')
+    if (!issue) {
+      return
+    }
+    setIsProcessing(true)
+    try {
+      if (issue === '__copyright__') {
+        Linking.openURL(DMCA_LINK)
+        return
+      }
+      await store.agent.createModerationReport({
+        reasonType: issue,
+        subject: {
+          $type: 'com.atproto.repo.strongRef',
+          uri: postUri,
+          cid: postCid,
+        },
+        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 = () => {
+    setShowTextInput(false)
+  }
+
+  return (
+    <View testID="reportPostModal" style={[s.flex1, s.pl10, s.pr10, pal.view]}>
+      {showTextInput ? (
+        <InputIssueDetails
+          details={details}
+          setDetails={setDetails}
+          goBack={goBack}
+          submitReport={submitReport}
+          isProcessing={isProcessing}
+        />
+      ) : (
+        <SelectIssue
+          setShowTextInput={setShowTextInput}
+          error={error}
+          issue={issue}
+          setIssue={setIssue}
+          submitReport={submitReport}
+          isProcessing={isProcessing}
+        />
+      )}
+    </View>
+  )
+}
+
+const SelectIssue = ({
+  error,
+  setShowTextInput,
+  issue,
+  setIssue,
+  submitReport,
+  isProcessing,
+}: {
+  error: string | undefined
+  setShowTextInput: (v: boolean) => void
+  issue: string | undefined
+  setIssue: (v: string) => void
+  submitReport: () => void
+  isProcessing: boolean
+}) => {
+  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 onSelectIssue = (v: string) => setIssue(v)
+  const goToDetails = () => {
+    if (issue === '__copyright__') {
+      Linking.openURL(DMCA_LINK)
+      return
+    }
+    setShowTextInput(true)
+  }
+
+  return (
+    <>
+      <Text style={[pal.text, styles.title]}>Report post</Text>
+      <Text style={[pal.textLight, styles.description]}>
+        What is the issue with this post?
+      </Text>
+      <RadioGroup
+        testID="reportPostRadios"
+        items={ITEMS}
+        onSelect={onSelectIssue}
+      />
+      {error ? (
+        <View style={s.mt10}>
+          <ErrorMessage message={error} />
+        </View>
+      ) : undefined}
+      {issue ? (
+        <>
+          <SendReportButton
+            onPress={submitReport}
+            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>
+        </>
+      ) : undefined}
+    </>
+  )
+}
+
+const styles = StyleSheet.create({
+  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/SendReportButton.tsx b/src/view/com/modals/report/SendReportButton.tsx
new file mode 100644
index 000000000..82fb65f20
--- /dev/null
+++ b/src/view/com/modals/report/SendReportButton.tsx
@@ -0,0 +1,57 @@
+import React from 'react'
+import LinearGradient from 'react-native-linear-gradient'
+import {
+  ActivityIndicator,
+  StyleSheet,
+  TouchableOpacity,
+  View,
+} from 'react-native'
+import {Text} from '../../util/text/Text'
+import {s, gradients, colors} from 'lib/styles'
+
+export function SendReportButton({
+  onPress,
+  isProcessing,
+}: {
+  onPress: () => void
+  isProcessing: boolean
+}) {
+  // loading state
+  // =
+  if (isProcessing) {
+    return (
+      <View style={[styles.btn, s.mt10]}>
+        <ActivityIndicator />
+      </View>
+    )
+  }
+  return (
+    <TouchableOpacity
+      testID="sendReportBtn"
+      style={s.mt10}
+      onPress={onPress}
+      accessibilityRole="button"
+      accessibilityLabel="Report post"
+      accessibilityHint={`Reports post with reason and details`}>
+      <LinearGradient
+        colors={[gradients.blueLight.start, gradients.blueLight.end]}
+        start={{x: 0, y: 0}}
+        end={{x: 1, y: 1}}
+        style={[styles.btn]}>
+        <Text style={[s.white, s.bold, s.f18]}>Send Report</Text>
+      </LinearGradient>
+    </TouchableOpacity>
+  )
+}
+
+const styles = StyleSheet.create({
+  btn: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'center',
+    width: '100%',
+    borderRadius: 32,
+    padding: 14,
+    backgroundColor: colors.gray1,
+  },
+})