diff options
Diffstat (limited to 'src/view/com/modals/report')
-rw-r--r-- | src/view/com/modals/report/InputIssueDetails.tsx | 93 | ||||
-rw-r--r-- | src/view/com/modals/report/ReportAccount.tsx | 180 | ||||
-rw-r--r-- | src/view/com/modals/report/ReportPost.tsx | 251 | ||||
-rw-r--r-- | src/view/com/modals/report/SendReportButton.tsx | 57 |
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, + }, +}) |