Refactor/rewrite the entire moderation-application system
This commit is contained in:
@@ -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}>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
@@ -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}>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user