✨ Repurpose report post modal and re-use for list reporting (#1070)
* ✨ Repupose report post modal and re-use for list reporting * ✨ Allow reporting a feed generator * ✨ ♻️ Refactor report modal into one shared component for reporting different collections * ✅ Adjust report option selector in tests * ✅ Add test for list reporting * ♻️ ✨ Refactor reason options and add options for list and feedgen * 🧹 Cleanup remaining todo * Fix to mutelist react keys * Fix regression from rebase * Improve customfeed mobile header --------- Co-authored-by: Paul Frazee <pfrazee@gmail.com>zio/stable
parent
a5762c2d7d
commit
abbc6543f4
|
@ -56,12 +56,12 @@ describe('Home screen', () => {
|
||||||
.atIndex(0)
|
.atIndex(0)
|
||||||
.tap()
|
.tap()
|
||||||
await element(by.text('Report post')).tap()
|
await element(by.text('Report post')).tap()
|
||||||
await expect(element(by.id('reportPostModal'))).toBeVisible()
|
await expect(element(by.id('reportModal'))).toBeVisible()
|
||||||
await element(
|
await element(
|
||||||
by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'),
|
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
|
||||||
).tap()
|
).tap()
|
||||||
await element(by.id('sendReportBtn')).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 () => {
|
it('Can swipe between feeds', async () => {
|
||||||
|
|
|
@ -138,4 +138,33 @@ describe('Mute lists', () => {
|
||||||
await element(by.id('saveBtn')).tap()
|
await element(by.id('saveBtn')).tap()
|
||||||
await expect(element(by.id('listAddRemoveUserModal'))).not.toBeVisible()
|
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()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -125,12 +125,12 @@ describe('Profile screen', () => {
|
||||||
it('Can report another user', async () => {
|
it('Can report another user', async () => {
|
||||||
await element(by.id('profileHeaderDropdownBtn')).tap()
|
await element(by.id('profileHeaderDropdownBtn')).tap()
|
||||||
await element(by.text('Report Account')).tap()
|
await element(by.text('Report Account')).tap()
|
||||||
await expect(element(by.id('reportAccountModal'))).toBeVisible()
|
await expect(element(by.id('reportModal'))).toBeVisible()
|
||||||
await element(
|
await element(
|
||||||
by.id('reportAccountRadios-com.atproto.moderation.defs#reasonSpam'),
|
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
|
||||||
).tap()
|
).tap()
|
||||||
await element(by.id('sendReportBtn')).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 () => {
|
it('Can like posts', async () => {
|
||||||
|
@ -173,11 +173,11 @@ describe('Profile screen', () => {
|
||||||
const posts = by.id('feedItem-by-bob.test')
|
const posts = by.id('feedItem-by-bob.test')
|
||||||
await element(by.id('postDropdownBtn').withAncestor(posts)).atIndex(0).tap()
|
await element(by.id('postDropdownBtn').withAncestor(posts)).atIndex(0).tap()
|
||||||
await element(by.text('Report post')).tap()
|
await element(by.text('Report post')).tap()
|
||||||
await expect(element(by.id('reportPostModal'))).toBeVisible()
|
await expect(element(by.id('reportModal'))).toBeVisible()
|
||||||
await element(
|
await element(
|
||||||
by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'),
|
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
|
||||||
).tap()
|
).tap()
|
||||||
await element(by.id('sendReportBtn')).tap()
|
await element(by.id('sendReportBtn')).tap()
|
||||||
await expect(element(by.id('reportPostModal'))).not.toBeVisible()
|
await expect(element(by.id('reportModal'))).not.toBeVisible()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -105,23 +105,23 @@ describe('Thread screen', () => {
|
||||||
const post = by.id('postThreadItem-by-bob.test')
|
const post = by.id('postThreadItem-by-bob.test')
|
||||||
await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap()
|
await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap()
|
||||||
await element(by.text('Report post')).tap()
|
await element(by.text('Report post')).tap()
|
||||||
await expect(element(by.id('reportPostModal'))).toBeVisible()
|
await expect(element(by.id('reportModal'))).toBeVisible()
|
||||||
await element(
|
await element(
|
||||||
by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'),
|
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
|
||||||
).tap()
|
).tap()
|
||||||
await element(by.id('sendReportBtn')).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 () => {
|
it('Can report a reply post', async () => {
|
||||||
const post = by.id('postThreadItem-by-carla.test')
|
const post = by.id('postThreadItem-by-carla.test')
|
||||||
await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap()
|
await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap()
|
||||||
await element(by.text('Report post')).tap()
|
await element(by.text('Report post')).tap()
|
||||||
await expect(element(by.id('reportPostModal'))).toBeVisible()
|
await expect(element(by.id('reportModal'))).toBeVisible()
|
||||||
await element(
|
await element(
|
||||||
by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'),
|
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
|
||||||
).tap()
|
).tap()
|
||||||
await element(by.id('sendReportBtn')).tap()
|
await element(by.id('sendReportBtn')).tap()
|
||||||
await expect(element(by.id('reportPostModal'))).not.toBeVisible()
|
await expect(element(by.id('reportModal'))).not.toBeVisible()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -306,7 +306,7 @@ export class ListModel {
|
||||||
this.hasMore = !!this.loadMoreCursor
|
this.hasMore = !!this.loadMoreCursor
|
||||||
this.list = res.data.list
|
this.list = res.data.list
|
||||||
this.items = this.items.concat(
|
this.items = this.items.concat(
|
||||||
res.data.items.map(item => ({...item, _reactKey: item.subject})),
|
res.data.items.map(item => ({...item, _reactKey: item.subject.did})),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,16 +48,15 @@ export interface ModerationDetailsModal {
|
||||||
moderation: ModerationUI
|
moderation: ModerationUI
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReportPostModal {
|
export type ReportModal = {
|
||||||
name: 'report-post'
|
name: 'report'
|
||||||
postUri: string
|
} & (
|
||||||
postCid: string
|
| {
|
||||||
}
|
uri: string
|
||||||
|
cid: string
|
||||||
export interface ReportAccountModal {
|
}
|
||||||
name: 'report-account'
|
| {did: string}
|
||||||
did: string
|
)
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateOrEditMuteListModal {
|
export interface CreateOrEditMuteListModal {
|
||||||
name: 'create-or-edit-mute-list'
|
name: 'create-or-edit-mute-list'
|
||||||
|
@ -159,8 +158,7 @@ export type Modal =
|
||||||
|
|
||||||
// Moderation
|
// Moderation
|
||||||
| ModerationDetailsModal
|
| ModerationDetailsModal
|
||||||
| ReportAccountModal
|
| ReportModal
|
||||||
| ReportPostModal
|
|
||||||
| CreateOrEditMuteListModal
|
| CreateOrEditMuteListModal
|
||||||
| ListAddRemoveUserModal
|
| ListAddRemoveUserModal
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ export const ListActions = ({
|
||||||
isOwner,
|
isOwner,
|
||||||
onPressDeleteList,
|
onPressDeleteList,
|
||||||
onPressShareList,
|
onPressShareList,
|
||||||
|
onPressReportList,
|
||||||
reversed = false, // Default value of reversed is false
|
reversed = false, // Default value of reversed is false
|
||||||
}: {
|
}: {
|
||||||
isOwner: boolean
|
isOwner: boolean
|
||||||
|
@ -19,6 +20,7 @@ export const ListActions = ({
|
||||||
onPressEditList?: () => void
|
onPressEditList?: () => void
|
||||||
onPressDeleteList?: () => void
|
onPressDeleteList?: () => void
|
||||||
onPressShareList?: () => void
|
onPressShareList?: () => void
|
||||||
|
onPressReportList?: () => void
|
||||||
reversed?: boolean // New optional prop
|
reversed?: boolean // New optional prop
|
||||||
}) => {
|
}) => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
@ -64,6 +66,17 @@ export const ListActions = ({
|
||||||
onPress={onPressShareList}>
|
onPress={onPressShareList}>
|
||||||
<FontAwesomeIcon icon={'share'} style={[pal.text]} />
|
<FontAwesomeIcon icon={'share'} style={[pal.text]} />
|
||||||
</Button>,
|
</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
|
// If reversed is true, reverse the array to reverse the order of the buttons
|
||||||
|
|
|
@ -45,6 +45,7 @@ export const ListItems = observer(
|
||||||
onPressEditList,
|
onPressEditList,
|
||||||
onPressDeleteList,
|
onPressDeleteList,
|
||||||
onPressShareList,
|
onPressShareList,
|
||||||
|
onPressReportList,
|
||||||
renderEmptyState,
|
renderEmptyState,
|
||||||
testID,
|
testID,
|
||||||
headerOffset = 0,
|
headerOffset = 0,
|
||||||
|
@ -57,6 +58,7 @@ export const ListItems = observer(
|
||||||
onPressEditList: () => void
|
onPressEditList: () => void
|
||||||
onPressDeleteList: () => void
|
onPressDeleteList: () => void
|
||||||
onPressShareList: () => void
|
onPressShareList: () => void
|
||||||
|
onPressReportList: () => void
|
||||||
renderEmptyState?: () => JSX.Element
|
renderEmptyState?: () => JSX.Element
|
||||||
testID?: string
|
testID?: string
|
||||||
headerOffset?: number
|
headerOffset?: number
|
||||||
|
@ -169,6 +171,7 @@ export const ListItems = observer(
|
||||||
onPressEditList={onPressEditList}
|
onPressEditList={onPressEditList}
|
||||||
onPressDeleteList={onPressDeleteList}
|
onPressDeleteList={onPressDeleteList}
|
||||||
onPressShareList={onPressShareList}
|
onPressShareList={onPressShareList}
|
||||||
|
onPressReportList={onPressReportList}
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null
|
||||||
} else if (item === ERROR_ITEM) {
|
} else if (item === ERROR_ITEM) {
|
||||||
|
@ -208,6 +211,7 @@ export const ListItems = observer(
|
||||||
onPressEditList,
|
onPressEditList,
|
||||||
onPressDeleteList,
|
onPressDeleteList,
|
||||||
onPressShareList,
|
onPressShareList,
|
||||||
|
onPressReportList,
|
||||||
onPressTryAgain,
|
onPressTryAgain,
|
||||||
onPressRetryLoadMore,
|
onPressRetryLoadMore,
|
||||||
],
|
],
|
||||||
|
@ -267,6 +271,7 @@ const ListHeader = observer(
|
||||||
onPressEditList,
|
onPressEditList,
|
||||||
onPressDeleteList,
|
onPressDeleteList,
|
||||||
onPressShareList,
|
onPressShareList,
|
||||||
|
onPressReportList,
|
||||||
}: {
|
}: {
|
||||||
list: AppBskyGraphDefs.ListView
|
list: AppBskyGraphDefs.ListView
|
||||||
isOwner: boolean
|
isOwner: boolean
|
||||||
|
@ -274,6 +279,7 @@ const ListHeader = observer(
|
||||||
onPressEditList: () => void
|
onPressEditList: () => void
|
||||||
onPressDeleteList: () => void
|
onPressDeleteList: () => void
|
||||||
onPressShareList: () => void
|
onPressShareList: () => void
|
||||||
|
onPressReportList: () => void
|
||||||
}) => {
|
}) => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
@ -319,6 +325,7 @@ const ListHeader = observer(
|
||||||
onPressEditList={onPressEditList}
|
onPressEditList={onPressEditList}
|
||||||
onToggleSubscribed={onToggleSubscribed}
|
onToggleSubscribed={onToggleSubscribed}
|
||||||
onPressShareList={onPressShareList}
|
onPressShareList={onPressShareList}
|
||||||
|
onPressReportList={onPressReportList}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -13,14 +13,13 @@ import * as ConfirmModal from './Confirm'
|
||||||
import * as EditProfileModal from './EditProfile'
|
import * as EditProfileModal from './EditProfile'
|
||||||
import * as ProfilePreviewModal from './ProfilePreview'
|
import * as ProfilePreviewModal from './ProfilePreview'
|
||||||
import * as ServerInputModal from './ServerInput'
|
import * as ServerInputModal from './ServerInput'
|
||||||
import * as ReportPostModal from './report/ReportPost'
|
|
||||||
import * as RepostModal from './Repost'
|
import * as RepostModal from './Repost'
|
||||||
import * as SelfLabelModal from './SelfLabel'
|
import * as SelfLabelModal from './SelfLabel'
|
||||||
import * as CreateOrEditMuteListModal from './CreateOrEditMuteList'
|
import * as CreateOrEditMuteListModal from './CreateOrEditMuteList'
|
||||||
import * as ListAddRemoveUserModal from './ListAddRemoveUser'
|
import * as ListAddRemoveUserModal from './ListAddRemoveUser'
|
||||||
import * as AltImageModal from './AltImage'
|
import * as AltImageModal from './AltImage'
|
||||||
import * as EditImageModal 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 DeleteAccountModal from './DeleteAccount'
|
||||||
import * as ChangeHandleModal from './ChangeHandle'
|
import * as ChangeHandleModal from './ChangeHandle'
|
||||||
import * as WaitlistModal from './Waitlist'
|
import * as WaitlistModal from './Waitlist'
|
||||||
|
@ -87,12 +86,9 @@ export const ModalsContainer = observer(function ModalsContainer() {
|
||||||
} else if (activeModal?.name === 'server-input') {
|
} else if (activeModal?.name === 'server-input') {
|
||||||
snapPoints = ServerInputModal.snapPoints
|
snapPoints = ServerInputModal.snapPoints
|
||||||
element = <ServerInputModal.Component {...activeModal} />
|
element = <ServerInputModal.Component {...activeModal} />
|
||||||
} else if (activeModal?.name === 'report-post') {
|
} else if (activeModal?.name === 'report') {
|
||||||
snapPoints = ReportPostModal.snapPoints
|
snapPoints = ReportModal.snapPoints
|
||||||
element = <ReportPostModal.Component {...activeModal} />
|
element = <ReportModal.Component {...activeModal} />
|
||||||
} else if (activeModal?.name === 'report-account') {
|
|
||||||
snapPoints = ReportAccountModal.snapPoints
|
|
||||||
element = <ReportAccountModal.Component {...activeModal} />
|
|
||||||
} else if (activeModal?.name === 'create-or-edit-mute-list') {
|
} else if (activeModal?.name === 'create-or-edit-mute-list') {
|
||||||
snapPoints = CreateOrEditMuteListModal.snapPoints
|
snapPoints = CreateOrEditMuteListModal.snapPoints
|
||||||
element = <CreateOrEditMuteListModal.Component {...activeModal} />
|
element = <CreateOrEditMuteListModal.Component {...activeModal} />
|
||||||
|
|
|
@ -10,8 +10,7 @@ import * as ConfirmModal from './Confirm'
|
||||||
import * as EditProfileModal from './EditProfile'
|
import * as EditProfileModal from './EditProfile'
|
||||||
import * as ProfilePreviewModal from './ProfilePreview'
|
import * as ProfilePreviewModal from './ProfilePreview'
|
||||||
import * as ServerInputModal from './ServerInput'
|
import * as ServerInputModal from './ServerInput'
|
||||||
import * as ReportPostModal from './report/ReportPost'
|
import * as ReportModal from './report/Modal'
|
||||||
import * as ReportAccountModal from './report/ReportAccount'
|
|
||||||
import * as CreateOrEditMuteListModal from './CreateOrEditMuteList'
|
import * as CreateOrEditMuteListModal from './CreateOrEditMuteList'
|
||||||
import * as ListAddRemoveUserModal from './ListAddRemoveUser'
|
import * as ListAddRemoveUserModal from './ListAddRemoveUser'
|
||||||
import * as DeleteAccountModal from './DeleteAccount'
|
import * as DeleteAccountModal from './DeleteAccount'
|
||||||
|
@ -76,10 +75,8 @@ function Modal({modal}: {modal: ModalIface}) {
|
||||||
element = <ProfilePreviewModal.Component {...modal} />
|
element = <ProfilePreviewModal.Component {...modal} />
|
||||||
} else if (modal.name === 'server-input') {
|
} else if (modal.name === 'server-input') {
|
||||||
element = <ServerInputModal.Component {...modal} />
|
element = <ServerInputModal.Component {...modal} />
|
||||||
} else if (modal.name === 'report-post') {
|
} else if (modal.name === 'report') {
|
||||||
element = <ReportPostModal.Component {...modal} />
|
element = <ReportModal.Component {...modal} />
|
||||||
} else if (modal.name === 'report-account') {
|
|
||||||
element = <ReportAccountModal.Component {...modal} />
|
|
||||||
} else if (modal.name === 'create-or-edit-mute-list') {
|
} else if (modal.name === 'create-or-edit-mute-list') {
|
||||||
element = <CreateOrEditMuteListModal.Component {...modal} />
|
element = <CreateOrEditMuteListModal.Component {...modal} />
|
||||||
} else if (modal.name === 'list-add-remove-user') {
|
} else if (modal.name === 'list-add-remove-user') {
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import React, {useState, useMemo} from 'react'
|
import React, {useState, useMemo} from 'react'
|
||||||
import {Linking, StyleSheet, TouchableOpacity, View} from 'react-native'
|
import {Linking, StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||||
import {ScrollView} from 'react-native-gesture-handler'
|
import {ScrollView} from 'react-native-gesture-handler'
|
||||||
import {ComAtprotoModerationDefs} from '@atproto/api'
|
import {AtUri} from '@atproto/api'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
import {RadioGroup, RadioGroupItem} from '../../util/forms/RadioGroup'
|
|
||||||
import {Text} from '../../util/text/Text'
|
import {Text} from '../../util/text/Text'
|
||||||
import * as Toast from '../../util/Toast'
|
import * as Toast from '../../util/Toast'
|
||||||
import {ErrorMessage} from '../../util/error/ErrorMessage'
|
import {ErrorMessage} from '../../util/error/ErrorMessage'
|
||||||
|
@ -12,25 +11,43 @@ import {cleanError} from 'lib/strings/errors'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {SendReportButton} from './SendReportButton'
|
import {SendReportButton} from './SendReportButton'
|
||||||
import {InputIssueDetails} from './InputIssueDetails'
|
import {InputIssueDetails} from './InputIssueDetails'
|
||||||
|
import {ReportReasonOptions} from './ReasonOptions'
|
||||||
|
import {CollectionId} from './types'
|
||||||
|
|
||||||
const DMCA_LINK = 'https://bsky.app/support/copyright'
|
const DMCA_LINK = 'https://bsky.app/support/copyright'
|
||||||
|
|
||||||
export const snapPoints = [575]
|
export const snapPoints = [575]
|
||||||
|
|
||||||
export function Component({
|
const CollectionNames = {
|
||||||
postUri,
|
[CollectionId.FeedGenerator]: 'Feed',
|
||||||
postCid,
|
[CollectionId.Profile]: 'Profile',
|
||||||
}: {
|
[CollectionId.List]: 'List',
|
||||||
postUri: string
|
[CollectionId.Post]: 'Post',
|
||||||
postCid: string
|
}
|
||||||
}) {
|
|
||||||
|
type ReportComponentProps =
|
||||||
|
| {
|
||||||
|
uri: string
|
||||||
|
cid: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
did: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Component(content: ReportComponentProps) {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const [isProcessing, setIsProcessing] = useState(false)
|
const [isProcessing, setIsProcessing] = useState(false)
|
||||||
const [showTextInput, setShowTextInput] = useState(false)
|
const [showDetailsInput, setShowDetailsInput] = useState(false)
|
||||||
const [error, setError] = useState<string>()
|
const [error, setError] = useState<string>()
|
||||||
const [issue, setIssue] = useState<string>()
|
const [issue, setIssue] = useState<string>()
|
||||||
const [details, setDetails] = 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 () => {
|
const submitReport = async () => {
|
||||||
setError('')
|
setError('')
|
||||||
|
@ -43,12 +60,14 @@ export function Component({
|
||||||
Linking.openURL(DMCA_LINK)
|
Linking.openURL(DMCA_LINK)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const $type = !isAccountReport
|
||||||
|
? 'com.atproto.repo.strongRef'
|
||||||
|
: 'com.atproto.admin.defs#repoRef'
|
||||||
await store.agent.createModerationReport({
|
await store.agent.createModerationReport({
|
||||||
reasonType: issue,
|
reasonType: issue,
|
||||||
subject: {
|
subject: {
|
||||||
$type: 'com.atproto.repo.strongRef',
|
$type,
|
||||||
uri: postUri,
|
...content,
|
||||||
cid: postCid,
|
|
||||||
},
|
},
|
||||||
reason: details,
|
reason: details,
|
||||||
})
|
})
|
||||||
|
@ -63,13 +82,13 @@ export function Component({
|
||||||
}
|
}
|
||||||
|
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
setShowTextInput(false)
|
setShowDetailsInput(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView testID="reportPostModal" style={[s.flex1, pal.view]}>
|
<ScrollView testID="reportModal" style={[s.flex1, pal.view]}>
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
{showTextInput ? (
|
{showDetailsInput ? (
|
||||||
<InputIssueDetails
|
<InputIssueDetails
|
||||||
details={details}
|
details={details}
|
||||||
setDetails={setDetails}
|
setDetails={setDetails}
|
||||||
|
@ -79,12 +98,13 @@ export function Component({
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<SelectIssue
|
<SelectIssue
|
||||||
setShowTextInput={setShowTextInput}
|
setShowDetailsInput={setShowDetailsInput}
|
||||||
error={error}
|
error={error}
|
||||||
issue={issue}
|
issue={issue}
|
||||||
setIssue={setIssue}
|
setIssue={setIssue}
|
||||||
submitReport={submitReport}
|
submitReport={submitReport}
|
||||||
isProcessing={isProcessing}
|
isProcessing={isProcessing}
|
||||||
|
atUri={atUri}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</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 = ({
|
const SelectIssue = ({
|
||||||
error,
|
error,
|
||||||
setShowTextInput,
|
setShowDetailsInput,
|
||||||
issue,
|
issue,
|
||||||
setIssue,
|
setIssue,
|
||||||
submitReport,
|
submitReport,
|
||||||
isProcessing,
|
isProcessing,
|
||||||
|
atUri,
|
||||||
}: {
|
}: {
|
||||||
error: string | undefined
|
error: string | undefined
|
||||||
setShowTextInput: (v: boolean) => void
|
setShowDetailsInput: (v: boolean) => void
|
||||||
issue: string | undefined
|
issue: string | undefined
|
||||||
setIssue: (v: string) => void
|
setIssue: (v: string) => void
|
||||||
submitReport: () => void
|
submitReport: () => void
|
||||||
isProcessing: boolean
|
isProcessing: boolean
|
||||||
|
atUri: AtUri | null
|
||||||
}) => {
|
}) => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const ITEMS: RadioGroupItem[] = useMemo(
|
const collectionName = getCollectionNameForReport(atUri)
|
||||||
() => [
|
|
||||||
{
|
|
||||||
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 onSelectIssue = (v: string) => setIssue(v)
|
||||||
const goToDetails = () => {
|
const goToDetails = () => {
|
||||||
if (issue === '__copyright__') {
|
if (issue === '__copyright__') {
|
||||||
Linking.openURL(DMCA_LINK)
|
Linking.openURL(DMCA_LINK)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setShowTextInput(true)
|
setShowDetailsInput(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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]}>
|
<Text style={[pal.textLight, styles.description]}>
|
||||||
What is the issue with this post?
|
What is the issue with this {collectionName}?
|
||||||
</Text>
|
</Text>
|
||||||
<RadioGroup
|
<ReportReasonOptions
|
||||||
testID="reportPostRadios"
|
atUri={atUri}
|
||||||
items={ITEMS}
|
selectedIssue={issue}
|
||||||
onSelect={onSelectIssue}
|
onSelectIssue={onSelectIssue}
|
||||||
/>
|
/>
|
||||||
{error ? (
|
{error ? (
|
||||||
<View style={s.mt10}>
|
<View style={s.mt10}>
|
||||||
<ErrorMessage message={error} />
|
<ErrorMessage message={error} />
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : 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
|
<SendReportButton
|
||||||
onPress={submitReport}
|
onPress={submitReport}
|
|
@ -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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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,
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -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',
|
||||||
|
}
|
|
@ -245,7 +245,7 @@ const ProfileHeaderLoaded = observer(
|
||||||
const onPressReportAccount = React.useCallback(() => {
|
const onPressReportAccount = React.useCallback(() => {
|
||||||
track('ProfileHeader:ReportAccountButtonClicked')
|
track('ProfileHeader:ReportAccountButtonClicked')
|
||||||
store.shell.openModal({
|
store.shell.openModal({
|
||||||
name: 'report-account',
|
name: 'report',
|
||||||
did: view.did,
|
did: view.did,
|
||||||
})
|
})
|
||||||
}, [track, store, view])
|
}, [track, store, view])
|
||||||
|
|
|
@ -102,9 +102,9 @@ export function PostDropdownBtn({
|
||||||
label: 'Report post',
|
label: 'Report post',
|
||||||
onPress() {
|
onPress() {
|
||||||
store.shell.openModal({
|
store.shell.openModal({
|
||||||
name: 'report-post',
|
name: 'report',
|
||||||
postUri: itemUri,
|
uri: itemUri,
|
||||||
postCid: itemCid,
|
cid: itemCid,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
testID: 'postDropdownReportBtn',
|
testID: 'postDropdownReportBtn',
|
||||||
|
|
|
@ -89,6 +89,7 @@ import {faXmark} from '@fortawesome/free-solid-svg-icons/faXmark'
|
||||||
import {faPlay} from '@fortawesome/free-solid-svg-icons/faPlay'
|
import {faPlay} from '@fortawesome/free-solid-svg-icons/faPlay'
|
||||||
import {faPause} from '@fortawesome/free-solid-svg-icons/faPause'
|
import {faPause} from '@fortawesome/free-solid-svg-icons/faPause'
|
||||||
import {faThumbtack} from '@fortawesome/free-solid-svg-icons/faThumbtack'
|
import {faThumbtack} from '@fortawesome/free-solid-svg-icons/faThumbtack'
|
||||||
|
import {faList} from '@fortawesome/free-solid-svg-icons/faList'
|
||||||
|
|
||||||
export function setup() {
|
export function setup() {
|
||||||
library.add(
|
library.add(
|
||||||
|
@ -181,5 +182,6 @@ export function setup() {
|
||||||
faXmark,
|
faXmark,
|
||||||
faPlay,
|
faPlay,
|
||||||
faPause,
|
faPause,
|
||||||
|
faList,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -188,6 +188,15 @@ export const CustomFeedScreenInner = observer(
|
||||||
track('CustomFeed:Share')
|
track('CustomFeed:Share')
|
||||||
}, [handleOrDid, rkey, track])
|
}, [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(() => {
|
const onScrollToTop = React.useCallback(() => {
|
||||||
scrollElRef.current?.scrollToOffset({offset: 0, animated: true})
|
scrollElRef.current?.scrollToOffset({offset: 0, animated: true})
|
||||||
resetMainScroll()
|
resetMainScroll()
|
||||||
|
@ -200,15 +209,37 @@ export const CustomFeedScreenInner = observer(
|
||||||
const dropdownItems: DropdownItem[] = React.useMemo(() => {
|
const dropdownItems: DropdownItem[] = React.useMemo(() => {
|
||||||
let items: DropdownItem[] = [
|
let items: DropdownItem[] = [
|
||||||
{
|
{
|
||||||
testID: 'feedHeaderDropdownRemoveBtn',
|
testID: 'feedHeaderDropdownToggleSavedBtn',
|
||||||
label: 'Remove from my feeds',
|
label: currentFeed?.isSaved
|
||||||
|
? 'Remove from my feeds'
|
||||||
|
: 'Add to my feeds',
|
||||||
onPress: onToggleSaved,
|
onPress: onToggleSaved,
|
||||||
icon: {
|
icon: currentFeed?.isSaved
|
||||||
|
? {
|
||||||
ios: {
|
ios: {
|
||||||
name: 'trash',
|
name: 'trash',
|
||||||
},
|
},
|
||||||
android: 'ic_delete',
|
android: 'ic_delete',
|
||||||
web: 'trash',
|
web: 'trash',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
ios: {
|
||||||
|
name: 'plus',
|
||||||
|
},
|
||||||
|
android: '',
|
||||||
|
web: 'plus',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testID: 'feedHeaderDropdownReportBtn',
|
||||||
|
label: 'Report feed',
|
||||||
|
onPress: onPressReport,
|
||||||
|
icon: {
|
||||||
|
ios: {
|
||||||
|
name: 'exclamationmark.triangle',
|
||||||
|
},
|
||||||
|
android: 'ic_menu_report_image',
|
||||||
|
web: 'circle-exclamation',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -225,7 +256,7 @@ export const CustomFeedScreenInner = observer(
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
return items
|
return items
|
||||||
}, [onToggleSaved, onPressShare])
|
}, [currentFeed?.isSaved, onToggleSaved, onPressReport, onPressShare])
|
||||||
|
|
||||||
const renderHeaderBtns = React.useCallback(() => {
|
const renderHeaderBtns = React.useCallback(() => {
|
||||||
return (
|
return (
|
||||||
|
@ -258,12 +289,7 @@ export const CustomFeedScreenInner = observer(
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
{currentFeed?.isSaved ? (
|
{!currentFeed?.isSaved ? (
|
||||||
<NativeDropdown
|
|
||||||
testID="feedHeaderDropdownBtn"
|
|
||||||
items={dropdownItems}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Button
|
<Button
|
||||||
type="default-light"
|
type="default-light"
|
||||||
onPress={onToggleSaved}
|
onPress={onToggleSaved}
|
||||||
|
@ -275,7 +301,21 @@ export const CustomFeedScreenInner = observer(
|
||||||
Add to My Feeds
|
Add to My Feeds
|
||||||
</Text>
|
</Text>
|
||||||
</Button>
|
</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>
|
</View>
|
||||||
)
|
)
|
||||||
}, [
|
}, [
|
||||||
|
@ -370,6 +410,17 @@ export const CustomFeedScreenInner = observer(
|
||||||
color={pal.colors.icon}
|
color={pal.colors.icon}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
accessibilityLabel="Report this feed"
|
||||||
|
accessibilityHint=""
|
||||||
|
onPress={onPressReport}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="circle-exclamation"
|
||||||
|
size={18}
|
||||||
|
color={pal.colors.icon}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
@ -419,6 +470,7 @@ export const CustomFeedScreenInner = observer(
|
||||||
onToggleLiked,
|
onToggleLiked,
|
||||||
onPressShare,
|
onPressShare,
|
||||||
handleOrDid,
|
handleOrDid,
|
||||||
|
onPressReport,
|
||||||
rkey,
|
rkey,
|
||||||
isPinned,
|
isPinned,
|
||||||
onTogglePinned,
|
onTogglePinned,
|
||||||
|
|
|
@ -86,6 +86,15 @@ export const ProfileListScreen = withAuthRequired(
|
||||||
})
|
})
|
||||||
}, [store, list, navigation])
|
}, [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 onPressShareList = React.useCallback(() => {
|
||||||
const url = toShareUrl(`/profile/${name}/lists/${rkey}`)
|
const url = toShareUrl(`/profile/${name}/lists/${rkey}`)
|
||||||
shareUrl(url)
|
shareUrl(url)
|
||||||
|
@ -104,6 +113,7 @@ export const ProfileListScreen = withAuthRequired(
|
||||||
onPressEditList={onPressEditList}
|
onPressEditList={onPressEditList}
|
||||||
onToggleSubscribed={onToggleSubscribed}
|
onToggleSubscribed={onToggleSubscribed}
|
||||||
onPressShareList={onPressShareList}
|
onPressShareList={onPressShareList}
|
||||||
|
onPressReportList={onPressReportList}
|
||||||
reversed={true}
|
reversed={true}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -114,6 +124,7 @@ export const ProfileListScreen = withAuthRequired(
|
||||||
onPressEditList,
|
onPressEditList,
|
||||||
onPressShareList,
|
onPressShareList,
|
||||||
onToggleSubscribed,
|
onToggleSubscribed,
|
||||||
|
onPressReportList,
|
||||||
])
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -132,6 +143,7 @@ export const ProfileListScreen = withAuthRequired(
|
||||||
onToggleSubscribed={onToggleSubscribed}
|
onToggleSubscribed={onToggleSubscribed}
|
||||||
onPressEditList={onPressEditList}
|
onPressEditList={onPressEditList}
|
||||||
onPressDeleteList={onPressDeleteList}
|
onPressDeleteList={onPressDeleteList}
|
||||||
|
onPressReportList={onPressReportList}
|
||||||
onPressShareList={onPressShareList}
|
onPressShareList={onPressShareList}
|
||||||
style={[s.flex1]}
|
style={[s.flex1]}
|
||||||
/>
|
/>
|
||||||
|
|
Loading…
Reference in New Issue