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:
Paul Frazee 2023-08-03 22:08:30 -07:00 committed by GitHub
parent 3ae5a6b631
commit b154d3ea21
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 1193 additions and 717 deletions

View file

@ -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,
},
})

View file

@ -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()

View file

@ -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: {},
})

View file

@ -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,
},
})

View 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',
},
})

View file

@ -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,
},
})

View 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',
},
})

View file

@ -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,
},
})

View file

@ -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>

View file

@ -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,
},
})

View file

@ -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