Labeling & moderation updates [DRAFT] (#1057)
* First pass moving to the new labeling sdk (it compiles) * Correct behaviors around interpreting label moderation * Improve moderation state rendering * Improve hiders and alerts * Improve handling of mutes * Improve profile warnings * Add profile blurring to profile header * Add blocks to test cases * Render labels on profile cards, do not filter * Filter profiles from suggestions using moderation * Apply profile blurring to ProfileCard * Handle blocked and deleted quote posts * Temporarily translate content filtering settings to new labels * Fix types * Tune ContentHider & PostHider click targets * Put a warning on profilecard label pills * Fix screenhider learnmore link on mobile * Enforce no-override on user avatar * Dont enumerate profile blur-media labels in alerts * Fixes to muted posts (esp quotes of muted users) * Fixes to account/profile warnings * Bump @atproto/api@0.5.0 * Bump @atproto/api@0.5.1 * Fix tests * 1.43 * Remove log * Bump @atproto/api@0.5.2
This commit is contained in:
parent
3ae5a6b631
commit
b154d3ea21
43 changed files with 1193 additions and 717 deletions
|
@ -30,6 +30,7 @@ import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguages
|
|||
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
|
||||
import * as PreferencesHomeFeed from './PreferencesHomeFeed'
|
||||
import * as OnboardingModal from './OnboardingModal'
|
||||
import * as ModerationDetailsModal from './ModerationDetails'
|
||||
|
||||
const DEFAULT_SNAPPOINTS = ['90%']
|
||||
|
||||
|
@ -136,6 +137,9 @@ export const ModalsContainer = observer(function ModalsContainer() {
|
|||
} else if (activeModal?.name === 'onboarding') {
|
||||
snapPoints = OnboardingModal.snapPoints
|
||||
element = <OnboardingModal.Component />
|
||||
} else if (activeModal?.name === 'moderation-details') {
|
||||
snapPoints = ModerationDetailsModal.snapPoints
|
||||
element = <ModerationDetailsModal.Component {...activeModal} />
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
|
|||
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
||||
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
|
||||
import * as OnboardingModal from './OnboardingModal'
|
||||
import * as ModerationDetailsModal from './ModerationDetails'
|
||||
|
||||
import * as PreferencesHomeFeed from './PreferencesHomeFeed'
|
||||
|
||||
|
@ -110,6 +111,8 @@ function Modal({modal}: {modal: ModalIface}) {
|
|||
element = <PreferencesHomeFeed.Component />
|
||||
} else if (modal.name === 'onboarding') {
|
||||
element = <OnboardingModal.Component />
|
||||
} else if (modal.name === 'moderation-details') {
|
||||
element = <ModerationDetailsModal.Component {...modal} />
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
|
101
src/view/com/modals/ModerationDetails.tsx
Normal file
101
src/view/com/modals/ModerationDetails.tsx
Normal file
|
@ -0,0 +1,101 @@
|
|||
import React from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {ModerationUI} from '@atproto/api'
|
||||
import {useStores} from 'state/index'
|
||||
import {s} from 'lib/styles'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {TextLink} from '../util/Link'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {isDesktopWeb} from 'platform/detection'
|
||||
import {listUriToHref} from 'lib/strings/url-helpers'
|
||||
import {Button} from '../util/forms/Button'
|
||||
|
||||
export const snapPoints = [300]
|
||||
|
||||
export function Component({
|
||||
context,
|
||||
moderation,
|
||||
}: {
|
||||
context: 'account' | 'content'
|
||||
moderation: ModerationUI
|
||||
}) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
|
||||
let name
|
||||
let description
|
||||
if (!moderation.cause) {
|
||||
name = 'Content Warning'
|
||||
description =
|
||||
'Moderator has chosen to set a general warning on the content.'
|
||||
} else if (moderation.cause.type === 'blocking') {
|
||||
name = 'Account Blocked'
|
||||
description = 'You have blocked this user. You cannot view their content.'
|
||||
} else if (moderation.cause.type === 'blocked-by') {
|
||||
name = 'Account Blocks You'
|
||||
description = 'This user has blocked you. You cannot view their content.'
|
||||
} else if (moderation.cause.type === 'muted') {
|
||||
if (moderation.cause.source.type === 'user') {
|
||||
name = 'Account Muted'
|
||||
description = 'You have muted this user.'
|
||||
} else {
|
||||
const list = moderation.cause.source.list
|
||||
name = <>Account Muted by List</>
|
||||
description = (
|
||||
<>
|
||||
This user is included the{' '}
|
||||
<TextLink
|
||||
type="2xl"
|
||||
href={listUriToHref(list.uri)}
|
||||
text={list.name}
|
||||
style={pal.link}
|
||||
/>{' '}
|
||||
list which you have muted.
|
||||
</>
|
||||
)
|
||||
}
|
||||
} else {
|
||||
name = moderation.cause.labelDef.strings[context].en.name
|
||||
description = moderation.cause.labelDef.strings[context].en.description
|
||||
}
|
||||
|
||||
return (
|
||||
<View testID="moderationDetailsModal" style={[styles.container, 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={() => store.shell.closeModal()}>
|
||||
<Text type="button-lg" style={[pal.textLight, s.textCenter, s.white]}>
|
||||
Okay
|
||||
</Text>
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingHorizontal: isDesktopWeb ? 0 : 14,
|
||||
},
|
||||
title: {
|
||||
textAlign: 'center',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 12,
|
||||
},
|
||||
description: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
btn: {
|
||||
paddingVertical: 14,
|
||||
marginTop: isDesktopWeb ? 40 : 0,
|
||||
marginBottom: isDesktopWeb ? 0 : 40,
|
||||
},
|
||||
})
|
|
@ -7,7 +7,11 @@ import {
|
|||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {AppBskyEmbedImages} from '@atproto/api'
|
||||
import {
|
||||
AppBskyEmbedImages,
|
||||
ProfileModeration,
|
||||
moderateProfile,
|
||||
} from '@atproto/api'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
|
@ -31,11 +35,6 @@ import {Link, TextLink} from '../util/Link'
|
|||
import {useStores} from 'state/index'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
|
||||
import {
|
||||
getProfileViewBasicLabelInfo,
|
||||
getProfileModeration,
|
||||
} from 'lib/labeling/helpers'
|
||||
import {ProfileModeration} from 'lib/labeling/types'
|
||||
import {formatCount} from '../util/numeric/format'
|
||||
import {makeProfileLink} from 'lib/routes/links'
|
||||
|
||||
|
@ -99,9 +98,9 @@ export const FeedItem = observer(function ({
|
|||
handle: item.author.handle,
|
||||
displayName: item.author.displayName,
|
||||
avatar: item.author.avatar,
|
||||
moderation: getProfileModeration(
|
||||
store,
|
||||
getProfileViewBasicLabelInfo(item.author),
|
||||
moderation: moderateProfile(
|
||||
item.author,
|
||||
store.preferences.moderationOpts,
|
||||
),
|
||||
},
|
||||
...(item.additional?.map(({author}) => {
|
||||
|
@ -111,10 +110,7 @@ export const FeedItem = observer(function ({
|
|||
handle: author.handle,
|
||||
displayName: author.displayName,
|
||||
avatar: author.avatar,
|
||||
moderation: getProfileModeration(
|
||||
store,
|
||||
getProfileViewBasicLabelInfo(author),
|
||||
),
|
||||
moderation: moderateProfile(author, store.preferences.moderationOpts),
|
||||
}
|
||||
}) || []),
|
||||
]
|
||||
|
|
|
@ -26,7 +26,7 @@ import {PostEmbeds} from '../util/post-embeds'
|
|||
import {PostCtrls} from '../util/post-ctrls/PostCtrls'
|
||||
import {PostHider} from '../util/moderation/PostHider'
|
||||
import {ContentHider} from '../util/moderation/ContentHider'
|
||||
import {ImageHider} from '../util/moderation/ImageHider'
|
||||
import {PostAlerts} from '../util/moderation/PostAlerts'
|
||||
import {PostSandboxWarning} from '../util/PostSandboxWarning'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
|
@ -159,10 +159,9 @@ export const PostThreadItem = observer(function PostThreadItem({
|
|||
|
||||
if (item._isHighlightedPost) {
|
||||
return (
|
||||
<PostHider
|
||||
<Link
|
||||
testID={`postThreadItem-by-${item.post.author.handle}`}
|
||||
style={[styles.outer, styles.outerHighlighted, pal.border, pal.view]}
|
||||
moderation={item.moderation.thread}>
|
||||
style={[styles.outer, styles.outerHighlighted, pal.border, pal.view]}>
|
||||
<PostSandboxWarning />
|
||||
<View style={styles.layout}>
|
||||
<View style={styles.layoutAvi}>
|
||||
|
@ -227,7 +226,16 @@ export const PostThreadItem = observer(function PostThreadItem({
|
|||
</View>
|
||||
</View>
|
||||
<View style={[s.pl10, s.pr10, s.pb10]}>
|
||||
<ContentHider moderation={item.moderation.view}>
|
||||
<ContentHider
|
||||
moderation={item.moderation.content}
|
||||
ignoreMute
|
||||
style={styles.contentHider}
|
||||
childContainerStyle={styles.contentHiderChild}>
|
||||
<PostAlerts
|
||||
moderation={item.moderation.content}
|
||||
includeMute
|
||||
style={styles.alert}
|
||||
/>
|
||||
{item.richText?.text ? (
|
||||
<View
|
||||
style={[
|
||||
|
@ -242,9 +250,11 @@ export const PostThreadItem = observer(function PostThreadItem({
|
|||
/>
|
||||
</View>
|
||||
) : undefined}
|
||||
<ImageHider moderation={item.moderation.view} style={s.mb10}>
|
||||
<PostEmbeds embed={item.post.embed} style={s.mb10} />
|
||||
</ImageHider>
|
||||
{item.post.embed && (
|
||||
<ContentHider moderation={item.moderation.embed} style={s.mb10}>
|
||||
<PostEmbeds embed={item.post.embed} style={s.mb10} />
|
||||
</ContentHider>
|
||||
)}
|
||||
</ContentHider>
|
||||
<ExpandedPostDetails
|
||||
post={item.post}
|
||||
|
@ -311,7 +321,7 @@ export const PostThreadItem = observer(function PostThreadItem({
|
|||
/>
|
||||
</View>
|
||||
</View>
|
||||
</PostHider>
|
||||
</Link>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
|
@ -325,7 +335,7 @@ export const PostThreadItem = observer(function PostThreadItem({
|
|||
pal.view,
|
||||
item._showParentReplyLine && styles.noTopBorder,
|
||||
]}
|
||||
moderation={item.moderation.thread}>
|
||||
moderation={item.moderation.content}>
|
||||
{item._showParentReplyLine && (
|
||||
<View
|
||||
style={[
|
||||
|
@ -360,32 +370,34 @@ export const PostThreadItem = observer(function PostThreadItem({
|
|||
timestamp={item.post.indexedAt}
|
||||
postHref={itemHref}
|
||||
/>
|
||||
<ContentHider
|
||||
moderation={item.moderation.thread}
|
||||
containerStyle={styles.contentHider}>
|
||||
{item.richText?.text ? (
|
||||
<View style={styles.postTextContainer}>
|
||||
<RichText
|
||||
type="post-text"
|
||||
richText={item.richText}
|
||||
style={[pal.text, s.flex1]}
|
||||
lineHeight={1.3}
|
||||
/>
|
||||
</View>
|
||||
) : undefined}
|
||||
<ImageHider style={s.mb10} moderation={item.moderation.thread}>
|
||||
<PostAlerts
|
||||
moderation={item.moderation.content}
|
||||
style={styles.alert}
|
||||
/>
|
||||
{item.richText?.text ? (
|
||||
<View style={styles.postTextContainer}>
|
||||
<RichText
|
||||
type="post-text"
|
||||
richText={item.richText}
|
||||
style={[pal.text, s.flex1]}
|
||||
lineHeight={1.3}
|
||||
/>
|
||||
</View>
|
||||
) : undefined}
|
||||
{item.post.embed && (
|
||||
<ContentHider style={s.mb10} moderation={item.moderation.embed}>
|
||||
<PostEmbeds embed={item.post.embed} style={s.mb10} />
|
||||
</ImageHider>
|
||||
{needsTranslation && (
|
||||
<View style={[pal.borderDark, styles.translateLink]}>
|
||||
<Link href={translatorUrl} title="Translate">
|
||||
<Text type="sm" style={pal.link}>
|
||||
Translate this post
|
||||
</Text>
|
||||
</Link>
|
||||
</View>
|
||||
)}
|
||||
</ContentHider>
|
||||
</ContentHider>
|
||||
)}
|
||||
{needsTranslation && (
|
||||
<View style={[pal.borderDark, styles.translateLink]}>
|
||||
<Link href={translatorUrl} title="Translate">
|
||||
<Text type="sm" style={pal.link}>
|
||||
Translate this post
|
||||
</Text>
|
||||
</Link>
|
||||
</View>
|
||||
)}
|
||||
<PostCtrls
|
||||
itemUri={itemUri}
|
||||
itemCid={itemCid}
|
||||
|
@ -515,6 +527,9 @@ const styles = StyleSheet.create({
|
|||
paddingRight: 5,
|
||||
maxWidth: 240,
|
||||
},
|
||||
alert: {
|
||||
marginBottom: 6,
|
||||
},
|
||||
postTextContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
@ -531,7 +546,10 @@ const styles = StyleSheet.create({
|
|||
marginBottom: 6,
|
||||
},
|
||||
contentHider: {
|
||||
marginTop: 4,
|
||||
marginBottom: 6,
|
||||
},
|
||||
contentHiderChild: {
|
||||
marginTop: 6,
|
||||
},
|
||||
expandedInfo: {
|
||||
flexDirection: 'row',
|
||||
|
|
|
@ -19,9 +19,8 @@ import {UserInfoText} from '../util/UserInfoText'
|
|||
import {PostMeta} from '../util/PostMeta'
|
||||
import {PostEmbeds} from '../util/post-embeds'
|
||||
import {PostCtrls} from '../util/post-ctrls/PostCtrls'
|
||||
import {PostHider} from '../util/moderation/PostHider'
|
||||
import {ContentHider} from '../util/moderation/ContentHider'
|
||||
import {ImageHider} from '../util/moderation/ImageHider'
|
||||
import {PostAlerts} from '../util/moderation/PostAlerts'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {RichText} from '../util/text/RichText'
|
||||
import * as Toast from '../util/Toast'
|
||||
|
@ -206,10 +205,7 @@ const PostLoaded = observer(
|
|||
}, [item, setDeleted, store])
|
||||
|
||||
return (
|
||||
<PostHider
|
||||
href={itemHref}
|
||||
style={[styles.outer, pal.view, pal.border, style]}
|
||||
moderation={item.moderation.list}>
|
||||
<Link href={itemHref} style={[styles.outer, pal.view, pal.border, style]}>
|
||||
{showReplyLine && <View style={styles.replyLine} />}
|
||||
<View style={styles.layout}>
|
||||
<View style={styles.layoutAvi}>
|
||||
|
@ -251,8 +247,13 @@ const PostLoaded = observer(
|
|||
</View>
|
||||
)}
|
||||
<ContentHider
|
||||
moderation={item.moderation.list}
|
||||
containerStyle={styles.contentHider}>
|
||||
moderation={item.moderation.content}
|
||||
style={styles.contentHider}
|
||||
childContainerStyle={styles.contentHiderChild}>
|
||||
<PostAlerts
|
||||
moderation={item.moderation.content}
|
||||
style={styles.alert}
|
||||
/>
|
||||
{item.richText?.text ? (
|
||||
<View style={styles.postTextContainer}>
|
||||
<RichText
|
||||
|
@ -264,9 +265,9 @@ const PostLoaded = observer(
|
|||
/>
|
||||
</View>
|
||||
) : undefined}
|
||||
<ImageHider moderation={item.moderation.list} style={s.mb10}>
|
||||
<ContentHider moderation={item.moderation.embed} style={s.mb10}>
|
||||
<PostEmbeds embed={item.post.embed} style={s.mb10} />
|
||||
</ImageHider>
|
||||
</ContentHider>
|
||||
{needsTranslation && (
|
||||
<View style={[pal.borderDark, styles.translateLink]}>
|
||||
<Link href={translatorUrl} title="Translate">
|
||||
|
@ -302,7 +303,7 @@ const PostLoaded = observer(
|
|||
/>
|
||||
</View>
|
||||
</View>
|
||||
</PostHider>
|
||||
</Link>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
@ -323,6 +324,9 @@ const styles = StyleSheet.create({
|
|||
layoutContent: {
|
||||
flex: 1,
|
||||
},
|
||||
alert: {
|
||||
marginBottom: 6,
|
||||
},
|
||||
postTextContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
@ -341,6 +345,9 @@ const styles = StyleSheet.create({
|
|||
borderLeftColor: colors.gray2,
|
||||
},
|
||||
contentHider: {
|
||||
marginTop: 4,
|
||||
marginBottom: 6,
|
||||
},
|
||||
contentHiderChild: {
|
||||
marginTop: 6,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -8,16 +8,14 @@ import {
|
|||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {PostsFeedItemModel} from 'state/models/feeds/post'
|
||||
import {ModerationBehaviorCode} from 'lib/labeling/types'
|
||||
import {Link, DesktopWebTextLink} from '../util/Link'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {UserInfoText} from '../util/UserInfoText'
|
||||
import {PostMeta} from '../util/PostMeta'
|
||||
import {PostCtrls} from '../util/post-ctrls/PostCtrls'
|
||||
import {PostEmbeds} from '../util/post-embeds'
|
||||
import {PostHider} from '../util/moderation/PostHider'
|
||||
import {ContentHider} from '../util/moderation/ContentHider'
|
||||
import {ImageHider} from '../util/moderation/ImageHider'
|
||||
import {PostAlerts} from '../util/moderation/PostAlerts'
|
||||
import {RichText} from '../util/text/RichText'
|
||||
import {PostSandboxWarning} from '../util/PostSandboxWarning'
|
||||
import * as Toast from '../util/Toast'
|
||||
|
@ -35,13 +33,11 @@ export const FeedItem = observer(function ({
|
|||
item,
|
||||
isThreadChild,
|
||||
isThreadParent,
|
||||
ignoreMuteFor,
|
||||
}: {
|
||||
item: PostsFeedItemModel
|
||||
isThreadChild?: boolean
|
||||
isThreadParent?: boolean
|
||||
showReplyLine?: boolean
|
||||
ignoreMuteFor?: string
|
||||
}) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
|
@ -147,26 +143,17 @@ export const FeedItem = observer(function ({
|
|||
isThreadParent ? styles.outerNoBottom : undefined,
|
||||
]
|
||||
|
||||
// moderation override
|
||||
let moderation = item.moderation.list
|
||||
if (
|
||||
ignoreMuteFor === item.post.author.did &&
|
||||
moderation.isMute &&
|
||||
!moderation.noOverride
|
||||
) {
|
||||
moderation = {behavior: ModerationBehaviorCode.Show}
|
||||
}
|
||||
|
||||
if (!record || deleted) {
|
||||
return <View />
|
||||
}
|
||||
|
||||
return (
|
||||
<PostHider
|
||||
<Link
|
||||
testID={`feedItem-by-${item.post.author.handle}`}
|
||||
style={outerStyles}
|
||||
href={itemHref}
|
||||
moderation={moderation}>
|
||||
noFeedback
|
||||
accessible={false}>
|
||||
{isThreadChild && (
|
||||
<View
|
||||
style={[styles.topReplyLine, {borderColor: pal.colors.replyLine}]}
|
||||
|
@ -255,8 +242,14 @@ export const FeedItem = observer(function ({
|
|||
</View>
|
||||
)}
|
||||
<ContentHider
|
||||
moderation={moderation}
|
||||
containerStyle={styles.contentHider}>
|
||||
moderation={item.moderation.content}
|
||||
ignoreMute
|
||||
style={styles.contentHider}
|
||||
childContainerStyle={styles.contentHiderChild}>
|
||||
<PostAlerts
|
||||
moderation={item.moderation.content}
|
||||
style={styles.alert}
|
||||
/>
|
||||
{item.richText?.text ? (
|
||||
<View style={styles.postTextContainer}>
|
||||
<RichText
|
||||
|
@ -267,9 +260,11 @@ export const FeedItem = observer(function ({
|
|||
/>
|
||||
</View>
|
||||
) : undefined}
|
||||
<ImageHider moderation={item.moderation.list} style={styles.embed}>
|
||||
<ContentHider
|
||||
moderation={item.moderation.embed}
|
||||
style={styles.embed}>
|
||||
<PostEmbeds embed={item.post.embed} style={styles.embed} />
|
||||
</ImageHider>
|
||||
</ContentHider>
|
||||
{needsTranslation && (
|
||||
<View style={[pal.borderDark, styles.translateLink]}>
|
||||
<Link href={translatorUrl} title="Translate">
|
||||
|
@ -306,7 +301,7 @@ export const FeedItem = observer(function ({
|
|||
/>
|
||||
</View>
|
||||
</View>
|
||||
</PostHider>
|
||||
</Link>
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -358,6 +353,10 @@ const styles = StyleSheet.create({
|
|||
layoutContent: {
|
||||
flex: 1,
|
||||
},
|
||||
alert: {
|
||||
marginTop: 6,
|
||||
marginBottom: 6,
|
||||
},
|
||||
postTextContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
@ -365,7 +364,10 @@ const styles = StyleSheet.create({
|
|||
paddingBottom: 4,
|
||||
},
|
||||
contentHider: {
|
||||
marginTop: 4,
|
||||
marginBottom: 6,
|
||||
},
|
||||
contentHiderChild: {
|
||||
marginTop: 6,
|
||||
},
|
||||
embed: {
|
||||
marginBottom: 6,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {PostsFeedSliceModel} from 'state/models/feeds/posts-slice'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {Link} from '../util/Link'
|
||||
|
@ -7,65 +8,61 @@ import {Text} from '../util/text/Text'
|
|||
import Svg, {Circle, Line} from 'react-native-svg'
|
||||
import {FeedItem} from './FeedItem'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {ModerationBehaviorCode} from 'lib/labeling/types'
|
||||
import {makeProfileLink} from 'lib/routes/links'
|
||||
|
||||
export function FeedSlice({
|
||||
slice,
|
||||
ignoreMuteFor,
|
||||
}: {
|
||||
slice: PostsFeedSliceModel
|
||||
ignoreMuteFor?: string
|
||||
}) {
|
||||
if (slice.moderation.list.behavior === ModerationBehaviorCode.Hide) {
|
||||
if (!ignoreMuteFor && !slice.moderation.list.noOverride) {
|
||||
export const FeedSlice = observer(
|
||||
({
|
||||
slice,
|
||||
ignoreFilterFor,
|
||||
}: {
|
||||
slice: PostsFeedSliceModel
|
||||
ignoreFilterFor?: string
|
||||
}) => {
|
||||
if (slice.shouldFilter(ignoreFilterFor)) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
if (slice.isThread && slice.items.length > 3) {
|
||||
const last = slice.items.length - 1
|
||||
|
||||
if (slice.isThread && slice.items.length > 3) {
|
||||
const last = slice.items.length - 1
|
||||
return (
|
||||
<>
|
||||
<FeedItem
|
||||
key={slice.items[0]._reactKey}
|
||||
item={slice.items[0]}
|
||||
isThreadParent={slice.isThreadParentAt(0)}
|
||||
isThreadChild={slice.isThreadChildAt(0)}
|
||||
/>
|
||||
<FeedItem
|
||||
key={slice.items[1]._reactKey}
|
||||
item={slice.items[1]}
|
||||
isThreadParent={slice.isThreadParentAt(1)}
|
||||
isThreadChild={slice.isThreadChildAt(1)}
|
||||
/>
|
||||
<ViewFullThread slice={slice} />
|
||||
<FeedItem
|
||||
key={slice.items[last]._reactKey}
|
||||
item={slice.items[last]}
|
||||
isThreadParent={slice.isThreadParentAt(last)}
|
||||
isThreadChild={slice.isThreadChildAt(last)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FeedItem
|
||||
key={slice.items[0]._reactKey}
|
||||
item={slice.items[0]}
|
||||
isThreadParent={slice.isThreadParentAt(0)}
|
||||
isThreadChild={slice.isThreadChildAt(0)}
|
||||
ignoreMuteFor={ignoreMuteFor}
|
||||
/>
|
||||
<FeedItem
|
||||
key={slice.items[1]._reactKey}
|
||||
item={slice.items[1]}
|
||||
isThreadParent={slice.isThreadParentAt(1)}
|
||||
isThreadChild={slice.isThreadChildAt(1)}
|
||||
ignoreMuteFor={ignoreMuteFor}
|
||||
/>
|
||||
<ViewFullThread slice={slice} />
|
||||
<FeedItem
|
||||
key={slice.items[last]._reactKey}
|
||||
item={slice.items[last]}
|
||||
isThreadParent={slice.isThreadParentAt(last)}
|
||||
isThreadChild={slice.isThreadChildAt(last)}
|
||||
ignoreMuteFor={ignoreMuteFor}
|
||||
/>
|
||||
{slice.items.map((item, i) => (
|
||||
<FeedItem
|
||||
key={item._reactKey}
|
||||
item={item}
|
||||
isThreadParent={slice.isThreadParentAt(i)}
|
||||
isThreadChild={slice.isThreadChildAt(i)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{slice.items.map((item, i) => (
|
||||
<FeedItem
|
||||
key={item._reactKey}
|
||||
item={item}
|
||||
isThreadParent={slice.isThreadParentAt(i)}
|
||||
isThreadChild={slice.isThreadChildAt(i)}
|
||||
ignoreMuteFor={ignoreMuteFor}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
function ViewFullThread({slice}: {slice: PostsFeedSliceModel}) {
|
||||
const pal = usePalette('default')
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import * as React from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {AppBskyActorDefs} from '@atproto/api'
|
||||
import {
|
||||
AppBskyActorDefs,
|
||||
moderateProfile,
|
||||
ProfileModeration,
|
||||
} from '@atproto/api'
|
||||
import {Link} from '../util/Link'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
|
@ -11,12 +15,11 @@ import {useStores} from 'state/index'
|
|||
import {FollowButton} from './FollowButton'
|
||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||
import {sanitizeHandle} from 'lib/strings/handles'
|
||||
import {
|
||||
getProfileViewBasicLabelInfo,
|
||||
getProfileModeration,
|
||||
} from 'lib/labeling/helpers'
|
||||
import {ModerationBehaviorCode} from 'lib/labeling/types'
|
||||
import {makeProfileLink} from 'lib/routes/links'
|
||||
import {
|
||||
describeModerationCause,
|
||||
getProfileModerationCauses,
|
||||
} from 'lib/moderation'
|
||||
|
||||
export const ProfileCard = observer(
|
||||
({
|
||||
|
@ -25,7 +28,6 @@ export const ProfileCard = observer(
|
|||
noBg,
|
||||
noBorder,
|
||||
followers,
|
||||
overrideModeration,
|
||||
renderButton,
|
||||
}: {
|
||||
testID?: string
|
||||
|
@ -33,7 +35,6 @@ export const ProfileCard = observer(
|
|||
noBg?: boolean
|
||||
noBorder?: boolean
|
||||
followers?: AppBskyActorDefs.ProfileView[] | undefined
|
||||
overrideModeration?: boolean
|
||||
renderButton?: (
|
||||
profile: AppBskyActorDefs.ProfileViewBasic,
|
||||
) => React.ReactNode
|
||||
|
@ -41,18 +42,11 @@ export const ProfileCard = observer(
|
|||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
|
||||
const moderation = getProfileModeration(
|
||||
store,
|
||||
getProfileViewBasicLabelInfo(profile),
|
||||
const moderation = moderateProfile(
|
||||
profile,
|
||||
store.preferences.moderationOpts,
|
||||
)
|
||||
|
||||
if (
|
||||
moderation.list.behavior === ModerationBehaviorCode.Hide &&
|
||||
!overrideModeration
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
testID={testID}
|
||||
|
@ -82,20 +76,17 @@ export const ProfileCard = observer(
|
|||
lineHeight={1.2}>
|
||||
{sanitizeDisplayName(
|
||||
profile.displayName || sanitizeHandle(profile.handle),
|
||||
moderation.profile,
|
||||
)}
|
||||
</Text>
|
||||
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
|
||||
{sanitizeHandle(profile.handle, '@')}
|
||||
</Text>
|
||||
{!!profile.viewer?.followedBy && (
|
||||
<View style={s.flexRow}>
|
||||
<View style={[s.mt5, pal.btn, styles.pill]}>
|
||||
<Text type="xs" style={pal.text}>
|
||||
Follows You
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
<ProfileCardPills
|
||||
followedBy={!!profile.viewer?.followedBy}
|
||||
moderation={moderation}
|
||||
/>
|
||||
{!!profile.viewer?.followedBy && <View style={s.flexRow} />}
|
||||
</View>
|
||||
{renderButton ? (
|
||||
<View style={styles.layoutButton}>{renderButton(profile)}</View>
|
||||
|
@ -114,6 +105,44 @@ export const ProfileCard = observer(
|
|||
},
|
||||
)
|
||||
|
||||
function ProfileCardPills({
|
||||
followedBy,
|
||||
moderation,
|
||||
}: {
|
||||
followedBy: boolean
|
||||
moderation: ProfileModeration
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
|
||||
const causes = getProfileModerationCauses(moderation)
|
||||
if (!followedBy && !causes.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.pills}>
|
||||
{followedBy && (
|
||||
<View style={[s.mt5, pal.btn, styles.pill]}>
|
||||
<Text type="xs" style={pal.text}>
|
||||
Follows You
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{causes.map(cause => {
|
||||
const desc = describeModerationCause(cause, 'account')
|
||||
return (
|
||||
<View style={[s.mt5, pal.btn, styles.pill]}>
|
||||
<Text type="xs" style={pal.text}>
|
||||
{cause?.type === 'label' ? '⚠' : ''}
|
||||
{desc.name}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const FollowersList = observer(
|
||||
({followers}: {followers?: AppBskyActorDefs.ProfileView[] | undefined}) => {
|
||||
const store = useStores()
|
||||
|
@ -125,9 +154,9 @@ const FollowersList = observer(
|
|||
const followersWithMods = followers
|
||||
.map(f => ({
|
||||
f,
|
||||
mod: getProfileModeration(store, getProfileViewBasicLabelInfo(f)),
|
||||
mod: moderateProfile(f, store.preferences.moderationOpts),
|
||||
}))
|
||||
.filter(({mod}) => mod.list.behavior !== ModerationBehaviorCode.Hide)
|
||||
.filter(({mod}) => !mod.account.filter)
|
||||
|
||||
return (
|
||||
<View style={styles.followedBy}>
|
||||
|
@ -218,6 +247,12 @@ const styles = StyleSheet.create({
|
|||
paddingRight: 10,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
pills: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
columnGap: 6,
|
||||
rowGap: 2,
|
||||
},
|
||||
pill: {
|
||||
borderRadius: 4,
|
||||
paddingHorizontal: 6,
|
||||
|
|
|
@ -21,15 +21,13 @@ import * as Toast from '../util/Toast'
|
|||
import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {ThemedText} from '../util/text/ThemedText'
|
||||
import {TextLink} from '../util/Link'
|
||||
import {RichText} from '../util/text/RichText'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
import {UserBanner} from '../util/UserBanner'
|
||||
import {ProfileHeaderWarnings} from '../util/moderation/ProfileHeaderWarnings'
|
||||
import {ProfileHeaderAlerts} from '../util/moderation/ProfileHeaderAlerts'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {listUriToHref} from 'lib/strings/url-helpers'
|
||||
import {isDesktopWeb, isNative} from 'platform/detection'
|
||||
import {FollowState} from 'state/models/cache/my-follows'
|
||||
import {shareUrl} from 'lib/sharing'
|
||||
|
@ -116,7 +114,10 @@ const ProfileHeaderLoaded = observer(
|
|||
}, [navigation])
|
||||
|
||||
const onPressAvi = React.useCallback(() => {
|
||||
if (view.avatar) {
|
||||
if (
|
||||
view.avatar &&
|
||||
!(view.moderation.avatar.blur && view.moderation.avatar.noOverride)
|
||||
) {
|
||||
store.shell.openLightbox(new ProfileImageLightbox(view))
|
||||
}
|
||||
}, [store, view])
|
||||
|
@ -434,6 +435,7 @@ const ProfileHeaderLoaded = observer(
|
|||
style={[pal.text, styles.title]}>
|
||||
{sanitizeDisplayName(
|
||||
view.displayName || sanitizeHandle(view.handle),
|
||||
view.moderation.profile,
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
|
@ -494,7 +496,9 @@ const ProfileHeaderLoaded = observer(
|
|||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
{view.descriptionRichText ? (
|
||||
{view.description &&
|
||||
view.descriptionRichText &&
|
||||
!view.moderation.profile.blur ? (
|
||||
<RichText
|
||||
testID="profileHeaderDescription"
|
||||
style={[styles.description, pal.text]}
|
||||
|
@ -504,52 +508,7 @@ const ProfileHeaderLoaded = observer(
|
|||
) : undefined}
|
||||
</>
|
||||
)}
|
||||
<ProfileHeaderWarnings moderation={view.moderation.view} />
|
||||
<View style={styles.moderationLines}>
|
||||
{view.viewer.blocking ? (
|
||||
<View
|
||||
testID="profileHeaderBlockedNotice"
|
||||
style={[styles.moderationNotice, pal.viewLight]}>
|
||||
<FontAwesomeIcon icon="ban" style={[pal.text]} />
|
||||
<Text type="lg-medium" style={pal.text}>
|
||||
Account blocked
|
||||
</Text>
|
||||
</View>
|
||||
) : view.viewer.muted ? (
|
||||
<View
|
||||
testID="profileHeaderMutedNotice"
|
||||
style={[styles.moderationNotice, pal.viewLight]}>
|
||||
<FontAwesomeIcon
|
||||
icon={['far', 'eye-slash']}
|
||||
style={[pal.text]}
|
||||
/>
|
||||
<Text type="lg-medium" style={pal.text}>
|
||||
Account muted{' '}
|
||||
{view.viewer.mutedByList && (
|
||||
<Text type="lg-medium" style={pal.text}>
|
||||
by{' '}
|
||||
<TextLink
|
||||
type="lg-medium"
|
||||
style={pal.link}
|
||||
href={listUriToHref(view.viewer.mutedByList.uri)}
|
||||
text={view.viewer.mutedByList.name}
|
||||
/>
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
) : undefined}
|
||||
{view.viewer.blockedBy && (
|
||||
<View
|
||||
testID="profileHeaderBlockedNotice"
|
||||
style={[styles.moderationNotice, pal.viewLight]}>
|
||||
<FontAwesomeIcon icon="ban" style={[pal.text]} />
|
||||
<Text type="lg-medium" style={pal.text}>
|
||||
This account has blocked you
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<ProfileHeaderAlerts moderation={view.moderation} />
|
||||
</View>
|
||||
{!isDesktopWeb && !hideBackButton && (
|
||||
<TouchableWithoutFeedback
|
||||
|
@ -693,19 +652,6 @@ const styles = StyleSheet.create({
|
|||
paddingVertical: 2,
|
||||
},
|
||||
|
||||
moderationLines: {
|
||||
gap: 6,
|
||||
},
|
||||
|
||||
moderationNotice: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
gap: 8,
|
||||
},
|
||||
|
||||
br40: {borderRadius: 40},
|
||||
br50: {borderRadius: 50},
|
||||
})
|
||||
|
|
|
@ -3,6 +3,7 @@ import {StyleSheet, View} from 'react-native'
|
|||
import Svg, {Circle, Rect, Path} from 'react-native-svg'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {HighPriorityImage} from 'view/com/util/images/Image'
|
||||
import {ModerationUI} from '@atproto/api'
|
||||
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
|
||||
import {
|
||||
usePhotoLibraryPermission,
|
||||
|
@ -13,7 +14,6 @@ import {colors} from 'lib/styles'
|
|||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {isWeb, isAndroid} from 'platform/detection'
|
||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||
import {AvatarModeration} from 'lib/labeling/types'
|
||||
import {UserPreviewLink} from './UserPreviewLink'
|
||||
import {DropdownItem, NativeDropdown} from './forms/NativeDropdown'
|
||||
|
||||
|
@ -23,7 +23,7 @@ interface BaseUserAvatarProps {
|
|||
type?: Type
|
||||
size: number
|
||||
avatar?: string | null
|
||||
moderation?: AvatarModeration
|
||||
moderation?: ModerationUI
|
||||
}
|
||||
|
||||
interface UserAvatarProps extends BaseUserAvatarProps {
|
||||
|
@ -213,20 +213,20 @@ export function UserAvatar({
|
|||
],
|
||||
)
|
||||
|
||||
const warning = useMemo(() => {
|
||||
if (!moderation?.warn) {
|
||||
const alert = useMemo(() => {
|
||||
if (!moderation?.alert) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<View style={[styles.warningIconContainer, pal.view]}>
|
||||
<View style={[styles.alertIconContainer, pal.view]}>
|
||||
<FontAwesomeIcon
|
||||
icon="exclamation-circle"
|
||||
style={styles.warningIcon}
|
||||
style={styles.alertIcon}
|
||||
size={Math.floor(size / 3)}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}, [moderation?.warn, size, pal])
|
||||
}, [moderation?.alert, size, pal])
|
||||
|
||||
// onSelectNewAvatar is only passed as prop on the EditProfile component
|
||||
return onSelectNewAvatar ? (
|
||||
|
@ -259,12 +259,12 @@ export function UserAvatar({
|
|||
source={{uri: avatar}}
|
||||
blurRadius={moderation?.blur ? BLUR_AMOUNT : 0}
|
||||
/>
|
||||
{warning}
|
||||
{alert}
|
||||
</View>
|
||||
) : (
|
||||
<View style={{width: size, height: size}}>
|
||||
<DefaultAvatar type={type} size={size} />
|
||||
{warning}
|
||||
{alert}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
@ -289,13 +289,13 @@ const styles = StyleSheet.create({
|
|||
justifyContent: 'center',
|
||||
backgroundColor: colors.gray5,
|
||||
},
|
||||
warningIconContainer: {
|
||||
alertIconContainer: {
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
borderRadius: 100,
|
||||
},
|
||||
warningIcon: {
|
||||
alertIcon: {
|
||||
color: colors.red3,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, {useMemo} from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {ModerationUI} from '@atproto/api'
|
||||
import {Image} from 'expo-image'
|
||||
import {colors} from 'lib/styles'
|
||||
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
|
||||
|
@ -10,7 +11,6 @@ import {
|
|||
useCameraPermission,
|
||||
} from 'lib/hooks/usePermissions'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {AvatarModeration} from 'lib/labeling/types'
|
||||
import {isWeb, isAndroid} from 'platform/detection'
|
||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||
import {NativeDropdown, DropdownItem} from './forms/NativeDropdown'
|
||||
|
@ -21,7 +21,7 @@ export function UserBanner({
|
|||
onSelectNewBanner,
|
||||
}: {
|
||||
banner?: string | null
|
||||
moderation?: AvatarModeration
|
||||
moderation?: ModerationUI
|
||||
onSelectNewBanner?: (img: RNImage | null) => void
|
||||
}) {
|
||||
const store = useStores()
|
||||
|
|
|
@ -1,36 +1,32 @@
|
|||
import React from 'react'
|
||||
import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {ModerationUI} from '@atproto/api'
|
||||
import {Text} from '../text/Text'
|
||||
import {addStyle} from 'lib/styles'
|
||||
import {ModerationBehavior, ModerationBehaviorCode} from 'lib/labeling/types'
|
||||
import {InfoCircleIcon} from 'lib/icons'
|
||||
import {describeModerationCause} from 'lib/moderation'
|
||||
import {useStores} from 'state/index'
|
||||
import {isDesktopWeb} from 'platform/detection'
|
||||
|
||||
export function ContentHider({
|
||||
testID,
|
||||
moderation,
|
||||
ignoreMute,
|
||||
style,
|
||||
containerStyle,
|
||||
childContainerStyle,
|
||||
children,
|
||||
}: React.PropsWithChildren<{
|
||||
testID?: string
|
||||
moderation: ModerationBehavior
|
||||
moderation: ModerationUI
|
||||
ignoreMute?: boolean
|
||||
style?: StyleProp<ViewStyle>
|
||||
containerStyle?: StyleProp<ViewStyle>
|
||||
childContainerStyle?: StyleProp<ViewStyle>
|
||||
}>) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
const [override, setOverride] = React.useState(false)
|
||||
const onPressShow = React.useCallback(() => {
|
||||
setOverride(true)
|
||||
}, [setOverride])
|
||||
const onPressHide = React.useCallback(() => {
|
||||
setOverride(false)
|
||||
}, [setOverride])
|
||||
|
||||
if (
|
||||
moderation.behavior === ModerationBehaviorCode.Show ||
|
||||
moderation.behavior === ModerationBehaviorCode.Warn ||
|
||||
moderation.behavior === ModerationBehaviorCode.WarnImages
|
||||
) {
|
||||
if (!moderation.blur || (ignoreMute && moderation.cause?.type === 'muted')) {
|
||||
return (
|
||||
<View testID={testID} style={style}>
|
||||
{children}
|
||||
|
@ -38,73 +34,61 @@ export function ContentHider({
|
|||
)
|
||||
}
|
||||
|
||||
if (moderation.behavior === ModerationBehaviorCode.Hide) {
|
||||
return null
|
||||
}
|
||||
|
||||
const desc = describeModerationCause(moderation.cause, 'content')
|
||||
return (
|
||||
<View style={[styles.container, pal.view, pal.border, containerStyle]}>
|
||||
<View testID={testID} style={style}>
|
||||
<Pressable
|
||||
onPress={override ? onPressHide : onPressShow}
|
||||
accessibilityLabel={override ? 'Hide post' : 'Show post'}
|
||||
// TODO: The text labelling should be split up so controls have unique roles
|
||||
accessibilityHint={
|
||||
override
|
||||
? 'Re-hide post'
|
||||
: 'Shows post hidden based on your moderation settings'
|
||||
}
|
||||
style={[
|
||||
styles.description,
|
||||
pal.viewLight,
|
||||
override && styles.descriptionOpen,
|
||||
]}>
|
||||
<Text type="md" style={pal.textLight}>
|
||||
{moderation.reason || 'Content warning'}
|
||||
onPress={() => {
|
||||
if (!moderation.noOverride) {
|
||||
setOverride(v => !v)
|
||||
}
|
||||
}}
|
||||
accessibilityRole="button"
|
||||
accessibilityHint={override ? 'Hide the content' : 'Show the content'}
|
||||
accessibilityLabel=""
|
||||
style={[styles.cover, pal.viewLight]}>
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
store.shell.openModal({
|
||||
name: 'moderation-details',
|
||||
context: 'content',
|
||||
moderation,
|
||||
})
|
||||
}}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Learn more about this warning"
|
||||
accessibilityHint="">
|
||||
<InfoCircleIcon size={18} style={pal.text} />
|
||||
</Pressable>
|
||||
<Text type="lg" style={pal.text}>
|
||||
{desc.name}
|
||||
</Text>
|
||||
<View style={styles.showBtn}>
|
||||
<Text type="md-medium" style={pal.link}>
|
||||
{override ? 'Hide' : 'Show'}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
{override && (
|
||||
<View style={[styles.childrenContainer, pal.border]}>
|
||||
<View testID={testID} style={addStyle(style, styles.child)}>
|
||||
{children}
|
||||
{!moderation.noOverride && (
|
||||
<View style={styles.showBtn}>
|
||||
<Text type="xl" style={pal.link}>
|
||||
{override ? 'Hide' : 'Show'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
)}
|
||||
</Pressable>
|
||||
{override && <View style={childContainerStyle}>{children}</View>}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginBottom: 10,
|
||||
borderWidth: 1,
|
||||
borderRadius: 12,
|
||||
},
|
||||
description: {
|
||||
cover: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
borderRadius: 8,
|
||||
marginTop: 4,
|
||||
paddingVertical: 14,
|
||||
paddingLeft: 14,
|
||||
paddingRight: 18,
|
||||
borderRadius: 12,
|
||||
},
|
||||
descriptionOpen: {
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
},
|
||||
icon: {
|
||||
marginRight: 10,
|
||||
paddingRight: isDesktopWeb ? 18 : 22,
|
||||
},
|
||||
showBtn: {
|
||||
marginLeft: 'auto',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
childrenContainer: {
|
||||
paddingHorizontal: 12,
|
||||
paddingTop: 8,
|
||||
},
|
||||
child: {},
|
||||
})
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
import React from 'react'
|
||||
import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {Text} from '../text/Text'
|
||||
import {ModerationBehavior, ModerationBehaviorCode} from 'lib/labeling/types'
|
||||
import {isDesktopWeb} from 'platform/detection'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
|
||||
|
||||
export function ImageHider({
|
||||
testID,
|
||||
moderation,
|
||||
style,
|
||||
children,
|
||||
}: React.PropsWithChildren<{
|
||||
testID?: string
|
||||
moderation: ModerationBehavior
|
||||
style?: StyleProp<ViewStyle>
|
||||
}>) {
|
||||
const pal = usePalette('default')
|
||||
const [override, setOverride] = React.useState(false)
|
||||
const onPressToggle = React.useCallback(() => {
|
||||
setOverride(v => !v)
|
||||
}, [setOverride])
|
||||
|
||||
if (moderation.behavior === ModerationBehaviorCode.Hide) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (moderation.behavior !== ModerationBehaviorCode.WarnImages) {
|
||||
return (
|
||||
<View testID={testID} style={style}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View testID={testID} style={style}>
|
||||
<View style={[styles.cover, pal.viewLight]}>
|
||||
<Pressable
|
||||
onPress={onPressToggle}
|
||||
style={[styles.toggleBtn]}
|
||||
accessibilityLabel="Show image"
|
||||
accessibilityHint="">
|
||||
<FontAwesomeIcon
|
||||
icon={override ? 'eye' : ['far', 'eye-slash']}
|
||||
size={24}
|
||||
style={pal.text as FontAwesomeIconStyle}
|
||||
/>
|
||||
<Text type="lg" style={pal.text}>
|
||||
{moderation.reason || 'Content warning'}
|
||||
</Text>
|
||||
<View style={styles.flex1} />
|
||||
<Text type="xl-bold" style={pal.link}>
|
||||
{override ? 'Hide' : 'Show'}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
{override && children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
cover: {
|
||||
borderRadius: 8,
|
||||
marginTop: 4,
|
||||
},
|
||||
toggleBtn: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: isDesktopWeb ? 24 : 20,
|
||||
paddingVertical: isDesktopWeb ? 20 : 18,
|
||||
},
|
||||
flex1: {
|
||||
flex: 1,
|
||||
},
|
||||
})
|
68
src/view/com/util/moderation/PostAlerts.tsx
Normal file
68
src/view/com/util/moderation/PostAlerts.tsx
Normal file
|
@ -0,0 +1,68 @@
|
|||
import React from 'react'
|
||||
import {Pressable, StyleProp, StyleSheet, ViewStyle} from 'react-native'
|
||||
import {ModerationUI} from '@atproto/api'
|
||||
import {Text} from '../text/Text'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {InfoCircleIcon} from 'lib/icons'
|
||||
import {describeModerationCause} from 'lib/moderation'
|
||||
import {useStores} from 'state/index'
|
||||
|
||||
export function PostAlerts({
|
||||
moderation,
|
||||
includeMute,
|
||||
style,
|
||||
}: {
|
||||
moderation: ModerationUI
|
||||
includeMute?: boolean
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
|
||||
const shouldAlert =
|
||||
!!moderation.cause &&
|
||||
(moderation.alert ||
|
||||
(includeMute && moderation.blur && moderation.cause?.type === 'muted'))
|
||||
if (!shouldAlert) {
|
||||
return null
|
||||
}
|
||||
|
||||
const desc = describeModerationCause(moderation.cause, 'content')
|
||||
return (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
store.shell.openModal({
|
||||
name: 'moderation-details',
|
||||
context: 'content',
|
||||
moderation,
|
||||
})
|
||||
}}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Learn more about this warning"
|
||||
accessibilityHint=""
|
||||
style={[styles.container, pal.viewLight, style]}>
|
||||
<InfoCircleIcon style={pal.text} size={18} />
|
||||
<Text type="lg" style={pal.text}>
|
||||
{desc.name}
|
||||
</Text>
|
||||
<Text type="lg" style={[pal.link, styles.learnMoreBtn]}>
|
||||
Learn More
|
||||
</Text>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
paddingVertical: 8,
|
||||
paddingLeft: 14,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 8,
|
||||
},
|
||||
learnMoreBtn: {
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
})
|
|
@ -1,17 +1,20 @@
|
|||
import React, {ComponentProps} from 'react'
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {StyleSheet, Pressable, View} from 'react-native'
|
||||
import {ModerationUI} from '@atproto/api'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {Link} from '../Link'
|
||||
import {Text} from '../text/Text'
|
||||
import {addStyle} from 'lib/styles'
|
||||
import {ModerationBehaviorCode, ModerationBehavior} from 'lib/labeling/types'
|
||||
import {describeModerationCause} from 'lib/moderation'
|
||||
import {InfoCircleIcon} from 'lib/icons'
|
||||
import {useStores} from 'state/index'
|
||||
import {isDesktopWeb} from 'platform/detection'
|
||||
|
||||
interface Props extends ComponentProps<typeof Link> {
|
||||
// testID?: string
|
||||
// href?: string
|
||||
// style: StyleProp<ViewStyle>
|
||||
moderation: ModerationBehavior
|
||||
moderation: ModerationUI
|
||||
}
|
||||
|
||||
export function PostHider({
|
||||
|
@ -22,60 +25,71 @@ export function PostHider({
|
|||
children,
|
||||
...props
|
||||
}: Props) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
const [override, setOverride] = React.useState(false)
|
||||
const bg = override ? pal.viewLight : pal.view
|
||||
|
||||
if (moderation.behavior === ModerationBehaviorCode.Hide) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (moderation.behavior === ModerationBehaviorCode.Warn) {
|
||||
if (!moderation.blur) {
|
||||
return (
|
||||
<>
|
||||
<View style={[styles.description, bg, pal.border]}>
|
||||
<FontAwesomeIcon
|
||||
icon={['far', 'eye-slash']}
|
||||
style={[styles.icon, pal.text]}
|
||||
/>
|
||||
<Text type="md" style={pal.textLight}>
|
||||
{moderation.reason || 'Content warning'}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.showBtn}
|
||||
onPress={() => setOverride(v => !v)}
|
||||
accessibilityRole="button">
|
||||
<Text type="md" style={pal.link}>
|
||||
{override ? 'Hide' : 'Show'} post
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{override && (
|
||||
<View style={[styles.childrenContainer, pal.border, bg]}>
|
||||
<Link
|
||||
testID={testID}
|
||||
style={addStyle(style, styles.child)}
|
||||
href={href}
|
||||
noFeedback>
|
||||
{children}
|
||||
</Link>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
<Link
|
||||
testID={testID}
|
||||
style={style}
|
||||
href={href}
|
||||
noFeedback
|
||||
accessible={false}
|
||||
{...props}>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
// NOTE: any further label enforcement should occur in ContentContainer
|
||||
const desc = describeModerationCause(moderation.cause, 'content')
|
||||
return (
|
||||
<Link
|
||||
testID={testID}
|
||||
style={style}
|
||||
href={href}
|
||||
noFeedback
|
||||
accessible={false}
|
||||
{...props}>
|
||||
{children}
|
||||
</Link>
|
||||
<>
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
if (!moderation.noOverride) {
|
||||
setOverride(v => !v)
|
||||
}
|
||||
}}
|
||||
accessibilityRole="button"
|
||||
accessibilityHint={override ? 'Hide the content' : 'Show the content'}
|
||||
accessibilityLabel=""
|
||||
style={[styles.description, pal.viewLight]}>
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
store.shell.openModal({
|
||||
name: 'moderation-details',
|
||||
context: 'content',
|
||||
moderation,
|
||||
})
|
||||
}}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Learn more about this warning"
|
||||
accessibilityHint="">
|
||||
<InfoCircleIcon size={18} style={pal.text} />
|
||||
</Pressable>
|
||||
<Text type="lg" style={pal.text}>
|
||||
{desc.name}
|
||||
</Text>
|
||||
{!moderation.noOverride && (
|
||||
<Text type="xl" style={[styles.showBtn, pal.link]}>
|
||||
{override ? 'Hide' : 'Show'}
|
||||
</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
{override && (
|
||||
<View style={[styles.childrenContainer, pal.border, pal.viewLight]}>
|
||||
<Link
|
||||
testID={testID}
|
||||
style={addStyle(style, styles.child)}
|
||||
href={href}
|
||||
noFeedback>
|
||||
{children}
|
||||
</Link>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -83,22 +97,23 @@ const styles = StyleSheet.create({
|
|||
description: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 18,
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
icon: {
|
||||
marginRight: 10,
|
||||
paddingLeft: 18,
|
||||
paddingRight: isDesktopWeb ? 18 : 22,
|
||||
marginTop: 1,
|
||||
},
|
||||
showBtn: {
|
||||
marginLeft: 'auto',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
childrenContainer: {
|
||||
paddingHorizontal: 6,
|
||||
paddingHorizontal: 4,
|
||||
paddingBottom: 6,
|
||||
},
|
||||
child: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 12,
|
||||
borderWidth: 0,
|
||||
borderTopWidth: 0,
|
||||
borderRadius: 8,
|
||||
},
|
||||
})
|
||||
|
|
76
src/view/com/util/moderation/ProfileHeaderAlerts.tsx
Normal file
76
src/view/com/util/moderation/ProfileHeaderAlerts.tsx
Normal file
|
@ -0,0 +1,76 @@
|
|||
import React from 'react'
|
||||
import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
||||
import {ProfileModeration} from '@atproto/api'
|
||||
import {Text} from '../text/Text'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {InfoCircleIcon} from 'lib/icons'
|
||||
import {
|
||||
describeModerationCause,
|
||||
getProfileModerationCauses,
|
||||
} from 'lib/moderation'
|
||||
import {useStores} from 'state/index'
|
||||
|
||||
export function ProfileHeaderAlerts({
|
||||
moderation,
|
||||
style,
|
||||
}: {
|
||||
moderation: ProfileModeration
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
|
||||
const causes = getProfileModerationCauses(moderation)
|
||||
if (!causes.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.grid}>
|
||||
{causes.map(cause => {
|
||||
const desc = describeModerationCause(cause, 'account')
|
||||
return (
|
||||
<Pressable
|
||||
testID="profileHeaderAlert"
|
||||
key={desc.name}
|
||||
onPress={() => {
|
||||
store.shell.openModal({
|
||||
name: 'moderation-details',
|
||||
context: 'content',
|
||||
moderation: {cause},
|
||||
})
|
||||
}}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Learn more about this warning"
|
||||
accessibilityHint=""
|
||||
style={[styles.container, pal.viewLight, style]}>
|
||||
<InfoCircleIcon style={pal.text} size={24} />
|
||||
<Text type="lg" style={pal.text}>
|
||||
{desc.name}
|
||||
</Text>
|
||||
<Text type="lg" style={[pal.link, styles.learnMoreBtn]}>
|
||||
Learn More
|
||||
</Text>
|
||||
</Pressable>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
grid: {
|
||||
gap: 4,
|
||||
},
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 8,
|
||||
},
|
||||
learnMoreBtn: {
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
})
|
|
@ -1,44 +0,0 @@
|
|||
import React from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {Text} from '../text/Text'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {ModerationBehavior, ModerationBehaviorCode} from 'lib/labeling/types'
|
||||
|
||||
export function ProfileHeaderWarnings({
|
||||
moderation,
|
||||
}: {
|
||||
moderation: ModerationBehavior
|
||||
}) {
|
||||
const palErr = usePalette('error')
|
||||
if (moderation.behavior === ModerationBehaviorCode.Show) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<View style={[styles.container, palErr.border, palErr.view]}>
|
||||
<FontAwesomeIcon
|
||||
icon="circle-exclamation"
|
||||
style={palErr.text as FontAwesomeIconStyle}
|
||||
size={20}
|
||||
/>
|
||||
<Text style={palErr.text}>
|
||||
This account has been flagged: {moderation.reason}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
borderWidth: 1,
|
||||
borderRadius: 6,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
})
|
|
@ -1,16 +1,24 @@
|
|||
import React from 'react'
|
||||
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
||||
import {
|
||||
TouchableWithoutFeedback,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
import {ModerationUI} from '@atproto/api'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {Text} from '../text/Text'
|
||||
import {Button} from '../forms/Button'
|
||||
import {isDesktopWeb} from 'platform/detection'
|
||||
import {ModerationBehaviorCode, ModerationBehavior} from 'lib/labeling/types'
|
||||
import {describeModerationCause} from 'lib/moderation'
|
||||
import {useStores} from 'state/index'
|
||||
|
||||
export function ScreenHider({
|
||||
testID,
|
||||
|
@ -22,24 +30,17 @@ export function ScreenHider({
|
|||
}: React.PropsWithChildren<{
|
||||
testID?: string
|
||||
screenDescription: string
|
||||
moderation: ModerationBehavior
|
||||
moderation: ModerationUI
|
||||
style?: StyleProp<ViewStyle>
|
||||
containerStyle?: StyleProp<ViewStyle>
|
||||
}>) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
const palInverted = usePalette('inverted')
|
||||
const [override, setOverride] = React.useState(false)
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
const onPressBack = React.useCallback(() => {
|
||||
if (navigation.canGoBack()) {
|
||||
navigation.goBack()
|
||||
} else {
|
||||
navigation.navigate('Home')
|
||||
}
|
||||
}, [navigation])
|
||||
|
||||
if (moderation.behavior !== ModerationBehaviorCode.Hide || override) {
|
||||
if (!moderation.blur || override) {
|
||||
return (
|
||||
<View testID={testID} style={style}>
|
||||
{children}
|
||||
|
@ -47,6 +48,7 @@ export function ScreenHider({
|
|||
)
|
||||
}
|
||||
|
||||
const desc = describeModerationCause(moderation.cause, 'account')
|
||||
return (
|
||||
<View style={[styles.container, pal.view, containerStyle]}>
|
||||
<View style={styles.iconContainer}>
|
||||
|
@ -63,11 +65,38 @@ export function ScreenHider({
|
|||
</Text>
|
||||
<Text type="2xl" style={[styles.description, pal.textLight]}>
|
||||
This {screenDescription} has been flagged:{' '}
|
||||
{moderation.reason || 'Content warning'}
|
||||
<Text type="2xl-medium" style={pal.text}>
|
||||
{desc.name}
|
||||
</Text>
|
||||
.{' '}
|
||||
<TouchableWithoutFeedback
|
||||
onPress={() => {
|
||||
store.shell.openModal({
|
||||
name: 'moderation-details',
|
||||
context: 'account',
|
||||
moderation,
|
||||
})
|
||||
}}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Learn more about this warning"
|
||||
accessibilityHint="">
|
||||
<Text type="2xl" style={pal.link}>
|
||||
Learn More
|
||||
</Text>
|
||||
</TouchableWithoutFeedback>
|
||||
</Text>
|
||||
{!isDesktopWeb && <View style={styles.spacer} />}
|
||||
<View style={styles.btnContainer}>
|
||||
<Button type="inverted" onPress={onPressBack} style={styles.btn}>
|
||||
<Button
|
||||
type="inverted"
|
||||
onPress={() => {
|
||||
if (navigation.canGoBack()) {
|
||||
navigation.goBack()
|
||||
} else {
|
||||
navigation.navigate('Home')
|
||||
}
|
||||
}}
|
||||
style={styles.btn}>
|
||||
<Text type="button-lg" style={pal.textInverted}>
|
||||
Go back
|
||||
</Text>
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import React from 'react'
|
||||
import {StyleProp, StyleSheet, ViewStyle} from 'react-native'
|
||||
import {AppBskyEmbedImages, AppBskyEmbedRecordWithMedia} from '@atproto/api'
|
||||
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
||||
import {
|
||||
AppBskyEmbedRecord,
|
||||
AppBskyFeedPost,
|
||||
AppBskyEmbedImages,
|
||||
AppBskyEmbedRecordWithMedia,
|
||||
} from '@atproto/api'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {PostMeta} from '../PostMeta'
|
||||
import {Link} from '../Link'
|
||||
|
@ -9,6 +14,55 @@ import {usePalette} from 'lib/hooks/usePalette'
|
|||
import {ComposerOptsQuote} from 'state/models/ui/shell'
|
||||
import {PostEmbeds} from '.'
|
||||
import {makeProfileLink} from 'lib/routes/links'
|
||||
import {InfoCircleIcon} from 'lib/icons'
|
||||
|
||||
export function MaybeQuoteEmbed({
|
||||
embed,
|
||||
style,
|
||||
}: {
|
||||
embed: AppBskyEmbedRecord.View
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
if (
|
||||
AppBskyEmbedRecord.isViewRecord(embed.record) &&
|
||||
AppBskyFeedPost.isRecord(embed.record.value) &&
|
||||
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,
|
||||
embeds: embed.record.embeds,
|
||||
}}
|
||||
style={style}
|
||||
/>
|
||||
)
|
||||
} else if (AppBskyEmbedRecord.isViewBlocked(embed.record)) {
|
||||
return (
|
||||
<View style={[styles.errorContainer, pal.borderDark]}>
|
||||
<InfoCircleIcon size={18} style={pal.text} />
|
||||
<Text type="lg" style={pal.text}>
|
||||
Blocked
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
} else if (AppBskyEmbedRecord.isViewNotFound(embed.record)) {
|
||||
return (
|
||||
<View style={[styles.errorContainer, pal.borderDark]}>
|
||||
<InfoCircleIcon size={18} style={pal.text} />
|
||||
<Text type="lg" style={pal.text}>
|
||||
Deleted
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function QuoteEmbed({
|
||||
quote,
|
||||
|
@ -76,4 +130,14 @@ const styles = StyleSheet.create({
|
|||
paddingLeft: 13,
|
||||
paddingRight: 8,
|
||||
},
|
||||
errorContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
borderRadius: 8,
|
||||
marginTop: 8,
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 14,
|
||||
borderWidth: 1,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -12,7 +12,6 @@ import {
|
|||
AppBskyEmbedExternal,
|
||||
AppBskyEmbedRecord,
|
||||
AppBskyEmbedRecordWithMedia,
|
||||
AppBskyFeedPost,
|
||||
AppBskyFeedDefs,
|
||||
AppBskyGraphDefs,
|
||||
} from '@atproto/api'
|
||||
|
@ -24,7 +23,7 @@ import {usePalette} from 'lib/hooks/usePalette'
|
|||
import {YoutubeEmbed} from './YoutubeEmbed'
|
||||
import {ExternalLinkEmbed} from './ExternalLinkEmbed'
|
||||
import {getYoutubeVideoId} from 'lib/strings/url-helpers'
|
||||
import QuoteEmbed from './QuoteEmbed'
|
||||
import {MaybeQuoteEmbed} from './QuoteEmbed'
|
||||
import {AutoSizedImage} from '../images/AutoSizedImage'
|
||||
import {CustomFeedEmbed} from './CustomFeedEmbed'
|
||||
import {ListEmbed} from './ListEmbed'
|
||||
|
@ -49,25 +48,11 @@ export function PostEmbeds({
|
|||
|
||||
// quote post with media
|
||||
// =
|
||||
if (
|
||||
AppBskyEmbedRecordWithMedia.isView(embed) &&
|
||||
AppBskyEmbedRecord.isViewRecord(embed.record.record) &&
|
||||
AppBskyFeedPost.isRecord(embed.record.record.value) &&
|
||||
AppBskyFeedPost.validateRecord(embed.record.record.value).success
|
||||
) {
|
||||
if (AppBskyEmbedRecordWithMedia.isView(embed)) {
|
||||
return (
|
||||
<View style={[styles.stackContainer, style]}>
|
||||
<PostEmbeds embed={embed.media} />
|
||||
<QuoteEmbed
|
||||
quote={{
|
||||
author: embed.record.record.author,
|
||||
cid: embed.record.record.cid,
|
||||
uri: embed.record.record.uri,
|
||||
indexedAt: embed.record.record.indexedAt,
|
||||
text: embed.record.record.value.text,
|
||||
embeds: embed.record.record.embeds,
|
||||
}}
|
||||
/>
|
||||
<MaybeQuoteEmbed embed={embed.record} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
@ -75,25 +60,7 @@ export function PostEmbeds({
|
|||
// quote post
|
||||
// =
|
||||
if (AppBskyEmbedRecord.isView(embed)) {
|
||||
if (
|
||||
AppBskyEmbedRecord.isViewRecord(embed.record) &&
|
||||
AppBskyFeedPost.isRecord(embed.record.value) &&
|
||||
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,
|
||||
embeds: embed.record.embeds,
|
||||
}}
|
||||
style={style}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return <MaybeQuoteEmbed embed={embed} style={style} />
|
||||
}
|
||||
|
||||
// image embed
|
||||
|
|
|
@ -66,7 +66,6 @@ export const ModerationBlockedAccounts = withAuthRequired(
|
|||
testID={`blockedAccount-${index}`}
|
||||
key={item.did}
|
||||
profile={item}
|
||||
overrideModeration
|
||||
/>
|
||||
)
|
||||
return (
|
||||
|
|
|
@ -63,7 +63,6 @@ export const ModerationMutedAccounts = withAuthRequired(
|
|||
testID={`mutedAccount-${index}`}
|
||||
key={item.did}
|
||||
profile={item}
|
||||
overrideModeration
|
||||
/>
|
||||
)
|
||||
return (
|
||||
|
|
|
@ -232,7 +232,7 @@ export const ProfileScreen = withAuthRequired(
|
|||
)
|
||||
} else if (item instanceof PostsFeedSliceModel) {
|
||||
return (
|
||||
<FeedSlice slice={item} ignoreMuteFor={uiState.profile.did} />
|
||||
<FeedSlice slice={item} ignoreFilterFor={uiState.profile.did} />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -252,7 +252,7 @@ export const ProfileScreen = withAuthRequired(
|
|||
testID="profileView"
|
||||
style={styles.container}
|
||||
screenDescription="profile"
|
||||
moderation={uiState.profile.moderation.view}>
|
||||
moderation={uiState.profile.moderation.account}>
|
||||
{uiState.profile.hasError ? (
|
||||
<ErrorScreen
|
||||
testID="profileErrorScreen"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue