diff options
Diffstat (limited to 'src/view')
-rw-r--r-- | src/view/com/lists/ListActions.tsx | 13 | ||||
-rw-r--r-- | src/view/com/lists/ListItems.tsx | 7 | ||||
-rw-r--r-- | src/view/com/modals/Modal.tsx | 12 | ||||
-rw-r--r-- | src/view/com/modals/Modal.web.tsx | 9 | ||||
-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.tsx | 123 | ||||
-rw-r--r-- | src/view/com/modals/report/ReportAccount.tsx | 197 | ||||
-rw-r--r-- | src/view/com/modals/report/types.ts | 8 | ||||
-rw-r--r-- | src/view/com/profile/ProfileHeader.tsx | 2 | ||||
-rw-r--r-- | src/view/com/util/forms/PostDropdownBtn.tsx | 6 | ||||
-rw-r--r-- | src/view/index.ts | 2 | ||||
-rw-r--r-- | src/view/screens/CustomFeed.tsx | 78 | ||||
-rw-r--r-- | src/view/screens/ProfileList.tsx | 12 |
13 files changed, 299 insertions, 335 deletions
diff --git a/src/view/com/lists/ListActions.tsx b/src/view/com/lists/ListActions.tsx index ee5a2afcb..353338198 100644 --- a/src/view/com/lists/ListActions.tsx +++ b/src/view/com/lists/ListActions.tsx @@ -11,6 +11,7 @@ export const ListActions = ({ isOwner, onPressDeleteList, onPressShareList, + onPressReportList, reversed = false, // Default value of reversed is false }: { isOwner: boolean @@ -19,6 +20,7 @@ export const ListActions = ({ onPressEditList?: () => void onPressDeleteList?: () => void onPressShareList?: () => void + onPressReportList?: () => void reversed?: boolean // New optional prop }) => { const pal = usePalette('default') @@ -64,6 +66,17 @@ export const ListActions = ({ onPress={onPressShareList}> <FontAwesomeIcon icon={'share'} style={[pal.text]} /> </Button>, + !isOwner && ( + <Button + key="reportListBtn" + testID="reportListBtn" + type="default" + accessibilityLabel="Report list" + accessibilityHint="" + onPress={onPressReportList}> + <FontAwesomeIcon icon={'circle-exclamation'} style={[pal.text]} /> + </Button> + ), ] // If reversed is true, reverse the array to reverse the order of the buttons diff --git a/src/view/com/lists/ListItems.tsx b/src/view/com/lists/ListItems.tsx index 188518ea5..94e22f35e 100644 --- a/src/view/com/lists/ListItems.tsx +++ b/src/view/com/lists/ListItems.tsx @@ -45,6 +45,7 @@ export const ListItems = observer( onPressEditList, onPressDeleteList, onPressShareList, + onPressReportList, renderEmptyState, testID, headerOffset = 0, @@ -57,6 +58,7 @@ export const ListItems = observer( onPressEditList: () => void onPressDeleteList: () => void onPressShareList: () => void + onPressReportList: () => void renderEmptyState?: () => JSX.Element testID?: string headerOffset?: number @@ -169,6 +171,7 @@ export const ListItems = observer( onPressEditList={onPressEditList} onPressDeleteList={onPressDeleteList} onPressShareList={onPressShareList} + onPressReportList={onPressReportList} /> ) : null } else if (item === ERROR_ITEM) { @@ -208,6 +211,7 @@ export const ListItems = observer( onPressEditList, onPressDeleteList, onPressShareList, + onPressReportList, onPressTryAgain, onPressRetryLoadMore, ], @@ -267,6 +271,7 @@ const ListHeader = observer( onPressEditList, onPressDeleteList, onPressShareList, + onPressReportList, }: { list: AppBskyGraphDefs.ListView isOwner: boolean @@ -274,6 +279,7 @@ const ListHeader = observer( onPressEditList: () => void onPressDeleteList: () => void onPressShareList: () => void + onPressReportList: () => void }) => { const pal = usePalette('default') const store = useStores() @@ -319,6 +325,7 @@ const ListHeader = observer( onPressEditList={onPressEditList} onToggleSubscribed={onToggleSubscribed} onPressShareList={onPressShareList} + onPressReportList={onPressReportList} /> )} </View> diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index ce5dc40e0..efd06412d 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -13,14 +13,13 @@ import * as ConfirmModal from './Confirm' import * as EditProfileModal from './EditProfile' import * as ProfilePreviewModal from './ProfilePreview' import * as ServerInputModal from './ServerInput' -import * as ReportPostModal from './report/ReportPost' import * as RepostModal from './Repost' import * as SelfLabelModal from './SelfLabel' import * as CreateOrEditMuteListModal from './CreateOrEditMuteList' import * as ListAddRemoveUserModal from './ListAddRemoveUser' import * as AltImageModal from './AltImage' import * as EditImageModal from './AltImage' -import * as ReportAccountModal from './report/ReportAccount' +import * as ReportModal from './report/Modal' import * as DeleteAccountModal from './DeleteAccount' import * as ChangeHandleModal from './ChangeHandle' import * as WaitlistModal from './Waitlist' @@ -87,12 +86,9 @@ export const ModalsContainer = observer(function ModalsContainer() { } else if (activeModal?.name === 'server-input') { snapPoints = ServerInputModal.snapPoints element = <ServerInputModal.Component {...activeModal} /> - } else if (activeModal?.name === 'report-post') { - snapPoints = ReportPostModal.snapPoints - element = <ReportPostModal.Component {...activeModal} /> - } else if (activeModal?.name === 'report-account') { - snapPoints = ReportAccountModal.snapPoints - element = <ReportAccountModal.Component {...activeModal} /> + } else if (activeModal?.name === 'report') { + snapPoints = ReportModal.snapPoints + element = <ReportModal.Component {...activeModal} /> } else if (activeModal?.name === 'create-or-edit-mute-list') { snapPoints = CreateOrEditMuteListModal.snapPoints element = <CreateOrEditMuteListModal.Component {...activeModal} /> diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index 0f6247822..0e28b1618 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -10,8 +10,7 @@ import * as ConfirmModal from './Confirm' import * as EditProfileModal from './EditProfile' import * as ProfilePreviewModal from './ProfilePreview' import * as ServerInputModal from './ServerInput' -import * as ReportPostModal from './report/ReportPost' -import * as ReportAccountModal from './report/ReportAccount' +import * as ReportModal from './report/Modal' import * as CreateOrEditMuteListModal from './CreateOrEditMuteList' import * as ListAddRemoveUserModal from './ListAddRemoveUser' import * as DeleteAccountModal from './DeleteAccount' @@ -76,10 +75,8 @@ function Modal({modal}: {modal: ModalIface}) { element = <ProfilePreviewModal.Component {...modal} /> } else if (modal.name === 'server-input') { element = <ServerInputModal.Component {...modal} /> - } else if (modal.name === 'report-post') { - element = <ReportPostModal.Component {...modal} /> - } else if (modal.name === 'report-account') { - element = <ReportAccountModal.Component {...modal} /> + } else if (modal.name === 'report') { + element = <ReportModal.Component {...modal} /> } else if (modal.name === 'create-or-edit-mute-list') { element = <CreateOrEditMuteListModal.Component {...modal} /> } else if (modal.name === 'list-add-remove-user') { 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', +} diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx index f8531d76c..dd3fb530e 100644 --- a/src/view/com/profile/ProfileHeader.tsx +++ b/src/view/com/profile/ProfileHeader.tsx @@ -245,7 +245,7 @@ const ProfileHeaderLoaded = observer( const onPressReportAccount = React.useCallback(() => { track('ProfileHeader:ReportAccountButtonClicked') store.shell.openModal({ - name: 'report-account', + name: 'report', did: view.did, }) }, [track, store, view]) diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index 27a1f20d0..65050d8c0 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -102,9 +102,9 @@ export function PostDropdownBtn({ label: 'Report post', onPress() { store.shell.openModal({ - name: 'report-post', - postUri: itemUri, - postCid: itemCid, + name: 'report', + uri: itemUri, + cid: itemCid, }) }, testID: 'postDropdownReportBtn', diff --git a/src/view/index.ts b/src/view/index.ts index 22cc6e837..4294508de 100644 --- a/src/view/index.ts +++ b/src/view/index.ts @@ -89,6 +89,7 @@ import {faXmark} from '@fortawesome/free-solid-svg-icons/faXmark' import {faPlay} from '@fortawesome/free-solid-svg-icons/faPlay' import {faPause} from '@fortawesome/free-solid-svg-icons/faPause' import {faThumbtack} from '@fortawesome/free-solid-svg-icons/faThumbtack' +import {faList} from '@fortawesome/free-solid-svg-icons/faList' export function setup() { library.add( @@ -181,5 +182,6 @@ export function setup() { faXmark, faPlay, faPause, + faList, ) } diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index 265f8a94c..2da2e2159 100644 --- a/src/view/screens/CustomFeed.tsx +++ b/src/view/screens/CustomFeed.tsx @@ -188,6 +188,15 @@ export const CustomFeedScreenInner = observer( track('CustomFeed:Share') }, [handleOrDid, rkey, track]) + const onPressReport = React.useCallback(() => { + if (!currentFeed) return + store.shell.openModal({ + name: 'report', + uri: currentFeed.uri, + cid: currentFeed.data.cid, + }) + }, [store, currentFeed]) + const onScrollToTop = React.useCallback(() => { scrollElRef.current?.scrollToOffset({offset: 0, animated: true}) resetMainScroll() @@ -200,15 +209,37 @@ export const CustomFeedScreenInner = observer( const dropdownItems: DropdownItem[] = React.useMemo(() => { let items: DropdownItem[] = [ { - testID: 'feedHeaderDropdownRemoveBtn', - label: 'Remove from my feeds', + testID: 'feedHeaderDropdownToggleSavedBtn', + label: currentFeed?.isSaved + ? 'Remove from my feeds' + : 'Add to my feeds', onPress: onToggleSaved, + icon: currentFeed?.isSaved + ? { + ios: { + name: 'trash', + }, + android: 'ic_delete', + web: 'trash', + } + : { + ios: { + name: 'plus', + }, + android: '', + web: 'plus', + }, + }, + { + testID: 'feedHeaderDropdownReportBtn', + label: 'Report feed', + onPress: onPressReport, icon: { ios: { - name: 'trash', + name: 'exclamationmark.triangle', }, - android: 'ic_delete', - web: 'trash', + android: 'ic_menu_report_image', + web: 'circle-exclamation', }, }, { @@ -225,7 +256,7 @@ export const CustomFeedScreenInner = observer( }, ] return items - }, [onToggleSaved, onPressShare]) + }, [currentFeed?.isSaved, onToggleSaved, onPressReport, onPressShare]) const renderHeaderBtns = React.useCallback(() => { return ( @@ -258,12 +289,7 @@ export const CustomFeedScreenInner = observer( /> </Button> ) : undefined} - {currentFeed?.isSaved ? ( - <NativeDropdown - testID="feedHeaderDropdownBtn" - items={dropdownItems} - /> - ) : ( + {!currentFeed?.isSaved ? ( <Button type="default-light" onPress={onToggleSaved} @@ -275,7 +301,21 @@ export const CustomFeedScreenInner = observer( Add to My Feeds </Text> </Button> - )} + ) : null} + <NativeDropdown testID="feedHeaderDropdownBtn" items={dropdownItems}> + <View + style={{ + paddingLeft: currentFeed?.isSaved ? 12 : 6, + paddingRight: 12, + paddingVertical: 8, + }}> + <FontAwesomeIcon + icon="ellipsis" + size={20} + color={pal.colors.textLight} + /> + </View> + </NativeDropdown> </View> ) }, [ @@ -370,6 +410,17 @@ export const CustomFeedScreenInner = observer( color={pal.colors.icon} /> </Button> + <Button + type="default" + accessibilityLabel="Report this feed" + accessibilityHint="" + onPress={onPressReport}> + <FontAwesomeIcon + icon="circle-exclamation" + size={18} + color={pal.colors.icon} + /> + </Button> </View> )} </View> @@ -419,6 +470,7 @@ export const CustomFeedScreenInner = observer( onToggleLiked, onPressShare, handleOrDid, + onPressReport, rkey, isPinned, onTogglePinned, diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx index 0502e8dc8..651fac21f 100644 --- a/src/view/screens/ProfileList.tsx +++ b/src/view/screens/ProfileList.tsx @@ -86,6 +86,15 @@ export const ProfileListScreen = withAuthRequired( }) }, [store, list, navigation]) + const onPressReportList = React.useCallback(() => { + if (!list.list) return + store.shell.openModal({ + name: 'report', + uri: list.uri, + cid: list.list.cid, + }) + }, [store, list]) + const onPressShareList = React.useCallback(() => { const url = toShareUrl(`/profile/${name}/lists/${rkey}`) shareUrl(url) @@ -104,6 +113,7 @@ export const ProfileListScreen = withAuthRequired( onPressEditList={onPressEditList} onToggleSubscribed={onToggleSubscribed} onPressShareList={onPressShareList} + onPressReportList={onPressReportList} reversed={true} /> ) @@ -114,6 +124,7 @@ export const ProfileListScreen = withAuthRequired( onPressEditList, onPressShareList, onToggleSubscribed, + onPressReportList, ]) return ( @@ -132,6 +143,7 @@ export const ProfileListScreen = withAuthRequired( onToggleSubscribed={onToggleSubscribed} onPressEditList={onPressEditList} onPressDeleteList={onPressDeleteList} + onPressReportList={onPressReportList} onPressShareList={onPressShareList} style={[s.flex1]} /> |