diff --git a/__e2e__/tests/home-screen.test.ts b/__e2e__/tests/home-screen.test.ts
index d0eeb670..7bad3c19 100644
--- a/__e2e__/tests/home-screen.test.ts
+++ b/__e2e__/tests/home-screen.test.ts
@@ -56,12 +56,12 @@ describe('Home screen', () => {
.atIndex(0)
.tap()
await element(by.text('Report post')).tap()
- await expect(element(by.id('reportPostModal'))).toBeVisible()
+ await expect(element(by.id('reportModal'))).toBeVisible()
await element(
- by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'),
+ by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
).tap()
await element(by.id('sendReportBtn')).tap()
- await expect(element(by.id('reportPostModal'))).not.toBeVisible()
+ await expect(element(by.id('reportModal'))).not.toBeVisible()
})
it('Can swipe between feeds', async () => {
diff --git a/__e2e__/tests/mute-lists.test.ts b/__e2e__/tests/mute-lists.test.ts
index 9c6980bb..870e7ced 100644
--- a/__e2e__/tests/mute-lists.test.ts
+++ b/__e2e__/tests/mute-lists.test.ts
@@ -138,4 +138,33 @@ describe('Mute lists', () => {
await element(by.id('saveBtn')).tap()
await expect(element(by.id('listAddRemoveUserModal'))).not.toBeVisible()
})
+
+ it('Can report a mute list', async () => {
+ await element(by.id('bottomBarHomeBtn')).tap()
+ // Last test leaves us in the list view so we are going back 1 screen to the lists list screen
+ await element(by.id('viewHeaderDrawerBtn')).tap()
+ // then to the moderation screen
+ await element(by.id('viewHeaderDrawerBtn')).tap()
+ // then to the home screen
+ await element(by.id('viewHeaderDrawerBtn')).tap()
+ // then open the drawer to go to settings
+ await element(by.id('viewHeaderDrawerBtn')).tap()
+ await element(by.id('menuItemButton-Settings')).tap()
+ await element(by.id('signOutBtn')).tap()
+ await expect(element(by.id('signInButton'))).toBeVisible()
+ await login(service, 'bob.test', 'hunter2')
+ await element(by.id('bottomBarSearchBtn')).tap()
+ await element(by.id('searchTextInput')).typeText('alice')
+ await element(by.id('searchAutoCompleteResult-alice.test')).tap()
+ await element(by.id('selector-3')).tap()
+ await element(by.id('list-Bad Ppl')).tap()
+ await element(by.id('reportListBtn')).tap()
+ await expect(element(by.id('reportModal'))).toBeVisible()
+ await expect(element(by.text('Report List'))).toBeVisible()
+ await element(
+ by.id('reportReasonRadios-com.atproto.moderation.defs#reasonRude'),
+ ).tap()
+ await element(by.id('sendReportBtn')).tap()
+ await expect(element(by.id('reportModal'))).not.toBeVisible()
+ })
})
diff --git a/__e2e__/tests/profile-screen.test.ts b/__e2e__/tests/profile-screen.test.ts
index 59b7326b..da798009 100644
--- a/__e2e__/tests/profile-screen.test.ts
+++ b/__e2e__/tests/profile-screen.test.ts
@@ -125,12 +125,12 @@ describe('Profile screen', () => {
it('Can report another user', async () => {
await element(by.id('profileHeaderDropdownBtn')).tap()
await element(by.text('Report Account')).tap()
- await expect(element(by.id('reportAccountModal'))).toBeVisible()
+ await expect(element(by.id('reportModal'))).toBeVisible()
await element(
- by.id('reportAccountRadios-com.atproto.moderation.defs#reasonSpam'),
+ by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
).tap()
await element(by.id('sendReportBtn')).tap()
- await expect(element(by.id('reportAccountModal'))).not.toBeVisible()
+ await expect(element(by.id('reportModal'))).not.toBeVisible()
})
it('Can like posts', async () => {
@@ -173,11 +173,11 @@ describe('Profile screen', () => {
const posts = by.id('feedItem-by-bob.test')
await element(by.id('postDropdownBtn').withAncestor(posts)).atIndex(0).tap()
await element(by.text('Report post')).tap()
- await expect(element(by.id('reportPostModal'))).toBeVisible()
+ await expect(element(by.id('reportModal'))).toBeVisible()
await element(
- by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'),
+ by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
).tap()
await element(by.id('sendReportBtn')).tap()
- await expect(element(by.id('reportPostModal'))).not.toBeVisible()
+ await expect(element(by.id('reportModal'))).not.toBeVisible()
})
})
diff --git a/__e2e__/tests/thread-screen.test.ts b/__e2e__/tests/thread-screen.test.ts
index 081282a3..0964988e 100644
--- a/__e2e__/tests/thread-screen.test.ts
+++ b/__e2e__/tests/thread-screen.test.ts
@@ -105,23 +105,23 @@ describe('Thread screen', () => {
const post = by.id('postThreadItem-by-bob.test')
await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap()
await element(by.text('Report post')).tap()
- await expect(element(by.id('reportPostModal'))).toBeVisible()
+ await expect(element(by.id('reportModal'))).toBeVisible()
await element(
- by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'),
+ by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
).tap()
await element(by.id('sendReportBtn')).tap()
- await expect(element(by.id('reportPostModal'))).not.toBeVisible()
+ await expect(element(by.id('reportModal'))).not.toBeVisible()
})
it('Can report a reply post', async () => {
const post = by.id('postThreadItem-by-carla.test')
await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap()
await element(by.text('Report post')).tap()
- await expect(element(by.id('reportPostModal'))).toBeVisible()
+ await expect(element(by.id('reportModal'))).toBeVisible()
await element(
- by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'),
+ by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
).tap()
await element(by.id('sendReportBtn')).tap()
- await expect(element(by.id('reportPostModal'))).not.toBeVisible()
+ await expect(element(by.id('reportModal'))).not.toBeVisible()
})
})
diff --git a/src/state/models/content/list.ts b/src/state/models/content/list.ts
index 2498cf58..5d4ffb4f 100644
--- a/src/state/models/content/list.ts
+++ b/src/state/models/content/list.ts
@@ -306,7 +306,7 @@ export class ListModel {
this.hasMore = !!this.loadMoreCursor
this.list = res.data.list
this.items = this.items.concat(
- res.data.items.map(item => ({...item, _reactKey: item.subject})),
+ res.data.items.map(item => ({...item, _reactKey: item.subject.did})),
)
}
}
diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts
index 348fa489..e5fd5d42 100644
--- a/src/state/models/ui/shell.ts
+++ b/src/state/models/ui/shell.ts
@@ -48,16 +48,15 @@ export interface ModerationDetailsModal {
moderation: ModerationUI
}
-export interface ReportPostModal {
- name: 'report-post'
- postUri: string
- postCid: string
-}
-
-export interface ReportAccountModal {
- name: 'report-account'
- did: string
-}
+export type ReportModal = {
+ name: 'report'
+} & (
+ | {
+ uri: string
+ cid: string
+ }
+ | {did: string}
+)
export interface CreateOrEditMuteListModal {
name: 'create-or-edit-mute-list'
@@ -159,8 +158,7 @@ export type Modal =
// Moderation
| ModerationDetailsModal
- | ReportAccountModal
- | ReportPostModal
+ | ReportModal
| CreateOrEditMuteListModal
| ListAddRemoveUserModal
diff --git a/src/view/com/lists/ListActions.tsx b/src/view/com/lists/ListActions.tsx
index ee5a2afc..35333819 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}>
,
+ !isOwner && (
+
+ ),
]
// 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 188518ea..94e22f35 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}
/>
)}
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index ce5dc40e..efd06412 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 =
- } else if (activeModal?.name === 'report-post') {
- snapPoints = ReportPostModal.snapPoints
- element =
- } else if (activeModal?.name === 'report-account') {
- snapPoints = ReportAccountModal.snapPoints
- element =
+ } else if (activeModal?.name === 'report') {
+ snapPoints = ReportModal.snapPoints
+ element =
} else if (activeModal?.name === 'create-or-edit-mute-list') {
snapPoints = CreateOrEditMuteListModal.snapPoints
element =
diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx
index 0f624782..0e28b161 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 =
} else if (modal.name === 'server-input') {
element =
- } else if (modal.name === 'report-post') {
- element =
- } else if (modal.name === 'report-account') {
- element =
+ } else if (modal.name === 'report') {
+ element =
} else if (modal.name === 'create-or-edit-mute-list') {
element =
} 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
similarity index 55%
rename from src/view/com/modals/report/ReportPost.tsx
rename to src/view/com/modals/report/Modal.tsx
index 34ec8c2f..f386b110 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()
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('')
@@ -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 (
-
+
- {showTextInput ? (
+ {showDetailsInput ? (
) : (
)}
@@ -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: (
-
-
- Spam
-
- Excessive mentions or replies
-
- ),
- },
- {
- key: ComAtprotoModerationDefs.REASONSEXUAL,
- label: (
-
-
- Unwanted Sexual Content
-
-
- Nudity or pornography not labeled as such
-
-
- ),
- },
- {
- key: '__copyright__',
- label: (
-
-
- Copyright Violation
-
- Contains copyrighted material
-
- ),
- },
- {
- key: ComAtprotoModerationDefs.REASONRUDE,
- label: (
-
-
- Anti-Social Behavior
-
-
- Harassment, trolling, or intolerance
-
-
- ),
- },
- {
- key: ComAtprotoModerationDefs.REASONVIOLATION,
- label: (
-
-
- Illegal and Urgent
-
-
- Glaring violations of law or terms of service
-
-
- ),
- },
- {
- key: ComAtprotoModerationDefs.REASONOTHER,
- label: (
-
-
- Other
-
-
- An issue not included in these options
-
-
- ),
- },
- ],
- [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 (
<>
- Report post
+ Report {collectionName}
- What is the issue with this post?
+ What is the issue with this {collectionName}?
-
{error ? (
) : 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 ? (
<>
+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: ,
+ }))
+ }, [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 (
+
+ )
+}
diff --git a/src/view/com/modals/report/ReportAccount.tsx b/src/view/com/modals/report/ReportAccount.tsx
deleted file mode 100644
index b53c54ca..00000000
--- 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()
- const [issue, setIssue] = useState()
- const onSelectIssue = (v: string) => setIssue(v)
- const [details, setDetails] = useState()
- 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 (
-
- {showDetailsInput ? (
-
- ) : (
-
- )}
-
- )
-}
-
-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: (
-
-
- Misleading Account
-
-
- Impersonation or false claims about identity or affiliation
-
-
- ),
- },
- {
- key: ComAtprotoModerationDefs.REASONSPAM,
- label: (
-
-
- Frequently Posts Unwanted Content
-
-
- Spam; excessive mentions or replies
-
-
- ),
- },
- {
- key: ComAtprotoModerationDefs.REASONVIOLATION,
- label: (
-
-
- Name or Description Violates Community Standards
-
-
- Terms used violate community standards
-
-
- ),
- },
- ],
- [pal],
- )
- return (
- <>
-
- Report Account
-
-
- What is the issue with this account?
-
-
-
- For other issues, please report specific posts.
-
- {error ? (
-
-
-
- ) : undefined}
-
-
- Add details to report
-
- >
- )
-}
-
-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 00000000..ca947ecb
--- /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 f8531d76..dd3fb530 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 27a1f20d..65050d8c 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 22cc6e83..4294508d 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 265f8a94..2da2e215 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(
/>
) : undefined}
- {currentFeed?.isSaved ? (
-
- ) : (
+ {!currentFeed?.isSaved ? (
- )}
+ ) : null}
+
+
+
+
+
)
}, [
@@ -370,6 +410,17 @@ export const CustomFeedScreenInner = observer(
color={pal.colors.icon}
/>
+
)}
@@ -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 0502e8dc..651fac21 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]}
/>