Replace the report modal entirely with the report dialog
This commit is contained in:
@@ -47,6 +47,9 @@ export function SelectReportOptionView({
|
|||||||
} else if (props.params.type === 'list') {
|
} else if (props.params.type === 'list') {
|
||||||
title = _(msg`Report this list`)
|
title = _(msg`Report this list`)
|
||||||
description = _(msg`Why should this list be reviewed?`)
|
description = _(msg`Why should this list be reviewed?`)
|
||||||
|
} else if (props.params.type === 'feedgen') {
|
||||||
|
title = _(msg`Report this feed`)
|
||||||
|
description = _(msg`Why should this feed be reviewed?`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export type ReportDialogProps = {
|
|||||||
control: Dialog.DialogOuterProps['control']
|
control: Dialog.DialogOuterProps['control']
|
||||||
params:
|
params:
|
||||||
| {
|
| {
|
||||||
type: 'post' | 'list' | 'other'
|
type: 'post' | 'list' | 'feedgen' | 'other'
|
||||||
uri: string
|
uri: string
|
||||||
cid: string
|
cid: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
export const LOGIN_INCLUDE_DEV_SERVERS = true
|
export const LOGIN_INCLUDE_DEV_SERVERS = true
|
||||||
export const PWI_ENABLED = true
|
export const PWI_ENABLED = true
|
||||||
export const NEW_ONBOARDING_ENABLED = true
|
export const NEW_ONBOARDING_ENABLED = true
|
||||||
export const NEW_REPORT_DIALOG_ENABLED = true
|
|
||||||
|
|||||||
@@ -79,6 +79,14 @@ export function useReportOptions(): ReportOptions {
|
|||||||
},
|
},
|
||||||
...common,
|
...common,
|
||||||
],
|
],
|
||||||
|
feedgen: [
|
||||||
|
{
|
||||||
|
reason: ComAtprotoModerationDefs.REASONVIOLATION,
|
||||||
|
title: _(msg`Name or Description Violates Community Standards`),
|
||||||
|
description: _(msg`Terms used violate community standards`),
|
||||||
|
},
|
||||||
|
...common,
|
||||||
|
],
|
||||||
other: common,
|
other: common,
|
||||||
}
|
}
|
||||||
}, [_])
|
}, [_])
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {shareUrl} from 'lib/sharing'
|
|||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {Shadow} from '#/state/cache/types'
|
import {Shadow} from '#/state/cache/types'
|
||||||
import {NEW_REPORT_DIALOG_ENABLED} from '#/lib/build-flags'
|
|
||||||
|
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import * as Toast from 'view/com/util/Toast'
|
import * as Toast from 'view/com/util/Toast'
|
||||||
@@ -40,7 +39,7 @@ export function ProfileHeaderDropdownBtn({
|
|||||||
const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
|
const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
|
||||||
const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
|
const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const control = useReportDialogControl()
|
const reportDialogControl = useReportDialogControl()
|
||||||
|
|
||||||
const invalidateProfileQuery = React.useCallback(() => {
|
const invalidateProfileQuery = React.useCallback(() => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
@@ -137,15 +136,8 @@ export function ProfileHeaderDropdownBtn({
|
|||||||
|
|
||||||
const onPressReportAccount = React.useCallback(() => {
|
const onPressReportAccount = React.useCallback(() => {
|
||||||
track('ProfileHeader:ReportAccountButtonClicked')
|
track('ProfileHeader:ReportAccountButtonClicked')
|
||||||
if (NEW_REPORT_DIALOG_ENABLED) {
|
reportDialogControl.open()
|
||||||
control.open()
|
}, [track, reportDialogControl])
|
||||||
} else {
|
|
||||||
openModal({
|
|
||||||
name: 'report',
|
|
||||||
did: profile.did,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [track, openModal, profile, control])
|
|
||||||
|
|
||||||
const isMe = React.useMemo(
|
const isMe = React.useMemo(
|
||||||
() => currentAccount?.did === profile.did,
|
() => currentAccount?.did === profile.did,
|
||||||
@@ -269,7 +261,7 @@ export function ProfileHeaderDropdownBtn({
|
|||||||
return dropdownItems?.length ? (
|
return dropdownItems?.length ? (
|
||||||
<>
|
<>
|
||||||
<ReportDialog
|
<ReportDialog
|
||||||
control={control}
|
control={reportDialogControl}
|
||||||
params={{type: 'account', did: profile.did}}
|
params={{type: 'account', did: profile.did}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -26,16 +26,6 @@ export interface EditProfileModal {
|
|||||||
onUpdate?: () => void
|
onUpdate?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ReportModal = {
|
|
||||||
name: 'report'
|
|
||||||
} & (
|
|
||||||
| {
|
|
||||||
uri: string
|
|
||||||
cid: string
|
|
||||||
}
|
|
||||||
| {did: string}
|
|
||||||
)
|
|
||||||
|
|
||||||
export interface CreateOrEditListModal {
|
export interface CreateOrEditListModal {
|
||||||
name: 'create-or-edit-list'
|
name: 'create-or-edit-list'
|
||||||
purpose?: string
|
purpose?: string
|
||||||
@@ -181,9 +171,6 @@ export type Modal =
|
|||||||
| ContentLanguagesSettingsModal
|
| ContentLanguagesSettingsModal
|
||||||
| PostLanguagesSettingsModal
|
| PostLanguagesSettingsModal
|
||||||
|
|
||||||
// Moderation
|
|
||||||
| ReportModal
|
|
||||||
|
|
||||||
// Lists
|
// Lists
|
||||||
| CreateOrEditListModal
|
| CreateOrEditListModal
|
||||||
| UserAddRemoveListsModal
|
| UserAddRemoveListsModal
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import * as UserAddRemoveListsModal from './UserAddRemoveLists'
|
|||||||
import * as ListAddUserModal from './ListAddRemoveUsers'
|
import * as ListAddUserModal from './ListAddRemoveUsers'
|
||||||
import * as AltImageModal from './AltImage'
|
import * as AltImageModal from './AltImage'
|
||||||
import * as EditImageModal from './AltImage'
|
import * as EditImageModal from './AltImage'
|
||||||
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 InviteCodesModal from './InviteCodes'
|
import * as InviteCodesModal from './InviteCodes'
|
||||||
@@ -69,9 +68,6 @@ export function ModalsContainer() {
|
|||||||
} else if (activeModal?.name === 'edit-profile') {
|
} else if (activeModal?.name === 'edit-profile') {
|
||||||
snapPoints = EditProfileModal.snapPoints
|
snapPoints = EditProfileModal.snapPoints
|
||||||
element = <EditProfileModal.Component {...activeModal} />
|
element = <EditProfileModal.Component {...activeModal} />
|
||||||
} else if (activeModal?.name === 'report') {
|
|
||||||
snapPoints = ReportModal.snapPoints
|
|
||||||
element = <ReportModal.Component {...activeModal} />
|
|
||||||
} else if (activeModal?.name === 'create-or-edit-list') {
|
} else if (activeModal?.name === 'create-or-edit-list') {
|
||||||
snapPoints = CreateOrEditListModal.snapPoints
|
snapPoints = CreateOrEditListModal.snapPoints
|
||||||
element = <CreateOrEditListModal.Component {...activeModal} />
|
element = <CreateOrEditListModal.Component {...activeModal} />
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {useModals, useModalControls} from '#/state/modals'
|
|||||||
import type {Modal as ModalIface} from '#/state/modals'
|
import type {Modal as ModalIface} from '#/state/modals'
|
||||||
import * as ConfirmModal from './Confirm'
|
import * as ConfirmModal from './Confirm'
|
||||||
import * as EditProfileModal from './EditProfile'
|
import * as EditProfileModal from './EditProfile'
|
||||||
import * as ReportModal from './report/Modal'
|
|
||||||
import * as CreateOrEditListModal from './CreateOrEditList'
|
import * as CreateOrEditListModal from './CreateOrEditList'
|
||||||
import * as UserAddRemoveLists from './UserAddRemoveLists'
|
import * as UserAddRemoveLists from './UserAddRemoveLists'
|
||||||
import * as ListAddUserModal from './ListAddRemoveUsers'
|
import * as ListAddUserModal from './ListAddRemoveUsers'
|
||||||
@@ -79,8 +78,6 @@ function Modal({modal}: {modal: ModalIface}) {
|
|||||||
element = <ConfirmModal.Component {...modal} />
|
element = <ConfirmModal.Component {...modal} />
|
||||||
} else if (modal.name === 'edit-profile') {
|
} else if (modal.name === 'edit-profile') {
|
||||||
element = <EditProfileModal.Component {...modal} />
|
element = <EditProfileModal.Component {...modal} />
|
||||||
} else if (modal.name === 'report') {
|
|
||||||
element = <ReportModal.Component {...modal} />
|
|
||||||
} else if (modal.name === 'create-or-edit-list') {
|
} else if (modal.name === 'create-or-edit-list') {
|
||||||
element = <CreateOrEditListModal.Component {...modal} />
|
element = <CreateOrEditListModal.Component {...modal} />
|
||||||
} else if (modal.name === 'user-add-remove-lists') {
|
} else if (modal.name === 'user-add-remove-lists') {
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {View, TouchableOpacity, StyleSheet} from 'react-native'
|
|
||||||
import {TextInput} from '../util'
|
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|
||||||
import {CharProgress} from '../../composer/char-progress/CharProgress'
|
|
||||||
import {Text} from '../../util/text/Text'
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
|
||||||
import {s} from 'lib/styles'
|
|
||||||
import {SendReportButton} from './SendReportButton'
|
|
||||||
import {Trans, msg} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
|
|
||||||
export function InputIssueDetails({
|
|
||||||
details,
|
|
||||||
setDetails,
|
|
||||||
goBack,
|
|
||||||
submitReport,
|
|
||||||
isProcessing,
|
|
||||||
}: {
|
|
||||||
details: string | undefined
|
|
||||||
setDetails: (v: string) => void
|
|
||||||
goBack: () => void
|
|
||||||
submitReport: () => void
|
|
||||||
isProcessing: boolean
|
|
||||||
}) {
|
|
||||||
const pal = usePalette('default')
|
|
||||||
const {_} = useLingui()
|
|
||||||
const {isMobile} = useWebMediaQueries()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
marginTop: isMobile ? 12 : 0,
|
|
||||||
}}>
|
|
||||||
<TouchableOpacity
|
|
||||||
testID="addDetailsBtn"
|
|
||||||
style={[s.mb10, styles.backBtn]}
|
|
||||||
onPress={goBack}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={_(msg`Add details`)}
|
|
||||||
accessibilityHint="Add more details to your report">
|
|
||||||
<FontAwesomeIcon size={18} icon="angle-left" style={[pal.link]} />
|
|
||||||
<Text style={[pal.text, s.f18, pal.link]}>
|
|
||||||
{' '}
|
|
||||||
<Trans>Back</Trans>
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<View style={[pal.btn, styles.detailsInputContainer]}>
|
|
||||||
<TextInput
|
|
||||||
accessibilityLabel={_(msg`Text input field`)}
|
|
||||||
accessibilityHint="Enter a reason for reporting this post."
|
|
||||||
placeholder="Enter a reason or any other details here."
|
|
||||||
placeholderTextColor={pal.textLight.color}
|
|
||||||
value={details}
|
|
||||||
onChangeText={setDetails}
|
|
||||||
autoFocus={true}
|
|
||||||
numberOfLines={3}
|
|
||||||
multiline={true}
|
|
||||||
textAlignVertical="top"
|
|
||||||
maxLength={300}
|
|
||||||
style={[styles.detailsInput, pal.text]}
|
|
||||||
/>
|
|
||||||
<View style={styles.detailsInputBottomBar}>
|
|
||||||
<View style={styles.charCounter}>
|
|
||||||
<CharProgress count={details?.length || 0} />
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<SendReportButton onPress={submitReport} isProcessing={isProcessing} />
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
backBtn: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
detailsInputContainer: {
|
|
||||||
borderRadius: 8,
|
|
||||||
},
|
|
||||||
detailsInput: {
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
paddingTop: 12,
|
|
||||||
paddingBottom: 12,
|
|
||||||
borderRadius: 8,
|
|
||||||
minHeight: 100,
|
|
||||||
fontSize: 16,
|
|
||||||
},
|
|
||||||
detailsInputBottomBar: {
|
|
||||||
alignSelf: 'flex-end',
|
|
||||||
},
|
|
||||||
charCounter: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingRight: 10,
|
|
||||||
paddingBottom: 8,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
import React, {useState, useMemo} from 'react'
|
|
||||||
import {Linking, StyleSheet, TouchableOpacity, View} from 'react-native'
|
|
||||||
import {ScrollView} from 'react-native-gesture-handler'
|
|
||||||
import {AtUri} from '@atproto/api'
|
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
|
||||||
import {s} from 'lib/styles'
|
|
||||||
import {Text} from '../../util/text/Text'
|
|
||||||
import * as Toast from '../../util/Toast'
|
|
||||||
import {ErrorMessage} from '../../util/error/ErrorMessage'
|
|
||||||
import {cleanError} from 'lib/strings/errors'
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {SendReportButton} from './SendReportButton'
|
|
||||||
import {InputIssueDetails} from './InputIssueDetails'
|
|
||||||
import {ReportReasonOptions} from './ReasonOptions'
|
|
||||||
import {CollectionId} from './types'
|
|
||||||
import {Trans, msg} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {useModalControls} from '#/state/modals'
|
|
||||||
import {getAgent} from '#/state/session'
|
|
||||||
|
|
||||||
const DMCA_LINK = 'https://bsky.social/about/support/copyright'
|
|
||||||
|
|
||||||
export const snapPoints = [575]
|
|
||||||
|
|
||||||
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 {closeModal} = useModalControls()
|
|
||||||
const pal = usePalette('default')
|
|
||||||
const {isMobile} = useWebMediaQueries()
|
|
||||||
const [isProcessing, setIsProcessing] = 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('')
|
|
||||||
if (!issue) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setIsProcessing(true)
|
|
||||||
try {
|
|
||||||
if (issue === '__copyright__') {
|
|
||||||
Linking.openURL(DMCA_LINK)
|
|
||||||
closeModal()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const $type = !isAccountReport
|
|
||||||
? 'com.atproto.repo.strongRef'
|
|
||||||
: 'com.atproto.admin.defs#repoRef'
|
|
||||||
await getAgent().createModerationReport({
|
|
||||||
reasonType: issue,
|
|
||||||
subject: {
|
|
||||||
$type,
|
|
||||||
...content,
|
|
||||||
},
|
|
||||||
reason: details,
|
|
||||||
})
|
|
||||||
Toast.show("Thank you for your report! We'll look into it promptly.")
|
|
||||||
|
|
||||||
closeModal()
|
|
||||||
return
|
|
||||||
} catch (e: any) {
|
|
||||||
setError(cleanError(e))
|
|
||||||
setIsProcessing(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const goBack = () => {
|
|
||||||
setShowDetailsInput(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollView testID="reportModal" style={[s.flex1, pal.view]}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.container,
|
|
||||||
isMobile && {
|
|
||||||
paddingBottom: 40,
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
{showDetailsInput ? (
|
|
||||||
<InputIssueDetails
|
|
||||||
details={details}
|
|
||||||
setDetails={setDetails}
|
|
||||||
goBack={goBack}
|
|
||||||
submitReport={submitReport}
|
|
||||||
isProcessing={isProcessing}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<SelectIssue
|
|
||||||
setShowDetailsInput={setShowDetailsInput}
|
|
||||||
error={error}
|
|
||||||
issue={issue}
|
|
||||||
setIssue={setIssue}
|
|
||||||
submitReport={submitReport}
|
|
||||||
isProcessing={isProcessing}
|
|
||||||
atUri={atUri}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</ScrollView>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
setShowDetailsInput,
|
|
||||||
issue,
|
|
||||||
setIssue,
|
|
||||||
submitReport,
|
|
||||||
isProcessing,
|
|
||||||
atUri,
|
|
||||||
}: {
|
|
||||||
error: string | undefined
|
|
||||||
setShowDetailsInput: (v: boolean) => void
|
|
||||||
issue: string | undefined
|
|
||||||
setIssue: (v: string) => void
|
|
||||||
submitReport: () => void
|
|
||||||
isProcessing: boolean
|
|
||||||
atUri: AtUri | null
|
|
||||||
}) => {
|
|
||||||
const pal = usePalette('default')
|
|
||||||
const {_} = useLingui()
|
|
||||||
const collectionName = getCollectionNameForReport(atUri)
|
|
||||||
const onSelectIssue = (v: string) => setIssue(v)
|
|
||||||
const goToDetails = () => {
|
|
||||||
if (issue === '__copyright__') {
|
|
||||||
Linking.openURL(DMCA_LINK)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setShowDetailsInput(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Text style={[pal.text, styles.title]}>
|
|
||||||
<Trans>Report {collectionName}</Trans>
|
|
||||||
</Text>
|
|
||||||
<Text style={[pal.textLight, styles.description]}>
|
|
||||||
<Trans>What is the issue with this {collectionName}?</Trans>
|
|
||||||
</Text>
|
|
||||||
<View style={{marginBottom: 10}}>
|
|
||||||
<ReportReasonOptions
|
|
||||||
atUri={atUri}
|
|
||||||
selectedIssue={issue}
|
|
||||||
onSelectIssue={onSelectIssue}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
{error ? <ErrorMessage message={error} /> : undefined}
|
|
||||||
{/* 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}
|
|
||||||
isProcessing={isProcessing}
|
|
||||||
/>
|
|
||||||
<TouchableOpacity
|
|
||||||
testID="addDetailsBtn"
|
|
||||||
style={styles.addDetailsBtn}
|
|
||||||
onPress={goToDetails}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={_(msg`Add details`)}
|
|
||||||
accessibilityHint="Add more details to your report">
|
|
||||||
<Text style={[s.f18, pal.link]}>
|
|
||||||
<Trans>Add details to report</Trans>
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</>
|
|
||||||
) : undefined}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
paddingHorizontal: 10,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
textAlign: 'center',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
fontSize: 24,
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
textAlign: 'center',
|
|
||||||
fontSize: 17,
|
|
||||||
paddingHorizontal: 22,
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
addDetailsBtn: {
|
|
||||||
padding: 14,
|
|
||||||
alignSelf: 'center',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
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,62 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import LinearGradient from 'react-native-linear-gradient'
|
|
||||||
import {
|
|
||||||
ActivityIndicator,
|
|
||||||
StyleSheet,
|
|
||||||
TouchableOpacity,
|
|
||||||
View,
|
|
||||||
} from 'react-native'
|
|
||||||
import {Text} from '../../util/text/Text'
|
|
||||||
import {s, gradients, colors} from 'lib/styles'
|
|
||||||
import {Trans, msg} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
|
|
||||||
export function SendReportButton({
|
|
||||||
onPress,
|
|
||||||
isProcessing,
|
|
||||||
}: {
|
|
||||||
onPress: () => void
|
|
||||||
isProcessing: boolean
|
|
||||||
}) {
|
|
||||||
const {_} = useLingui()
|
|
||||||
// loading state
|
|
||||||
// =
|
|
||||||
if (isProcessing) {
|
|
||||||
return (
|
|
||||||
<View style={[styles.btn, s.mt10]}>
|
|
||||||
<ActivityIndicator />
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
|
||||||
testID="sendReportBtn"
|
|
||||||
style={s.mt10}
|
|
||||||
onPress={onPress}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={_(msg`Report post`)}
|
|
||||||
accessibilityHint={`Reports post with reason and details`}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
|
||||||
start={{x: 0, y: 0}}
|
|
||||||
end={{x: 1, y: 1}}
|
|
||||||
style={[styles.btn]}>
|
|
||||||
<Text style={[s.white, s.bold, s.f18]}>
|
|
||||||
<Trans>Send Report</Trans>
|
|
||||||
</Text>
|
|
||||||
</LinearGradient>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
btn: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
width: '100%',
|
|
||||||
borderRadius: 32,
|
|
||||||
padding: 14,
|
|
||||||
backgroundColor: colors.gray1,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
// 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',
|
|
||||||
}
|
|
||||||
@@ -83,7 +83,7 @@ let PostDropdownBtn = ({
|
|||||||
const hiddenPosts = useHiddenPosts()
|
const hiddenPosts = useHiddenPosts()
|
||||||
const {hidePost} = useHiddenPostsApi()
|
const {hidePost} = useHiddenPostsApi()
|
||||||
const openLink = useOpenLink()
|
const openLink = useOpenLink()
|
||||||
const control = useReportDialogControl()
|
const reportDialogControl = useReportDialogControl()
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
|
const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
|
||||||
|
|
||||||
@@ -171,7 +171,7 @@ let PostDropdownBtn = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ReportDialog
|
<ReportDialog
|
||||||
control={control}
|
control={reportDialogControl}
|
||||||
params={{
|
params={{
|
||||||
type: 'post',
|
type: 'post',
|
||||||
uri: postUri,
|
uri: postUri,
|
||||||
@@ -307,13 +307,7 @@ let PostDropdownBtn = ({
|
|||||||
<Menu.Item
|
<Menu.Item
|
||||||
testID="postDropdownReportBtn"
|
testID="postDropdownReportBtn"
|
||||||
label={_(msg`Report post`)}
|
label={_(msg`Report post`)}
|
||||||
onPress={() => {
|
onPress={() => reportDialogControl.open()}>
|
||||||
openModal({
|
|
||||||
name: 'report',
|
|
||||||
uri: postUri,
|
|
||||||
cid: postCid,
|
|
||||||
})
|
|
||||||
}}>
|
|
||||||
<Menu.ItemText>{_(msg`Report post`)}</Menu.ItemText>
|
<Menu.ItemText>{_(msg`Report post`)}</Menu.ItemText>
|
||||||
<Menu.ItemIcon icon={Warning} position="right" />
|
<Menu.ItemIcon icon={Warning} position="right" />
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|||||||
@@ -183,12 +183,12 @@ function ProfileScreenLoaded({
|
|||||||
const sectionTitles = useMemo<string[]>(() => {
|
const sectionTitles = useMemo<string[]>(() => {
|
||||||
return [
|
return [
|
||||||
showFiltersTab ? _(msg`Labels`) : undefined,
|
showFiltersTab ? _(msg`Labels`) : undefined,
|
||||||
showListsTab ? _(msg`Lists`) : undefined,
|
|
||||||
showPostsTab ? _(msg`Posts`) : undefined,
|
showPostsTab ? _(msg`Posts`) : undefined,
|
||||||
showRepliesTab ? _(msg`Replies`) : undefined,
|
showRepliesTab ? _(msg`Replies`) : undefined,
|
||||||
showMediaTab ? _(msg`Media`) : undefined,
|
showMediaTab ? _(msg`Media`) : undefined,
|
||||||
showLikesTab ? _(msg`Likes`) : undefined,
|
showLikesTab ? _(msg`Likes`) : undefined,
|
||||||
showFeedsTab ? _(msg`Feeds`) : undefined,
|
showFeedsTab ? _(msg`Feeds`) : undefined,
|
||||||
|
showListsTab ? _(msg`Lists`) : undefined,
|
||||||
].filter(Boolean) as string[]
|
].filter(Boolean) as string[]
|
||||||
}, [
|
}, [
|
||||||
showPostsTab,
|
showPostsTab,
|
||||||
@@ -212,9 +212,6 @@ function ProfileScreenLoaded({
|
|||||||
if (showFiltersTab) {
|
if (showFiltersTab) {
|
||||||
filtersIndex = nextIndex++
|
filtersIndex = nextIndex++
|
||||||
}
|
}
|
||||||
if (showListsTab && profile.associated?.labeler) {
|
|
||||||
listsIndex = nextIndex++
|
|
||||||
}
|
|
||||||
if (showPostsTab) {
|
if (showPostsTab) {
|
||||||
postsIndex = nextIndex++
|
postsIndex = nextIndex++
|
||||||
}
|
}
|
||||||
@@ -230,7 +227,7 @@ function ProfileScreenLoaded({
|
|||||||
if (showFeedsTab) {
|
if (showFeedsTab) {
|
||||||
feedsIndex = nextIndex++
|
feedsIndex = nextIndex++
|
||||||
}
|
}
|
||||||
if (showListsTab && !profile.associated?.labeler) {
|
if (showListsTab) {
|
||||||
listsIndex = nextIndex++
|
listsIndex = nextIndex++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import {ComposeIcon2} from 'lib/icons'
|
|||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {Trans, msg} from '@lingui/macro'
|
import {Trans, msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
|
||||||
import {useFeedSourceInfoQuery, FeedSourceFeedInfo} from '#/state/queries/feed'
|
import {useFeedSourceInfoQuery, FeedSourceFeedInfo} from '#/state/queries/feed'
|
||||||
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
|
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
|
||||||
import {
|
import {
|
||||||
@@ -150,7 +150,7 @@ export function ProfileFeedScreenInner({
|
|||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {hasSession, currentAccount} = useSession()
|
const {hasSession, currentAccount} = useSession()
|
||||||
const {openModal} = useModalControls()
|
const reportDialogControl = useReportDialogControl()
|
||||||
const {openComposer} = useComposerControls()
|
const {openComposer} = useComposerControls()
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const feedSectionRef = React.useRef<SectionRef>(null)
|
const feedSectionRef = React.useRef<SectionRef>(null)
|
||||||
@@ -246,13 +246,8 @@ export function ProfileFeedScreenInner({
|
|||||||
}, [feedInfo, track])
|
}, [feedInfo, track])
|
||||||
|
|
||||||
const onPressReport = React.useCallback(() => {
|
const onPressReport = React.useCallback(() => {
|
||||||
if (!feedInfo) return
|
reportDialogControl.open()
|
||||||
openModal({
|
}, [reportDialogControl])
|
||||||
name: 'report',
|
|
||||||
uri: feedInfo.uri,
|
|
||||||
cid: feedInfo.cid,
|
|
||||||
})
|
|
||||||
}, [openModal, feedInfo])
|
|
||||||
|
|
||||||
const onCurrentPageSelected = React.useCallback(
|
const onCurrentPageSelected = React.useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
@@ -391,6 +386,14 @@ export function ProfileFeedScreenInner({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={s.hContentRegion}>
|
<View style={s.hContentRegion}>
|
||||||
|
<ReportDialog
|
||||||
|
control={reportDialogControl}
|
||||||
|
params={{
|
||||||
|
type: 'feedgen',
|
||||||
|
uri: feedInfo.uri,
|
||||||
|
cid: feedInfo.cid,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<PagerWithHeader
|
<PagerWithHeader
|
||||||
items={SECTION_TITLES}
|
items={SECTION_TITLES}
|
||||||
isHeaderReady={true}
|
isHeaderReady={true}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import {Trans, msg} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useSetMinimalShellMode} from '#/state/shell'
|
import {useSetMinimalShellMode} from '#/state/shell'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
|
||||||
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
|
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
|
||||||
import {
|
import {
|
||||||
useListQuery,
|
useListQuery,
|
||||||
@@ -235,6 +236,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
|
|||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const {openModal, closeModal} = useModalControls()
|
const {openModal, closeModal} = useModalControls()
|
||||||
|
const reportDialogControl = useReportDialogControl()
|
||||||
const listMuteMutation = useListMuteMutation()
|
const listMuteMutation = useListMuteMutation()
|
||||||
const listBlockMutation = useListBlockMutation()
|
const listBlockMutation = useListBlockMutation()
|
||||||
const listDeleteMutation = useListDeleteMutation()
|
const listDeleteMutation = useListDeleteMutation()
|
||||||
@@ -398,12 +400,8 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
|
|||||||
])
|
])
|
||||||
|
|
||||||
const onPressReport = useCallback(() => {
|
const onPressReport = useCallback(() => {
|
||||||
openModal({
|
reportDialogControl.open()
|
||||||
name: 'report',
|
}, [reportDialogControl])
|
||||||
uri: list.uri,
|
|
||||||
cid: list.cid,
|
|
||||||
})
|
|
||||||
}, [openModal, list])
|
|
||||||
|
|
||||||
const onPressShare = useCallback(() => {
|
const onPressShare = useCallback(() => {
|
||||||
const url = toShareUrl(`/profile/${list.creator.did}/lists/${rkey}`)
|
const url = toShareUrl(`/profile/${list.creator.did}/lists/${rkey}`)
|
||||||
@@ -574,6 +572,14 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
|
|||||||
isOwner={list.creator.did === currentAccount?.did}
|
isOwner={list.creator.did === currentAccount?.did}
|
||||||
creator={list.creator}
|
creator={list.creator}
|
||||||
avatarType="list">
|
avatarType="list">
|
||||||
|
<ReportDialog
|
||||||
|
control={reportDialogControl}
|
||||||
|
params={{
|
||||||
|
type: 'list',
|
||||||
|
uri: list.uri,
|
||||||
|
cid: list.cid,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{isCurateList || isPinned ? (
|
{isCurateList || isPinned ? (
|
||||||
<Button
|
<Button
|
||||||
testID={isPinned ? 'unpinBtn' : 'pinBtn'}
|
testID={isPinned ? 'unpinBtn' : 'pinBtn'}
|
||||||
|
|||||||
Reference in New Issue
Block a user