Rework list of labels on own content

This commit is contained in:
Paul Frazee
2024-02-15 16:20:20 -08:00
parent 6d9e1f4dd9
commit b8d62a8af8
14 changed files with 207 additions and 265 deletions
@@ -0,0 +1,92 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {ComAtprotoLabelDefs} from '@atproto/api'
import {atoms as a, useBreakpoints} from '#/alf'
import {Text} from '#/components/Typography'
import * as Dialog from '#/components/Dialog'
import {GlobalDialogProps} from '#/components/dialogs'
import {Button} from '#/components/Button'
type Subject =
| {
uri: string
cid: string
}
| {
did: string
}
export interface LabelsOnMeDialogProps {
subject: Subject
labels: ComAtprotoLabelDefs.Label[]
}
export function LabelsOnMeDialog({
params,
cleanup,
}: GlobalDialogProps<LabelsOnMeDialogProps>) {
const {_} = useLingui()
const control = Dialog.useDialogControl()
const {gtMobile} = useBreakpoints()
const {subject, labels} = params
const isAccount = 'did' in subject
// REQUIRED CLEANUP
const onClose = React.useCallback(() => cleanup(), [cleanup])
// TODO
// const submit = async () => {
// try {
// const $type = !isAccountReport
// ? 'com.atproto.repo.strongRef'
// : 'com.atproto.admin.defs#repoRef'
// await getAgent().createModerationReport({
// reasonType: ComAtprotoModerationDefs.REASONAPPEAL,
// subject: {
// $type,
// ...props,
// },
// reason: details,
// })
// Toast.show(_(msg`We'll look into your appeal promptly.`))
// } finally {
// closeModal()
// }
// }
return (
<Dialog.Outer defaultOpen control={control} onClose={onClose}>
<Dialog.Handle />
<Dialog.ScrollableInner
accessibilityDescribedBy="dialog-description"
accessibilityLabelledBy="dialog-title">
<Text nativeID="dialog-title" style={[a.text_2xl, a.font_bold]}>
<Trans>Labels on my {isAccount ? 'account' : 'content'}</Trans>
</Text>
<Text nativeID="dialog-description" style={[a.text_sm]}>
<Trans>
You may appeal these labels if you feel they were placed in error.
</Trans>
</Text>
{labels.map(label => (
<Text key={`${label.src}-${label.val}`}>{label.val} TODO</Text>
))}
<View style={gtMobile && [a.flex_row, a.justify_end]}>
<Button
testID="doneBtn"
variant="outline"
color="primary"
size="small"
onPress={() => control.close()}
label={_(msg`Done`)}>
{_(msg`Done`)}
</Button>
</View>
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
-11
View File
@@ -36,16 +36,6 @@ export type ReportModal = {
| {did: string}
)
export type AppealLabelModal = {
name: 'appeal-label'
} & (
| {
uri: string
cid: string
}
| {did: string}
)
export interface CreateOrEditListModal {
name: 'create-or-edit-list'
purpose?: string
@@ -198,7 +188,6 @@ export type Modal =
// Moderation
| ReportModal
| AppealLabelModal
// Lists
| CreateOrEditListModal
-139
View File
@@ -1,139 +0,0 @@
import React, {useState} from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {ComAtprotoModerationDefs} from '@atproto/api'
import {ScrollView, TextInput} from './util'
import {Text} from '../util/text/Text'
import {s, colors} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {CharProgress} from '../composer/char-progress/CharProgress'
import {getAgent} from '#/state/session'
import * as Toast from '../util/Toast'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
export const snapPoints = ['40%']
type ReportComponentProps =
| {
uri: string
cid: string
}
| {
did: string
}
export function Component(props: ReportComponentProps) {
const pal = usePalette('default')
const [details, setDetails] = useState<string>('')
const {_} = useLingui()
const {closeModal} = useModalControls()
const {isMobile} = useWebMediaQueries()
const isAccountReport = 'did' in props
const submit = async () => {
try {
const $type = !isAccountReport
? 'com.atproto.repo.strongRef'
: 'com.atproto.admin.defs#repoRef'
await getAgent().createModerationReport({
reasonType: ComAtprotoModerationDefs.REASONAPPEAL,
subject: {
$type,
...props,
},
reason: details,
})
Toast.show(_(msg`We'll look into your appeal promptly.`))
} finally {
closeModal()
}
}
return (
<View
style={[
pal.view,
s.flex1,
isMobile ? {paddingHorizontal: 12} : undefined,
]}
testID="appealLabelModal">
<Text
type="2xl-bold"
style={[pal.text, s.textCenter, {paddingBottom: 8}]}>
<Trans>Appeal Content Warning</Trans>
</Text>
<ScrollView>
<View style={[pal.btn, styles.detailsInputContainer]}>
<TextInput
accessibilityLabel={_(msg`Text input field`)}
accessibilityHint={_(
msg`Please tell us why you think this content warning was incorrectly applied!`,
)}
placeholder={_(
msg`Please tell us why you think this content warning was incorrectly applied!`,
)}
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>
<TouchableOpacity
testID="confirmBtn"
onPress={submit}
style={styles.btn}
accessibilityRole="button"
accessibilityLabel={_(msg`Confirm`)}
accessibilityHint="">
<Text style={[s.white, s.bold, s.f18]}>
<Trans>Submit</Trans>
</Text>
</TouchableOpacity>
</ScrollView>
</View>
)
}
const styles = StyleSheet.create({
detailsInputContainer: {
borderRadius: 8,
marginBottom: 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,
},
btn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 32,
padding: 14,
backgroundColor: colors.blue3,
},
})
-4
View File
@@ -17,7 +17,6 @@ import * as ListAddUserModal from './ListAddRemoveUsers'
import * as AltImageModal from './AltImage'
import * as EditImageModal from './AltImage'
import * as ReportModal from './report/Modal'
import * as AppealLabelModal from './AppealLabel'
import * as DeleteAccountModal from './DeleteAccount'
import * as ChangeHandleModal from './ChangeHandle'
import * as WaitlistModal from './Waitlist'
@@ -75,9 +74,6 @@ export function ModalsContainer() {
} else if (activeModal?.name === 'report') {
snapPoints = ReportModal.snapPoints
element = <ReportModal.Component {...activeModal} />
} else if (activeModal?.name === 'appeal-label') {
snapPoints = AppealLabelModal.snapPoints
element = <AppealLabelModal.Component {...activeModal} />
} else if (activeModal?.name === 'create-or-edit-list') {
snapPoints = CreateOrEditListModal.snapPoints
element = <CreateOrEditListModal.Component {...activeModal} />
-3
View File
@@ -10,7 +10,6 @@ import type {Modal as ModalIface} from '#/state/modals'
import * as ConfirmModal from './Confirm'
import * as EditProfileModal from './EditProfile'
import * as ReportModal from './report/Modal'
import * as AppealLabelModal from './AppealLabel'
import * as CreateOrEditListModal from './CreateOrEditList'
import * as UserAddRemoveLists from './UserAddRemoveLists'
import * as ListAddUserModal from './ListAddRemoveUsers'
@@ -84,8 +83,6 @@ function Modal({modal}: {modal: ModalIface}) {
element = <EditProfileModal.Component {...modal} />
} else if (modal.name === 'report') {
element = <ReportModal.Component {...modal} />
} else if (modal.name === 'appeal-label') {
element = <AppealLabelModal.Component {...modal} />
} else if (modal.name === 'create-or-edit-list') {
element = <CreateOrEditListModal.Component {...modal} />
} else if (modal.name === 'user-add-remove-lists') {
@@ -26,6 +26,7 @@ import {PostCtrls} from '../util/post-ctrls/PostCtrls'
import {PostHider} from '../util/moderation/PostHider'
import {ContentHider} from '../util/moderation/ContentHider'
import {PostAlerts} from '../util/moderation/PostAlerts'
import {LabelsOnMyPost} from '../util/moderation/LabelsOnMe'
import {PostSandboxWarning} from '../util/PostSandboxWarning'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {usePalette} from 'lib/hooks/usePalette'
@@ -286,6 +287,7 @@ let PostThreadItemLoaded = ({
)}
</View>
<View style={[s.pl10, s.pr10, s.pb10]}>
<LabelsOnMyPost post={post} />
<ContentHider
modui={moderation.ui('contentView')}
ignoreMute
@@ -476,6 +478,7 @@ let PostThreadItemLoaded = ({
displayNameStyle={isThreadedChild && s.ml2}
style={isThreadedChild && s.mb2}
/>
<LabelsOnMyPost post={post} />
<PostAlerts
modui={moderation.ui('contentList')}
style={styles.alert}
+2
View File
@@ -16,6 +16,7 @@ import {PostEmbeds} from '../util/post-embeds'
import {PostCtrls} from '../util/post-ctrls/PostCtrls'
import {ContentHider} from '../util/moderation/ContentHider'
import {PostAlerts} from '../util/moderation/PostAlerts'
import {LabelsOnMyPost} from '../util/moderation/LabelsOnMe'
import {Text} from '../util/text/Text'
import {RichText} from '../util/text/RichText'
import {PreviewableUserAvatar} from '../util/UserAvatar'
@@ -175,6 +176,7 @@ function PostInner({
</Text>
</View>
)}
<LabelsOnMyPost post={post} />
<ContentHider
modui={moderation.ui('contentView')}
style={styles.contentHider}
+2 -5
View File
@@ -20,6 +20,7 @@ import {PostCtrls} from '../util/post-ctrls/PostCtrls'
import {PostEmbeds} from '../util/post-embeds'
import {ContentHider} from '../util/moderation/ContentHider'
import {PostAlerts} from '../util/moderation/PostAlerts'
import {LabelsOnMyPost} from '../util/moderation/LabelsOnMe'
import {RichText} from '../util/text/RichText'
import {PostSandboxWarning} from '../util/PostSandboxWarning'
import {PreviewableUserAvatar} from '../util/UserAvatar'
@@ -298,6 +299,7 @@ let FeedItemInner = ({
</Text>
</View>
)}
<LabelsOnMyPost post={post} />
<PostContent
moderation={moderation}
richText={richText}
@@ -309,9 +311,6 @@ let FeedItemInner = ({
record={record}
richText={richText}
onPressReply={onPressReply}
showAppealLabelItem={
post.author.did === currentAccount?.did && isModeratedPost
}
/>
</View>
</View>
@@ -324,12 +323,10 @@ let PostContent = ({
moderation,
richText,
postEmbed,
postAuthor,
}: {
moderation: ModerationDecision
richText: RichTextAPI
postEmbed: AppBskyFeedDefs.PostView['embed']
postAuthor: AppBskyFeedDefs.PostView['author']
}): React.ReactNode => {
const pal = usePalette('default')
const {_} = useLingui()
+2 -2
View File
@@ -54,7 +54,7 @@ import {logger} from '#/logger'
import {useSession} from '#/state/session'
import {Shadow} from '#/state/cache/types'
import {useRequireAuth} from '#/state/session'
import {LabelInfo} from '../util/moderation/LabelInfo'
import {LabelsOnMe} from '../util/moderation/LabelsOnMe'
import {useProfileShadow} from 'state/cache/profile-shadow'
import * as ModerationServiceCard from '#/components/ModerationServiceCard'
import {getModerationServiceTitle} from '#/lib/moderation'
@@ -635,7 +635,7 @@ let ProfileHeader = ({
)}
<ProfileHeaderAlerts moderation={moderation} />
{isMe && (
<LabelInfo details={{did: profile.did}} labels={profile.labels} />
<LabelsOnMe details={{did: profile.did}} labels={profile.labels} />
)}
<ModerationServiceCard.Loader
@@ -43,7 +43,6 @@ let PostDropdownBtn = ({
record,
richText,
style,
showAppealLabelItem,
}: {
testID: string
postAuthor: AppBskyActorDefs.ProfileViewBasic
@@ -52,7 +51,6 @@ let PostDropdownBtn = ({
record: AppBskyFeedPost.Record
richText: RichTextAPI
style?: StyleProp<ViewStyle>
showAppealLabelItem?: boolean
}): React.ReactNode => {
const {hasSession, currentAccount} = useSession()
const theme = useTheme()
@@ -255,23 +253,6 @@ let PostDropdownBtn = ({
web: ['far', 'trash-can'],
},
},
showAppealLabelItem && {
label: 'separator',
},
showAppealLabelItem && {
label: _(msg`Appeal content warning`),
onPress() {
openModal({name: 'appeal-label', uri: postUri, cid: postCid})
},
testID: 'postDropdownAppealBtn',
icon: {
ios: {
name: 'exclamationmark.triangle',
},
android: 'ic_menu_report_image',
web: 'circle-exclamation',
},
},
].filter(Boolean) as NativeDropdownItem[]
return (
@@ -1,61 +0,0 @@
import React from 'react'
import {Pressable, StyleProp, View, ViewStyle} from 'react-native'
import {ComAtprotoLabelDefs} from '@atproto/api'
import {Text} from '../text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export function LabelInfo({
details,
labels,
style,
}: {
details: {did: string} | {uri: string; cid: string}
labels: ComAtprotoLabelDefs.Label[] | undefined
style?: StyleProp<ViewStyle>
}) {
const pal = usePalette('default')
const {_} = useLingui()
const {openModal} = useModalControls()
if (!labels) {
return null
}
labels = labels.filter(l => !l.val.startsWith('!'))
if (!labels.length) {
return null
}
return (
<View
style={[
pal.viewLight,
{
flexDirection: 'row',
flexWrap: 'wrap',
paddingHorizontal: 12,
paddingVertical: 10,
borderRadius: 8,
},
style,
]}>
<Text type="sm" style={pal.text}>
<Trans>
A content warning has been applied to this{' '}
{'did' in details ? 'account' : 'post'}.
</Trans>{' '}
</Text>
<Pressable
accessibilityRole="button"
accessibilityLabel={_(msg`Appeal this decision`)}
accessibilityHint=""
onPress={() => openModal({name: 'appeal-label', ...details})}>
<Text type="sm" style={pal.link}>
<Trans>Appeal this decision.</Trans>
</Text>
</Pressable>
</View>
)
}
@@ -0,0 +1,86 @@
import React from 'react'
import {StyleProp, View, ViewStyle} from 'react-native'
import {AppBskyFeedDefs, ComAtprotoLabelDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useSession} from '#/state/session'
import {atoms as a} from '#/alf'
import {Button, ButtonText, ButtonIcon, ButtonSize} from '#/components/Button'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {useOpenGlobalDialog} from '#/components/dialogs'
import {LabelsOnMeDialog} from '#/components/dialogs/LabelsOnMe'
export function LabelsOnMe({
details,
labels,
size,
style,
}: {
details: {did: string} | {uri: string; cid: string}
labels: ComAtprotoLabelDefs.Label[] | undefined
size?: ButtonSize
style?: StyleProp<ViewStyle>
}) {
const {_} = useLingui()
const {currentAccount} = useSession()
const isAccount = 'did' in details
const openDialog = useOpenGlobalDialog()
if (!labels || !currentAccount) {
return null
}
labels = labels.filter(
l => !l.val.startsWith('!') && l.src !== currentAccount.did,
)
if (!labels.length) {
return null
}
return (
<View style={[a.flex_row, style]}>
<Button
variant="solid"
color="secondary"
size={size || 'small'}
label={_(msg`View information about these labels`)}
onPress={() => {
openDialog(LabelsOnMeDialog, {
subject: details,
labels: labels!,
})
}}>
<ButtonIcon position="left" icon={CircleInfo} />
<ButtonText>
{labels.length}{' '}
{labels.length === 1 ? (
<Trans>
label has been placed on this {isAccount ? 'account' : 'content'}
</Trans>
) : (
<Trans>
labels have been placed on this{' '}
{isAccount ? 'account' : 'content'}
</Trans>
)}
</ButtonText>
</Button>
</View>
)
}
export function LabelsOnMyPost({
post,
style,
}: {
post: AppBskyFeedDefs.PostView
style?: StyleProp<ViewStyle>
}) {
const {currentAccount} = useSession()
if (post.author.did !== currentAccount?.did) {
return null
}
return (
<LabelsOnMe details={post} labels={post.labels} size="tiny" style={style} />
)
}
@@ -41,7 +41,6 @@ let PostCtrls = ({
post,
record,
richText,
showAppealLabelItem,
style,
onPressReply,
}: {
@@ -49,7 +48,6 @@ let PostCtrls = ({
post: Shadow<AppBskyFeedDefs.PostView>
record: AppBskyFeedPost.Record
richText: RichTextAPI
showAppealLabelItem?: boolean
style?: StyleProp<ViewStyle>
onPressReply: () => void
}): React.ReactNode => {
@@ -229,7 +227,6 @@ let PostCtrls = ({
postUri={post.uri}
record={record}
richText={richText}
showAppealLabelItem={showAppealLabelItem}
style={styles.btnPad}
/>
</View>
+20 -18
View File
@@ -17,6 +17,7 @@ import {
RichText,
} from '@atproto/api'
import {moderationOptsOverrideContext} from '#/state/queries/preferences'
import {useSession} from '#/state/session'
import {atoms as a, useTheme} from '#/alf'
import {CenteredView, ScrollView} from '#/view/com/util/Views'
@@ -41,7 +42,7 @@ const LABEL_VALUES: (keyof typeof LABELS)[] = Object.keys(
) as (keyof typeof LABELS)[]
const MOCK_MOD_OPTS = {
userDid: 'did:web:alice.test',
userDid: '',
adultContentEnabled: true,
labelGroups: {},
mods: [
@@ -63,6 +64,7 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
const [target, setTarget] = React.useState<string[]>(['account'])
const [visibility, setVisiblity] = React.useState<string[]>(['hide'])
const labelStrings = useLabelStrings()
const {currentAccount} = useSession()
const isTargetMe =
scenario[0] === 'label' && scenarioSwitches.includes('targetMe')
@@ -73,6 +75,9 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
const isLoggedOut =
scenario[0] === 'label' && scenarioSwitches.includes('loggedOut')
const did =
isTargetMe && currentAccount ? currentAccount.did : 'did:web:bob.test'
const profile = React.useMemo(() => {
const mockedProfile = mock.profileViewBasic({
handle: `bob.test`,
@@ -82,17 +87,17 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
scenario[0] === 'label' && target[0] === 'account'
? [
mock.label({
src: isSelfLabel ? 'did:web:bob.test' : undefined,
src: isSelfLabel ? did : undefined,
val: label[0],
uri: `at://did:web:bob.test/`,
uri: `at://${did}/`,
}),
]
: scenario[0] === 'label' && target[0] === 'profile'
? [
mock.label({
src: isSelfLabel ? 'did:web:bob.test' : undefined,
src: isSelfLabel ? did : undefined,
val: label[0],
uri: `at://did:web:bob.test/app.bsky.actor.profile/self`,
uri: `at://${did}/app.bsky.actor.profile/self`,
}),
]
: undefined,
@@ -102,16 +107,17 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
blockedBy: undefined,
blocking:
scenario[0] === 'block'
? 'did://did:web:alice/app.bsky.actor.block/fake'
? `at://did:web:alice.test/app.bsky.actor.block/fake`
: undefined,
blockingByList: undefined,
}),
})
mockedProfile.did = did
mockedProfile.avatar = 'https://bsky.social/about/images/favicon-32x32.png'
mockedProfile.banner =
'https://bsky.social/about/images/social-card-default-gradient.png'
return mockedProfile
}, [scenario, target, label, isSelfLabel])
}, [scenario, target, label, isSelfLabel, did])
const post = React.useMemo(() => {
return mock.postView({
@@ -123,9 +129,9 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
scenario[0] === 'label' && target[0] === 'post'
? [
mock.label({
src: isSelfLabel ? 'did:web:bob.test' : undefined,
src: isSelfLabel ? did : undefined,
val: label[0],
uri: `at://bob.test/app.bsky.feed.post/fake`,
uri: `at:/${did}/app.bsky.feed.post/fake`,
}),
]
: undefined,
@@ -139,9 +145,9 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
scenario[0] === 'label' && target[0] === 'embed'
? [
mock.label({
src: isSelfLabel ? 'did:web:bob.test' : undefined,
src: isSelfLabel ? did : undefined,
val: label[0],
uri: `at://bob.test/app.bsky.feed.post/fake`,
uri: `at://${did}/app.bsky.feed.post/fake`,
}),
]
: undefined,
@@ -160,23 +166,19 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
],
},
})
}, [scenario, label, target, profile, isSelfLabel])
}, [scenario, label, target, profile, isSelfLabel, did])
const modOpts = React.useMemo(() => {
return {
...MOCK_MOD_OPTS,
userDid: isLoggedOut
? ''
: isTargetMe
? 'did:web:bob.test'
: 'did:web:alice.test',
userDid: isLoggedOut ? '' : isTargetMe ? did : 'did:web:alice.test',
adultContentEnabled: !noAdult,
labelGroups: {
[LABELS[label[0] as keyof typeof LABELS].groupId]:
visibility[0] as LabelPreference,
},
}
}, [label, visibility, noAdult, isLoggedOut, isTargetMe])
}, [label, visibility, noAdult, isLoggedOut, isTargetMe, did])
const profileModeration = React.useMemo(() => {
return moderateProfile(profile, modOpts)