Refactor/rewrite the entire moderation-application system

This commit is contained in:
Paul Frazee
2024-02-14 21:48:27 -08:00
parent cc635ae949
commit b009a34a7a
43 changed files with 1094 additions and 1062 deletions
@@ -1,6 +1,6 @@
import React from 'react'
import {View, StyleSheet, ActivityIndicator} from 'react-native'
import {ProfileModeration, AppBskyActorDefs} from '@atproto/api'
import {ModerationDecision, AppBskyActorDefs} from '@atproto/api'
import {Button} from '#/view/com/util/forms/Button'
import {usePalette} from 'lib/hooks/usePalette'
import {sanitizeDisplayName} from 'lib/strings/display-names'
@@ -18,7 +18,7 @@ import {logger} from '#/logger'
type Props = {
profile: AppBskyActorDefs.ProfileViewBasic
moderation: ProfileModeration
moderation: ModerationDecision
onFollowStateChange: (props: {
did: string
following: boolean
@@ -62,7 +62,7 @@ export function ProfileCard({
moderation,
}: {
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
moderation: ProfileModeration
moderation: ModerationDecision
onFollowStateChange: (props: {
did: string
following: boolean
@@ -110,7 +110,7 @@ export function ProfileCard({
<UserAvatar
size={40}
avatar={profile.avatar}
moderation={moderation.avatar}
moderation={moderation.ui('avatar')}
/>
</View>
<View style={styles.layoutContent}>
@@ -121,7 +121,7 @@ export function ProfileCard({
lineHeight={1.2}>
{sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
moderation.profile,
moderation.ui('displayName'),
)}
</Text>
<Text type="xl" style={[pal.textLight]} numberOfLines={1}>
+1 -1
View File
@@ -39,7 +39,7 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useExternalLinkFetch} from './useExternalLinkFetch'
import {isWeb, isNative, isAndroid, isIOS} from 'platform/detection'
import QuoteEmbed from '../util/post-embeds/QuoteEmbed'
import {QuoteEmbed} from '../util/post-embeds/QuoteEmbed'
import {GalleryModel} from 'state/models/media/gallery'
import {Gallery} from './photos/Gallery'
import {MAX_GRAPHEME_LENGTH} from 'lib/constants'
+3 -3
View File
@@ -15,7 +15,7 @@ import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {UserAvatar} from 'view/com/util/UserAvatar'
import {Text} from 'view/com/util/text/Text'
import QuoteEmbed from 'view/com/util/post-embeds/QuoteEmbed'
import {QuoteEmbed} from 'view/com/util/post-embeds/QuoteEmbed'
export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
const pal = usePalette('default')
@@ -86,7 +86,7 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
<UserAvatar
avatar={replyTo.author.avatar}
size={50}
moderation={replyTo.moderation?.avatar}
moderation={replyTo.moderation?.ui('avatar')}
/>
<View style={styles.replyToPost}>
<Text type="xl-medium" style={[pal.text]}>
@@ -103,7 +103,7 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
{replyTo.text}
</Text>
</View>
{images && !replyTo.moderation?.embed.blur && (
{images && !replyTo.moderation?.ui('contentMedia').blur && (
<ComposerReplyToImages images={images} showFull={showFull} />
)}
</View>
-4
View File
@@ -26,7 +26,6 @@ import * as AddAppPassword from './AddAppPasswords'
import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
import * as ModerationDetailsModal from './ModerationDetails'
import * as BirthDateSettingsModal from './BirthDateSettings'
import * as VerifyEmailModal from './VerifyEmail'
import * as ChangeEmailModal from './ChangeEmail'
@@ -127,9 +126,6 @@ export function ModalsContainer() {
} else if (activeModal?.name === 'post-languages-settings') {
snapPoints = PostLanguagesSettingsModal.snapPoints
element = <PostLanguagesSettingsModal.Component />
} else if (activeModal?.name === 'moderation-details') {
snapPoints = ModerationDetailsModal.snapPoints
element = <ModerationDetailsModal.Component {...activeModal} />
} else if (activeModal?.name === 'birth-date-settings') {
snapPoints = BirthDateSettingsModal.snapPoints
element = <BirthDateSettingsModal.Component />
-3
View File
@@ -28,7 +28,6 @@ import * as AddAppPassword from './AddAppPasswords'
import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
import * as ModerationDetailsModal from './ModerationDetails'
import * as BirthDateSettingsModal from './BirthDateSettings'
import * as VerifyEmailModal from './VerifyEmail'
import * as ChangeEmailModal from './ChangeEmail'
@@ -121,8 +120,6 @@ function Modal({modal}: {modal: ModalIface}) {
element = <AltTextImageModal.Component {...modal} />
} else if (modal.name === 'edit-image') {
element = <EditImageModal.Component {...modal} />
} else if (modal.name === 'moderation-details') {
element = <ModerationDetailsModal.Component {...modal} />
} else if (modal.name === 'birth-date-settings') {
element = <BirthDateSettingsModal.Component />
} else if (modal.name === 'verify-email') {
-154
View File
@@ -1,154 +0,0 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {ModerationUI} from '@atproto/api'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {s} from 'lib/styles'
import {Text} from '../util/text/Text'
import {TextLink} from '../util/Link'
import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection'
import {listUriToHref} from 'lib/strings/url-helpers'
import {Button} from '../util/forms/Button'
import {useModalControls} from '#/state/modals'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {useLabelStrings} from '#/lib/moderation/useLabelStrings'
export const snapPoints = [300]
export function Component({
context,
moderation,
}: {
context: 'account' | 'content'
moderation: ModerationUI
}) {
const {closeModal} = useModalControls()
const {isMobile} = useWebMediaQueries()
const pal = usePalette('default')
const {_} = useLingui()
const labelStrings = useLabelStrings()
let name
let description
if (!moderation.cause) {
name = _(msg`Content Warning`)
description = _(
msg`Moderator has chosen to set a general warning on the content.`,
)
} else if (moderation.cause.type === 'blocking') {
if (moderation.cause.source.type === 'list') {
const list = moderation.cause.source.list
name = _(msg`User Blocked by List`)
description = (
<Trans>
This user is included in the{' '}
<TextLink
type="2xl"
href={listUriToHref(list.uri)}
text={list.name}
style={pal.link}
/>{' '}
list which you have blocked.
</Trans>
)
} else {
name = _(msg`User Blocked`)
description = _(
msg`You have blocked this user. You cannot view their content.`,
)
}
} else if (moderation.cause.type === 'blocked-by') {
name = _(msg`User Blocks You`)
description = _(
msg`This user has blocked you. You cannot view their content.`,
)
} else if (moderation.cause.type === 'block-other') {
name = _(msg`Content Not Available`)
description = _(
msg`This content is not available because one of the users involved has blocked the other.`,
)
} else if (moderation.cause.type === 'muted') {
if (moderation.cause.source.type === 'list') {
const list = moderation.cause.source.list
name = _(msg`Account Muted by List`)
description = (
<Trans>
This user is included in the{' '}
<TextLink
type="2xl"
href={listUriToHref(list.uri)}
text={list.name}
style={pal.link}
/>{' '}
list which you have muted.
</Trans>
)
} else {
name = _(msg`Account Muted`)
description = _(msg`You have muted this user.`)
}
} else if (moderation.cause.type === 'label') {
if (moderation.cause.labelDef.id in labelStrings) {
name = labelStrings[moderation.cause.labelDef.id][context].name
description =
labelStrings[moderation.cause.labelDef.id][context].description
} else {
name = moderation.cause.labelDef.id
description = _(msg`Labeled ${moderation.cause.labelDef.id}`)
}
} else {
// should never happen
name = ''
description = ''
}
return (
<View
testID="moderationDetailsModal"
style={[
styles.container,
{
paddingHorizontal: isMobile ? 14 : 0,
},
pal.view,
]}>
<Text type="title-xl" style={[pal.text, styles.title]}>
{name}
</Text>
<Text type="2xl" style={[pal.text, styles.description]}>
{description}
</Text>
<View style={s.flex1} />
<Button
type="primary"
style={styles.btn}
onPress={() => {
closeModal()
}}>
<Text type="button-lg" style={[pal.textLight, s.textCenter, s.white]}>
Okay
</Text>
</Button>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
title: {
textAlign: 'center',
fontWeight: 'bold',
marginBottom: 12,
},
description: {
textAlign: 'center',
},
btn: {
paddingVertical: 14,
marginTop: isWeb ? 40 : 0,
marginBottom: isWeb ? 0 : 40,
},
})
+5 -5
View File
@@ -11,7 +11,7 @@ import {
AppBskyFeedDefs,
AppBskyFeedPost,
ModerationOpts,
ProfileModeration,
ModerationDecision,
moderateProfile,
AppBskyEmbedRecordWithMedia,
} from '@atproto/api'
@@ -54,7 +54,7 @@ interface Author {
handle: string
displayName?: string
avatar?: string
moderation: ProfileModeration
moderation: ModerationDecision
}
let FeedItem = ({
@@ -336,7 +336,7 @@ function CondensedAuthorsList({
did={authors[0].did}
handle={authors[0].handle}
avatar={authors[0].avatar}
moderation={authors[0].moderation.avatar}
moderation={authors[0].moderation.ui('avatar')}
/>
</View>
)
@@ -354,7 +354,7 @@ function CondensedAuthorsList({
<UserAvatar
size={35}
avatar={author.avatar}
moderation={author.moderation.avatar}
moderation={author.moderation.ui('avatar')}
/>
</View>
))}
@@ -412,7 +412,7 @@ function ExpandedAuthorsList({
<UserAvatar
size={35}
avatar={author.avatar}
moderation={author.moderation.avatar}
moderation={author.moderation.ui('avatar')}
/>
</View>
<View style={s.flex1}>
+6 -5
View File
@@ -90,11 +90,12 @@ export function PostThread({
? moderatePost(rootPost, moderationOpts)
: undefined
const cause = mod?.content.cause
return cause
? cause.type === 'label' && cause.labelDef.id === '!no-unauthenticated'
: false
return !!mod
?.ui('contentList')
.blurs.find(
cause =>
cause.type === 'label' && cause.labelDef.id === '!no-unauthenticated',
)
}, [rootPost, moderationOpts])
useSetTitle(
+11 -34
View File
@@ -5,7 +5,7 @@ import {
AppBskyFeedDefs,
AppBskyFeedPost,
RichText as RichTextAPI,
PostModeration,
ModerationDecision,
} from '@atproto/api'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
@@ -19,7 +19,6 @@ import {niceDate} from 'lib/strings/time'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {countLines, pluralize} from 'lib/strings/helpers'
import {isEmbedByEmbedder} from 'lib/embeds'
import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers'
import {PostMeta} from '../util/PostMeta'
import {PostEmbeds} from '../util/post-embeds'
@@ -144,7 +143,7 @@ let PostThreadItemLoaded = ({
post: Shadow<AppBskyFeedDefs.PostView>
record: AppBskyFeedPost.Record
richText: RichTextAPI
moderation: PostModeration
moderation: ModerationDecision
treeView: boolean
depth: number
prevPost: ThreadPost | undefined
@@ -256,7 +255,7 @@ let PostThreadItemLoaded = ({
did={post.author.did}
handle={post.author.handle}
avatar={post.author.avatar}
moderation={moderation.avatar}
moderation={moderation.ui('avatar')}
/>
</View>
<View style={styles.layoutContent}>
@@ -313,12 +312,12 @@ let PostThreadItemLoaded = ({
</View>
<View style={[s.pl10, s.pr10, s.pb10]}>
<ContentHider
moderation={moderation.content}
modui={moderation.ui('contentView')}
ignoreMute
style={styles.contentHider}
childContainerStyle={styles.contentHiderChild}>
<PostAlerts
moderation={moderation.content}
modui={moderation.ui('contentView')}
includeMute
style={styles.alert}
/>
@@ -338,18 +337,7 @@ let PostThreadItemLoaded = ({
</View>
) : undefined}
{post.embed && (
<ContentHider
moderation={moderation.embed}
moderationDecisions={moderation.decisions}
ignoreMute={isEmbedByEmbedder(post.embed, post.author.did)}
ignoreQuoteDecisions
style={s.mb10}>
<PostEmbeds
embed={post.embed}
moderation={moderation.embed}
moderationDecisions={moderation.decisions}
/>
</ContentHider>
<PostEmbeds embed={post.embed} moderation={moderation} />
)}
</ContentHider>
<ExpandedPostDetails
@@ -431,7 +419,7 @@ let PostThreadItemLoaded = ({
testID={`postThreadItem-by-${post.author.handle}`}
href={postHref}
style={[pal.view]}
moderation={moderation.content}
modui={moderation.ui('contentList')}
iconSize={isThreadedChild ? 26 : 38}
iconStyles={
isThreadedChild
@@ -482,7 +470,7 @@ let PostThreadItemLoaded = ({
did={post.author.did}
handle={post.author.handle}
avatar={post.author.avatar}
moderation={moderation.avatar}
moderation={moderation.ui('avatar')}
/>
{showChildReplyLine && (
@@ -507,14 +495,14 @@ let PostThreadItemLoaded = ({
timestamp={post.indexedAt}
postHref={postHref}
showAvatar={isThreadedChild}
avatarModeration={moderation.avatar}
avatarModeration={moderation.ui('avatar')}
avatarSize={28}
displayNameType="md-bold"
displayNameStyle={isThreadedChild && s.ml2}
style={isThreadedChild && s.mb2}
/>
<PostAlerts
moderation={moderation.content}
modui={moderation.ui('contentList')}
style={styles.alert}
/>
{richText?.text ? (
@@ -537,18 +525,7 @@ let PostThreadItemLoaded = ({
/>
) : undefined}
{post.embed && (
<ContentHider
style={styles.contentHider}
moderation={moderation.embed}
moderationDecisions={moderation.decisions}
ignoreMute={isEmbedByEmbedder(post.embed, post.author.did)}
ignoreQuoteDecisions>
<PostEmbeds
embed={post.embed}
moderation={moderation.embed}
moderationDecisions={moderation.decisions}
/>
</ContentHider>
<PostEmbeds embed={post.embed} moderation={moderation} />
)}
<PostCtrls
post={post}
+9 -16
View File
@@ -4,7 +4,7 @@ import {
AppBskyFeedDefs,
AppBskyFeedPost,
AtUri,
PostModeration,
ModerationDecision,
RichText as RichTextAPI,
} from '@atproto/api'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
@@ -92,7 +92,7 @@ function PostInner({
post: Shadow<AppBskyFeedDefs.PostView>
record: AppBskyFeedPost.Record
richText: RichTextAPI
moderation: PostModeration
moderation: ModerationDecision
showReplyLine?: boolean
style?: StyleProp<ViewStyle>
}) {
@@ -141,7 +141,7 @@ function PostInner({
did={post.author.did}
handle={post.author.handle}
avatar={post.author.avatar}
moderation={moderation.avatar}
moderation={moderation.ui('avatar')}
/>
</View>
<View style={styles.layoutContent}>
@@ -176,10 +176,13 @@ function PostInner({
</View>
)}
<ContentHider
moderation={moderation.content}
modui={moderation.ui('contentView')}
style={styles.contentHider}
childContainerStyle={styles.contentHiderChild}>
<PostAlerts moderation={moderation.content} style={styles.alert} />
<PostAlerts
modui={moderation.ui('contentView')}
style={styles.alert}
/>
{richText.text ? (
<View style={styles.postTextContainer}>
<RichText
@@ -201,17 +204,7 @@ function PostInner({
/>
) : undefined}
{post.embed ? (
<ContentHider
moderation={moderation.embed}
moderationDecisions={moderation.decisions}
ignoreQuoteDecisions
style={styles.contentHider}>
<PostEmbeds
embed={post.embed}
moderation={moderation.embed}
moderationDecisions={moderation.decisions}
/>
</ContentHider>
<PostEmbeds embed={post.embed} moderation={moderation} />
) : null}
</ContentHider>
<PostCtrls
+14 -24
View File
@@ -4,7 +4,7 @@ import {
AppBskyFeedDefs,
AppBskyFeedPost,
AtUri,
PostModeration,
ModerationDecision,
RichText as RichTextAPI,
} from '@atproto/api'
import {
@@ -28,7 +28,6 @@ import {usePalette} from 'lib/hooks/usePalette'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {makeProfileLink} from 'lib/routes/links'
import {isEmbedByEmbedder} from 'lib/embeds'
import {MAX_POST_LINES} from 'lib/constants'
import {countLines} from 'lib/strings/helpers'
import {useComposerControls} from '#/state/shell/composer'
@@ -50,7 +49,7 @@ export function FeedItem({
post: AppBskyFeedDefs.PostView
record: AppBskyFeedPost.Record
reason: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource | undefined
moderation: PostModeration
moderation: ModerationDecision
isThreadChild?: boolean
isThreadLastChild?: boolean
isThreadParent?: boolean
@@ -98,7 +97,7 @@ let FeedItemInner = ({
record: AppBskyFeedPost.Record
reason: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource | undefined
richText: RichTextAPI
moderation: PostModeration
moderation: ModerationDecision
isThreadChild?: boolean
isThreadLastChild?: boolean
isThreadParent?: boolean
@@ -111,9 +110,12 @@ let FeedItemInner = ({
const urip = new AtUri(post.uri)
return makeProfileLink(post.author, 'post', urip.rkey)
}, [post.uri, post.author])
const isModeratedPost =
moderation.decisions.post.cause?.type === 'label' &&
moderation.decisions.post.cause.label.src !== currentAccount?.did
const isModeratedPost = !!moderation.causes.find(
cause =>
cause?.type === 'label' &&
cause.label.src !== currentAccount?.did &&
cause.label.uri === post.uri,
)
const replyAuthorDid = useMemo(() => {
if (!record?.reply) {
@@ -247,7 +249,7 @@ let FeedItemInner = ({
did={post.author.did}
handle={post.author.handle}
avatar={post.author.avatar}
moderation={moderation.avatar}
moderation={moderation.ui('avatar')}
/>
{isThreadParent && (
<View
@@ -324,7 +326,7 @@ let PostContent = ({
postEmbed,
postAuthor,
}: {
moderation: PostModeration
moderation: ModerationDecision
richText: RichTextAPI
postEmbed: AppBskyFeedDefs.PostView['embed']
postAuthor: AppBskyFeedDefs.PostView['author']
@@ -342,10 +344,10 @@ let PostContent = ({
return (
<ContentHider
testID="contentHider-post"
moderation={moderation.content}
modui={moderation.ui('contentList')}
ignoreMute
childContainerStyle={styles.contentHiderChild}>
<PostAlerts moderation={moderation.content} style={styles.alert} />
<PostAlerts modui={moderation.ui('contentList')} style={styles.alert} />
{richText.text ? (
<View style={styles.postTextContainer}>
<RichText
@@ -367,19 +369,7 @@ let PostContent = ({
/>
) : undefined}
{postEmbed ? (
<ContentHider
testID="contentHider-embed"
moderation={moderation.embed}
moderationDecisions={moderation.decisions}
ignoreMute={isEmbedByEmbedder(postEmbed, postAuthor.did)}
ignoreQuoteDecisions
style={styles.embed}>
<PostEmbeds
embed={postEmbed}
moderation={moderation.embed}
moderationDecisions={moderation.decisions}
/>
</ContentHider>
<PostEmbeds embed={postEmbed} moderation={moderation} />
) : null}
</ContentHider>
)
+18 -17
View File
@@ -4,7 +4,7 @@ import {
AppBskyActorDefs,
moderateProfile,
ModerationCause,
ProfileModeration,
ModerationDecision,
} from '@atproto/api'
import {Link} from '../util/Link'
import {Text} from '../util/text/Text'
@@ -15,7 +15,7 @@ import {FollowButton} from './FollowButton'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {makeProfileLink} from 'lib/routes/links'
import {getProfileModerationCauses, getModerationCauseKey} from 'lib/moderation'
import {getModerationCauseKey} from 'lib/moderation'
import {Shadow} from '#/state/cache/types'
import {useModerationOpts} from '#/state/queries/preferences'
import {useProfileShadow} from '#/state/cache/profile-shadow'
@@ -51,11 +51,8 @@ export function ProfileCard({
return null
}
const moderation = moderateProfile(profile, moderationOpts)
if (
!noModFilter &&
moderation.account.filter &&
moderation.account.cause?.type !== 'muted'
) {
const modui = moderation.ui('profileList')
if (!noModFilter && modui.filter /* TODO && modui.type !== 'muted'*/) {
return null
}
@@ -78,7 +75,7 @@ export function ProfileCard({
<UserAvatar
size={40}
avatar={profile.avatar}
moderation={moderation.avatar}
moderation={moderation.ui('avatar')}
/>
</View>
<View style={styles.layoutContent}>
@@ -89,7 +86,7 @@ export function ProfileCard({
lineHeight={1.2}>
{sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
moderation.profile,
moderation.ui('displayName'),
)}
</Text>
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
@@ -122,12 +119,12 @@ export function ProfileCardPills({
moderation,
}: {
followedBy: boolean
moderation: ProfileModeration
moderation: ModerationDecision
}) {
const pal = usePalette('default')
const causes = getProfileModerationCauses(moderation)
if (!followedBy && !causes.length) {
const informs = moderation.ui('profileList').informs
if (!followedBy && !informs.length) {
return null
}
@@ -140,10 +137,10 @@ export function ProfileCardPills({
</Text>
</View>
)}
{causes.map(cause => (
{informs.map(inform => (
<ProfileCardPillModerationCause
key={getModerationCauseKey(cause)}
cause={cause}
key={getModerationCauseKey(inform)}
cause={inform}
/>
))}
</View>
@@ -183,7 +180,7 @@ function FollowersList({
f,
mod: moderateProfile(f, moderationOpts),
}))
.filter(({mod}) => !mod.account.filter)
.filter(({mod}) => !mod.ui('profileList').filter)
}, [followers, moderationOpts])
if (!followersWithMods?.length) {
@@ -205,7 +202,11 @@ function FollowersList({
{followersWithMods.slice(0, 3).map(({f, mod}) => (
<View key={f.did} style={styles.followedByAviContainer}>
<View style={[styles.followedByAvi, pal.view]}>
<UserAvatar avatar={f.avatar} size={32} moderation={mod.avatar} />
<UserAvatar
avatar={f.avatar}
size={32}
moderation={mod.ui('avatar')}
/>
</View>
</View>
))}
+9 -8
View File
@@ -134,10 +134,8 @@ let ProfileHeader = ({
}, [navigation])
const onPressAvi = React.useCallback(() => {
if (
profile.avatar &&
!(moderation.avatar.blur && moderation.avatar.noOverride)
) {
const modui = moderation.ui('avatar')
if (profile.avatar && !(modui.blur && modui.noOverride)) {
openLightbox(new ProfileImageLightbox(profile))
}
}, [openLightbox, profile, moderation])
@@ -407,7 +405,10 @@ let ProfileHeader = ({
style={{borderRadius: 0}}
/>
) : (
<UserBanner banner={profile.banner} moderation={moderation.avatar} />
<UserBanner
banner={profile.banner}
moderation={moderation.ui('banner')}
/>
)}
</View>
<View style={styles.content} pointerEvents="box-none">
@@ -538,7 +539,7 @@ let ProfileHeader = ({
style={[pal.text, styles.title]}>
{sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
moderation.profile,
moderation.ui('displayName'),
)}
</Text>
</View>
@@ -611,7 +612,7 @@ let ProfileHeader = ({
</Text>
</Text>
</View>
{descriptionRT && !moderation.profile.blur ? (
{descriptionRT && !moderation.ui('profileView').blur ? (
<View pointerEvents="auto">
<RichText
testID="profileHeaderDescription"
@@ -671,7 +672,7 @@ let ProfileHeader = ({
<UserAvatar
size={80}
avatar={profile.avatar}
moderation={moderation.avatar}
moderation={moderation.ui('avatar')}
/>
</View>
</TouchableWithoutFeedback>
@@ -214,7 +214,7 @@ function SuggestedFollow({
<UserAvatar
size={60}
avatar={profile.avatar}
moderation={moderation.avatar}
moderation={moderation.ui('avatar')}
/>
<View style={{width: '100%', paddingVertical: 12}}>
@@ -224,7 +224,7 @@ function SuggestedFollow({
numberOfLines={1}>
{sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
moderation.profile,
moderation.ui('displayName'),
)}
</Text>
<Text
+80 -89
View File
@@ -1,44 +1,44 @@
import React from 'react'
import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {usePalette} from 'lib/hooks/usePalette'
import {ModerationUI, PostModeration} from '@atproto/api'
import {Text} from '../text/Text'
import {ShieldExclamation} from 'lib/icons'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {ModerationUI} from '@atproto/api'
import {useLingui} from '@lingui/react'
import {msg, Trans} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {isPostMediaBlurred} from 'lib/moderation'
import {atoms as a, useTheme, useBreakpoints} from '#/alf'
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
import {Shield_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield'
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
import {Text} from '#/components/Typography'
import {ModerationDetailsDialog} from '#/components/dialogs/ModerationDetails'
import {useOpenGlobalDialog} from '#/components/dialogs'
export function ContentHider({
testID,
moderation,
moderationDecisions,
ignoreMute,
ignoreQuoteDecisions,
modui,
// ignoreMute, TODO
style,
childContainerStyle,
children,
}: React.PropsWithChildren<{
testID?: string
moderation: ModerationUI
moderationDecisions?: PostModeration['decisions']
modui: ModerationUI | undefined
ignoreMute?: boolean
ignoreQuoteDecisions?: boolean
style?: StyleProp<ViewStyle>
childContainerStyle?: StyleProp<ViewStyle>
}>) {
const pal = usePalette('default')
const t = useTheme()
const {_} = useLingui()
const [override, setOverride] = React.useState(false)
const {openModal} = useModalControls()
const desc = useModerationCauseDescription(moderation.cause, 'content')
const {gtMobile} = useBreakpoints()
const openDialog = useOpenGlobalDialog()
const blur = modui?.blurs[0]
const desc = useModerationCauseDescription(blur, 'content')
if (
!moderation.blur ||
(ignoreMute && moderation.cause?.type === 'muted') ||
shouldIgnoreQuote(moderationDecisions, ignoreQuoteDecisions)
!blur
// || (ignoreMute && moderation.cause?.type === 'muted') TODO
) {
return (
<View testID={testID} style={[styles.outer, style]}>
@@ -47,83 +47,74 @@ export function ContentHider({
)
}
const isMute = moderation.cause?.type === 'muted'
return (
<View testID={testID} style={[styles.outer, style]}>
<Pressable
onPress={() => {
if (!moderation.noOverride) {
setOverride(v => !v)
} else {
openModal({
name: 'moderation-details',
context: 'content',
moderation,
})
}
}}
accessibilityRole="button"
accessibilityHint={
override ? _(msg`Hide the content`) : _(msg`Show the content`)
}
accessibilityLabel=""
style={[
styles.cover,
moderation.noOverride
? {borderWidth: 1, borderColor: pal.colors.borderDark}
: pal.viewLight,
]}>
<Pressable
<View testID={testID} style={[a.overflow_hidden, style]}>
<View style={[a.flex_col, a.gap_xs]}>
<Button
variant="solid"
color="secondary"
size={gtMobile ? 'large' : 'small'}
shape="default"
onPress={() => {
openModal({
name: 'moderation-details',
context: 'content',
moderation,
})
if (!modui.noOverride) {
setOverride(v => !v)
} else {
openDialog(ModerationDetailsDialog, {
context: 'content',
modcause: blur,
})
}
}}
accessibilityRole="button"
accessibilityLabel={_(msg`Learn more about this warning`)}
accessibilityHint="">
{isMute ? (
<FontAwesomeIcon
icon={['far', 'eye-slash']}
size={18}
color={pal.colors.textLight}
/>
) : (
<ShieldExclamation size={18} style={pal.textLight} />
label={desc.name}
accessibilityHint={
override ? _(msg`Hide the content`) : _(msg`Show the content`)
}>
<ButtonIcon
icon={blur.type === 'muted' ? EyeSlash : Shield}
position="left"
/>{' '}
<ButtonText style={[a.flex_1, a.text_left]}>{desc.name}</ButtonText>
{!modui.noOverride && (
<ButtonText>
{override ? <Trans>Hide</Trans> : <Trans>Show</Trans>}
</ButtonText>
)}
</Pressable>
<Text type="md" style={[pal.text, {flex: 1}]} numberOfLines={2}>
{desc.name}
</Text>
<View style={styles.showBtn}>
<Text type="lg" style={pal.link}>
{moderation.noOverride ? (
<Trans>Learn more</Trans>
) : override ? (
<Trans>Hide</Trans>
) : (
<Trans>Show</Trans>
)}
</Text>
</View>
</Pressable>
</Button>
{blur.type === 'label' && !override && (
<Button
variant="ghost"
size="tiny"
onPress={() => {
openDialog(ModerationDetailsDialog, {
context: 'content',
modcause: blur,
})
}}
label={_(msg`Learn more`)}>
<ButtonText
style={[
a.flex_1,
a.text_sm,
a.font_normal,
t.atoms.text_contrast_medium,
a.text_left,
]}>
{/* TODO get actual labeler */}
<Trans>
Labeled by Bluesky Safety.{' '}
<Text style={[{color: t.palette.primary_500}, a.text_sm]}>
Learn more.
</Text>
</Trans>
</ButtonText>
</Button>
)}
</View>
{override && <View style={childContainerStyle}>{children}</View>}
</View>
)
}
function shouldIgnoreQuote(
decisions: PostModeration['decisions'] | undefined,
ignore: boolean | undefined,
): boolean {
if (!decisions || !ignore) {
return false
}
return !isPostMediaBlurred(decisions)
}
const styles = StyleSheet.create({
outer: {
overflow: 'hidden',
+82 -50
View File
@@ -1,67 +1,99 @@
import React from 'react'
import {Pressable, StyleProp, StyleSheet, ViewStyle} from 'react-native'
import {ModerationUI} from '@atproto/api'
import {StyleProp, View, ViewStyle} from 'react-native'
import {ModerationUI, ModerationCause} from '@atproto/api'
import {Text} from '../text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {ShieldExclamation} from 'lib/icons'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {Trans} from '@lingui/macro'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {getModerationCauseKey} from '#/lib/moderation'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
import {Shield_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
import {ModerationDetailsDialog} from '#/components/dialogs/ModerationDetails'
import {useOpenGlobalDialog} from '#/components/dialogs'
export function PostAlerts({
moderation,
modui,
style,
}: {
moderation: ModerationUI
modui: ModerationUI
includeMute?: boolean
style?: StyleProp<ViewStyle>
}) {
const pal = usePalette('default')
const {_} = useLingui()
const {openModal} = useModalControls()
const desc = useModerationCauseDescription(moderation.cause, 'content')
const shouldAlert = !!moderation.cause && moderation.alert
if (!shouldAlert) {
if (!modui.alert && !modui.inform) {
return null
}
return (
<Pressable
onPress={() => {
openModal({
name: 'moderation-details',
context: 'content',
moderation,
})
}}
accessibilityRole="button"
accessibilityLabel={_(msg`Learn more about this warning`)}
accessibilityHint=""
style={[styles.container, pal.viewLight, style]}>
<ShieldExclamation style={pal.text} size={16} />
<Text type="lg" style={[pal.text]}>
{desc.name}{' '}
<Text type="lg" style={[pal.link, styles.learnMoreBtn]}>
<Trans>Learn More</Trans>
</Text>
</Text>
</Pressable>
<View style={[a.flex_col, a.gap_xs, a.mb_sm, style]}>
{modui.inform && (
<View style={[a.flex_row, a.flex_wrap, a.gap_xs]}>
{modui.informs.map(cause => (
<PostInform key={getModerationCauseKey(cause)} cause={cause} />
))}
</View>
)}
{modui.alerts.map(cause => (
<PostAlert key={getModerationCauseKey(cause)} cause={cause} />
))}
</View>
)
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
paddingVertical: 8,
paddingLeft: 14,
paddingHorizontal: 16,
borderRadius: 8,
},
learnMoreBtn: {
marginLeft: 'auto',
},
})
function PostInform({cause}: {cause: ModerationCause}) {
const openDialog = useOpenGlobalDialog()
const desc = useModerationCauseDescription(cause, 'content')
return (
<Button
label={desc.name}
variant="solid"
color="secondary"
size="tiny"
shape="default"
onPress={() => {
openDialog(ModerationDetailsDialog, {
context: 'content',
modcause: cause,
})
}}>
<ButtonIcon
icon={cause.type === 'muted' ? EyeSlash : CircleInfo}
position="left"
/>{' '}
<ButtonText>{desc.name}</ButtonText>
</Button>
)
}
function PostAlert({cause}: {cause: ModerationCause}) {
const t = useTheme()
const openDialog = useOpenGlobalDialog()
const desc = useModerationCauseDescription(cause, 'content')
return (
<Button
label={desc.name}
variant="solid"
color="secondary"
size="small"
shape="default"
onPress={() => {
openDialog(ModerationDetailsDialog, {
context: 'content',
modcause: cause,
})
}}>
<ButtonIcon icon={Shield} position="left" />
<ButtonText style={[a.flex_1, a.text_left]}>
{desc.name}
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
{' — ' /* TODO get actual labeler */}
<Trans>Bluesky Safety</Trans>
</Text>
</ButtonText>
</Button>
)
}
+14 -12
View File
@@ -9,19 +9,21 @@ import {addStyle} from 'lib/styles'
import {ShieldExclamation} from 'lib/icons'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {ModerationDetailsDialog} from '#/components/dialogs/ModerationDetails'
import {useOpenGlobalDialog} from '#/components/dialogs'
interface Props extends ComponentProps<typeof Link> {
iconSize: number
iconStyles: StyleProp<ViewStyle>
moderation: ModerationUI
modui: ModerationUI
}
export function PostHider({
testID,
href,
moderation,
modui,
style,
children,
iconSize,
@@ -31,10 +33,11 @@ export function PostHider({
const pal = usePalette('default')
const {_} = useLingui()
const [override, setOverride] = React.useState(false)
const {openModal} = useModalControls()
const desc = useModerationCauseDescription(moderation.cause, 'content')
const openDialog = useOpenGlobalDialog()
const blur = modui.blurs[0]
const desc = useModerationCauseDescription(blur, 'content')
if (!moderation.blur) {
if (!blur) {
return (
<Link
testID={testID}
@@ -48,11 +51,11 @@ export function PostHider({
)
}
const isMute = moderation.cause?.type === 'muted'
const isMute = blur.type === 'muted'
return !override ? (
<Pressable
onPress={() => {
if (!moderation.noOverride) {
if (!modui.noOverride) {
setOverride(v => !v)
}
}}
@@ -68,10 +71,9 @@ export function PostHider({
]}>
<Pressable
onPress={() => {
openModal({
name: 'moderation-details',
openDialog(ModerationDetailsDialog, {
context: 'content',
moderation,
modcause: blur,
})
}}
accessibilityRole="button"
@@ -103,7 +105,7 @@ export function PostHider({
<Text type="sm" style={[{flex: 1}, pal.textLight]} numberOfLines={1}>
{desc.name}
</Text>
{!moderation.noOverride && (
{!modui.noOverride && (
<Text type="sm" style={[styles.showBtn, pal.link]}>
{override ? <Trans>Hide</Trans> : <Trans>Show</Trans>}
</Text>
@@ -1,101 +1,99 @@
import React from 'react'
import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {ModerationCause, ProfileModeration} from '@atproto/api'
import {StyleProp, View, ViewStyle} from 'react-native'
import {ModerationCause, ModerationDecision} from '@atproto/api'
import {Text} from '../text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {ShieldExclamation} from 'lib/icons'
import {getModerationCauseKey, getProfileModerationCauses} from 'lib/moderation'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useModalControls} from '#/state/modals'
import {getModerationCauseKey} from 'lib/moderation'
import {Trans} from '@lingui/macro'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
import {Shield_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
import {ModerationDetailsDialog} from '#/components/dialogs/ModerationDetails'
import {useOpenGlobalDialog} from '#/components/dialogs'
export function ProfileHeaderAlerts({
moderation,
style,
}: {
moderation: ProfileModeration
moderation: ModerationDecision
style?: StyleProp<ViewStyle>
}) {
const causes = getProfileModerationCauses(moderation)
if (!causes.length) {
const modui = moderation.ui('profileView')
if (!modui.alert && !modui.inform) {
return null
}
return (
<View style={styles.grid}>
{causes.map(cause => (
<ProfileHeaderAlert
key={getModerationCauseKey(cause)}
cause={cause}
style={style}
/>
<View style={[a.flex_col, a.gap_xs, a.mb_sm, style]}>
{modui.inform && (
<View style={[a.flex_row, a.flex_wrap, a.gap_xs]}>
{modui.informs.map(cause => (
<ProfileInform key={getModerationCauseKey(cause)} cause={cause} />
))}
</View>
)}
{modui.alerts.map(cause => (
<ProfileAlert key={getModerationCauseKey(cause)} cause={cause} />
))}
</View>
)
}
function ProfileHeaderAlert({
cause,
style,
}: {
cause: ModerationCause
style?: StyleProp<ViewStyle>
}) {
const pal = usePalette('default')
const {_} = useLingui()
const {openModal} = useModalControls()
const desc = useModerationCauseDescription(cause, 'account')
function ProfileInform({cause}: {cause: ModerationCause}) {
const openDialog = useOpenGlobalDialog()
const desc = useModerationCauseDescription(cause, 'content')
const isMute = cause.type === 'muted'
return (
<Pressable
testID="profileHeaderAlert"
key={desc.name}
<Button
label={desc.name}
variant="solid"
color="secondary"
size="tiny"
shape="default"
onPress={() => {
openModal({
name: 'moderation-details',
openDialog(ModerationDetailsDialog, {
context: 'content',
moderation: {cause},
modcause: cause,
})
}}
accessibilityRole="button"
accessibilityLabel={_(msg`Learn more about this warning`)}
accessibilityHint=""
style={[styles.container, pal.viewLight, style]}>
{isMute ? (
<FontAwesomeIcon
icon={['far', 'eye-slash']}
size={14}
color={pal.colors.textLight}
/>
) : (
<ShieldExclamation style={pal.text} size={18} />
)}
<Text type="sm" style={[{flex: 1}, pal.text]}>
{desc.name}
</Text>
<Text type="sm" style={[pal.link, styles.learnMoreBtn]}>
<Trans>Learn More</Trans>
</Text>
</Pressable>
}}>
<ButtonIcon
icon={cause.type === 'muted' ? EyeSlash : CircleInfo}
position="left"
/>{' '}
<ButtonText>{desc.name}</ButtonText>
</Button>
)
}
const styles = StyleSheet.create({
grid: {
gap: 4,
},
container: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 8,
},
learnMoreBtn: {
marginLeft: 'auto',
},
})
function ProfileAlert({cause}: {cause: ModerationCause}) {
const t = useTheme()
const openDialog = useOpenGlobalDialog()
const desc = useModerationCauseDescription(cause, 'content')
return (
<Button
label={desc.name}
variant="solid"
color="secondary"
size="small"
shape="default"
onPress={() => {
openDialog(ModerationDetailsDialog, {
context: 'content',
modcause: cause,
})
}}>
<ButtonIcon icon={Shield} position="left" />
<ButtonText style={[a.flex_1, a.text_left]}>
{desc.name}
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
{' — ' /* TODO get actual labeler */}
<Trans>Bluesky Safety</Trans>
</Text>
</ButtonText>
</Button>
)
}
+16 -13
View File
@@ -19,22 +19,24 @@ import {Text} from '../text/Text'
import {Button} from '../forms/Button'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {s} from '#/lib/styles'
import {CenteredView} from '../Views'
import {ModerationDetailsDialog} from '#/components/dialogs/ModerationDetails'
import {useOpenGlobalDialog} from '#/components/dialogs'
export function ScreenHider({
testID,
screenDescription,
moderation,
modui,
style,
containerStyle,
children,
}: React.PropsWithChildren<{
testID?: string
screenDescription: string
moderation: ModerationUI
modui: ModerationUI
style?: StyleProp<ViewStyle>
containerStyle?: StyleProp<ViewStyle>
}>) {
@@ -44,10 +46,11 @@ export function ScreenHider({
const [override, setOverride] = React.useState(false)
const navigation = useNavigation<NavigationProp>()
const {isMobile} = useWebMediaQueries()
const {openModal} = useModalControls()
const desc = useModerationCauseDescription(moderation.cause, 'account')
const openDialog = useOpenGlobalDialog()
const blur = modui.blurs[0]
const desc = useModerationCauseDescription(blur, 'content')
if (!moderation.blur || override) {
if (!blur || override) {
return (
<View testID={testID} style={style}>
{children}
@@ -55,9 +58,10 @@ export function ScreenHider({
)
}
const isNoPwi =
moderation.cause?.type === 'label' &&
moderation.cause?.labelDef.id === '!no-unauthenticated'
const isNoPwi = !!modui.blurs.find(
cause =>
cause.type === 'label' && cause.labelDef.id === '!no-unauthenticated',
)
return (
<CenteredView
style={[styles.container, pal.view, containerStyle]}
@@ -91,10 +95,9 @@ export function ScreenHider({
</Text>
<TouchableWithoutFeedback
onPress={() => {
openModal({
name: 'moderation-details',
openDialog(ModerationDetailsDialog, {
context: 'account',
moderation,
modcause: blur,
})
}}
accessibilityRole="button"
@@ -123,7 +126,7 @@ export function ScreenHider({
<Trans>Go back</Trans>
</Text>
</Button>
{!moderation.noOverride && (
{!modui.noOverride && (
<Button
type="default"
onPress={() => setOverride(v => !v)}
+85 -44
View File
@@ -1,13 +1,15 @@
import React from 'react'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {
AppBskyFeedDefs,
AppBskyEmbedRecord,
AppBskyFeedPost,
AppBskyEmbedImages,
AppBskyEmbedRecordWithMedia,
ModerationUI,
AppBskyEmbedExternal,
RichText as RichTextAPI,
moderatePost,
ModerationDecision,
} from '@atproto/api'
import {AtUri} from '@atproto/api'
import {PostMeta} from '../PostMeta'
@@ -21,14 +23,14 @@ import {makeProfileLink} from 'lib/routes/links'
import {InfoCircleIcon} from 'lib/icons'
import {Trans} from '@lingui/macro'
import {RichText} from 'view/com/util/text/RichText'
import {useModerationOpts} from '#/state/queries/preferences'
import {ContentHider} from '../moderation/ContentHider'
export function MaybeQuoteEmbed({
embed,
moderation,
style,
}: {
embed: AppBskyEmbedRecord.View
moderation: ModerationUI
style?: StyleProp<ViewStyle>
}) {
const pal = usePalette('default')
@@ -38,17 +40,9 @@ export function MaybeQuoteEmbed({
AppBskyFeedPost.validateRecord(embed.record.value).success
) {
return (
<QuoteEmbed
quote={{
author: embed.record.author,
cid: embed.record.cid,
uri: embed.record.uri,
indexedAt: embed.record.indexedAt,
text: embed.record.value.text,
facets: embed.record.value.facets,
embeds: embed.record.embeds,
}}
moderation={moderation}
<QuoteEmbedModerated
viewRecord={embed.record}
postRecord={embed.record.value}
style={style}
/>
)
@@ -74,19 +68,49 @@ export function MaybeQuoteEmbed({
return null
}
function QuoteEmbedModerated({
viewRecord,
postRecord,
style,
}: {
viewRecord: AppBskyEmbedRecord.ViewRecord
postRecord: AppBskyFeedPost.Record
style?: StyleProp<ViewStyle>
}) {
const moderationOpts = useModerationOpts()
const moderation = React.useMemo(() => {
return moderationOpts
? moderatePost(viewRecordToPostView(viewRecord), moderationOpts)
: undefined
}, [viewRecord, moderationOpts])
const quote = {
author: viewRecord.author,
cid: viewRecord.cid,
uri: viewRecord.uri,
indexedAt: viewRecord.indexedAt,
text: postRecord.text,
facets: postRecord.facets,
embeds: viewRecord.embeds,
}
return <QuoteEmbed quote={quote} moderation={moderation} style={style} />
}
export function QuoteEmbed({
quote,
moderation,
style,
}: {
quote: ComposerOptsQuote
moderation?: ModerationUI
moderation?: ModerationDecision
style?: StyleProp<ViewStyle>
}) {
const pal = usePalette('default')
const itemUrip = new AtUri(quote.uri)
const itemHref = makeProfileLink(quote.author, 'post', itemUrip.rkey)
const itemTitle = `Post by ${quote.author.handle}`
const richText = React.useMemo(
() =>
quote.text.trim()
@@ -94,6 +118,7 @@ export function QuoteEmbed({
: undefined,
[quote.text, quote.facets],
)
const embed = React.useMemo(() => {
const e = quote.embeds?.[0]
@@ -107,39 +132,55 @@ export function QuoteEmbed({
return e.media
}
}, [quote.embeds])
return (
<Link
style={[styles.container, pal.borderDark, style]}
hoverStyle={{borderColor: pal.colors.borderLinkHover}}
href={itemHref}
title={itemTitle}>
<View pointerEvents="none">
<PostMeta
author={quote.author}
showAvatar
authorHasWarning={false}
postHref={itemHref}
timestamp={quote.indexedAt}
/>
</View>
{moderation ? (
<PostAlerts moderation={moderation} style={styles.alert} />
) : null}
{richText ? (
<RichText
richText={richText}
type="post-text"
style={pal.text}
numberOfLines={20}
noLinks
/>
) : null}
{embed && <PostEmbeds embed={embed} moderation={{}} />}
</Link>
<ContentHider modui={moderation?.ui('contentList')}>
<Link
style={[styles.container, pal.borderDark, style]}
hoverStyle={{borderColor: pal.colors.borderLinkHover}}
href={itemHref}
title={itemTitle}>
<View pointerEvents="none">
<PostMeta
author={quote.author}
showAvatar
authorHasWarning={false}
postHref={itemHref}
timestamp={quote.indexedAt}
/>
</View>
{moderation ? (
<PostAlerts
modui={moderation.ui('contentView')}
style={styles.alert}
/>
) : null}
{richText ? (
<RichText
richText={richText}
type="post-text"
style={pal.text}
numberOfLines={20}
noLinks
/>
) : null}
{embed && <PostEmbeds embed={embed} moderation={moderation} />}
</Link>
</ContentHider>
)
}
export default QuoteEmbed
function viewRecordToPostView(
viewRecord: AppBskyEmbedRecord.ViewRecord,
): AppBskyFeedDefs.PostView {
const {value, embeds, ...rest} = viewRecord
return {
...rest,
$type: 'app.bsky.feed.defs#postView',
record: value,
embed: embeds?.[0],
}
}
const styles = StyleSheet.create({
container: {
+49 -55
View File
@@ -15,8 +15,7 @@ import {
AppBskyEmbedRecordWithMedia,
AppBskyFeedDefs,
AppBskyGraphDefs,
ModerationUI,
PostModeration,
ModerationDecision,
} from '@atproto/api'
import {Link} from '../Link'
import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
@@ -26,7 +25,6 @@ import {ExternalLinkEmbed} from './ExternalLinkEmbed'
import {MaybeQuoteEmbed} from './QuoteEmbed'
import {AutoSizedImage} from '../images/AutoSizedImage'
import {ListEmbed} from './ListEmbed'
import {isCauseALabelOnUri, isQuoteBlurred} from 'lib/moderation'
import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
import {ContentHider} from '../moderation/ContentHider'
import {isNative} from '#/platform/detection'
@@ -42,12 +40,10 @@ type Embed =
export function PostEmbeds({
embed,
moderation,
moderationDecisions,
style,
}: {
embed?: Embed
moderation: ModerationUI
moderationDecisions?: PostModeration['decisions']
moderation?: ModerationDecision
style?: StyleProp<ViewStyle>
}) {
const pal = usePalette('default')
@@ -66,18 +62,10 @@ export function PostEmbeds({
// quote post with media
// =
if (AppBskyEmbedRecordWithMedia.isView(embed)) {
const isModOnQuote =
(AppBskyEmbedRecord.isViewRecord(embed.record.record) &&
isCauseALabelOnUri(moderation.cause, embed.record.record.uri)) ||
(moderationDecisions && isQuoteBlurred(moderationDecisions))
const mediaModeration = isModOnQuote ? {} : moderation
const quoteModeration = isModOnQuote ? moderation : {}
return (
<View style={style}>
<PostEmbeds embed={embed.media} moderation={mediaModeration} />
<ContentHider moderation={quoteModeration}>
<MaybeQuoteEmbed embed={embed.record} moderation={quoteModeration} />
</ContentHider>
<PostEmbeds embed={embed.media} moderation={moderation} />
<MaybeQuoteEmbed embed={embed.record} />
</View>
)
}
@@ -86,6 +74,7 @@ export function PostEmbeds({
// custom feed embed (i.e. generator view)
// =
if (AppBskyFeedDefs.isGeneratorView(embed.record)) {
// TODO moderation
return (
<FeedSourceCard
feedUri={embed.record.uri}
@@ -97,16 +86,13 @@ export function PostEmbeds({
// list embed
if (AppBskyGraphDefs.isListView(embed.record)) {
// TODO moderation
return <ListEmbed item={embed.record} />
}
// quote post
// =
return (
<ContentHider moderation={moderation}>
<MaybeQuoteEmbed embed={embed} style={style} moderation={moderation} />
</ContentHider>
)
return <MaybeQuoteEmbed embed={embed} style={style} />
}
// image embed
@@ -132,35 +118,41 @@ export function PostEmbeds({
if (images.length === 1) {
const {alt, thumb, aspectRatio} = images[0]
return (
<View style={[styles.imagesContainer, style]}>
<AutoSizedImage
alt={alt}
uri={thumb}
dimensionsHint={aspectRatio}
onPress={() => _openLightbox(0)}
onPressIn={() => onPressIn(0)}
style={[styles.singleImage]}>
{alt === '' ? null : (
<View style={styles.altContainer}>
<Text style={styles.alt} accessible={false}>
ALT
</Text>
</View>
)}
</AutoSizedImage>
</View>
<ContentHider modui={moderation?.ui('contentMedia')}>
<View style={[styles.imagesContainer, style]}>
<AutoSizedImage
alt={alt}
uri={thumb}
dimensionsHint={aspectRatio}
onPress={() => _openLightbox(0)}
onPressIn={() => onPressIn(0)}
style={[styles.singleImage]}>
{alt === '' ? null : (
<View style={styles.altContainer}>
<Text style={styles.alt} accessible={false}>
ALT
</Text>
</View>
)}
</AutoSizedImage>
</View>
</ContentHider>
)
}
return (
<View style={[styles.imagesContainer, style]}>
<ImageLayoutGrid
images={embed.images}
onPress={_openLightbox}
onPressIn={onPressIn}
style={embed.images.length === 1 ? [styles.singleImage] : undefined}
/>
</View>
<ContentHider modui={moderation?.ui('contentMedia')}>
<View style={[styles.imagesContainer, style]}>
<ImageLayoutGrid
images={embed.images}
onPress={_openLightbox}
onPressIn={onPressIn}
style={
embed.images.length === 1 ? [styles.singleImage] : undefined
}
/>
</View>
</ContentHider>
)
}
}
@@ -171,15 +163,17 @@ export function PostEmbeds({
const link = embed.external
return (
<Link
asAnchor
anchorNoUnderline
href={link.uri}
style={[styles.extOuter, pal.view, pal.borderDark, style]}
hoverStyle={{borderColor: pal.colors.borderLinkHover}}
onLongPress={onShareExternal}>
<ExternalLinkEmbed link={link} />
</Link>
<ContentHider modui={moderation?.ui('contentMedia')}>
<Link
asAnchor
anchorNoUnderline
href={link.uri}
style={[styles.extOuter, pal.view, pal.borderDark, style]}
hoverStyle={{borderColor: pal.colors.borderLinkHover}}
onLongPress={onShareExternal}>
<ExternalLinkEmbed link={link} />
</Link>
</ContentHider>
)
}