diff options
Diffstat (limited to 'src/view/com/util')
21 files changed, 600 insertions, 488 deletions
diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index 2ce499765..bf21ff0d1 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -91,7 +91,7 @@ export const PostMeta = observer(function (opts: PostMetaOpts) { const styles = StyleSheet.create({ metaOneLine: { flexDirection: 'row', - alignItems: 'baseline', + alignItems: isAndroid ? 'center' : 'baseline', paddingBottom: 2, gap: 4, }, diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index d999ffb31..0f34f75aa 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -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, }, }) diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx index b7e91b5dd..7c5c583c2 100644 --- a/src/view/com/util/UserBanner.tsx +++ b/src/view/com/util/UserBanner.tsx @@ -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() diff --git a/src/view/com/util/UserPreviewLink.tsx b/src/view/com/util/UserPreviewLink.tsx index 7eedbc2d4..f43f9e80b 100644 --- a/src/view/com/util/UserPreviewLink.tsx +++ b/src/view/com/util/UserPreviewLink.tsx @@ -2,7 +2,7 @@ import React from 'react' import {Pressable, StyleProp, ViewStyle} from 'react-native' import {useStores} from 'state/index' import {Link} from './Link' -import {isDesktopWeb} from 'platform/detection' +import {isWeb} from 'platform/detection' import {makeProfileLink} from 'lib/routes/links' interface UserPreviewLinkProps { @@ -15,7 +15,7 @@ export function UserPreviewLink( ) { const store = useStores() - if (isDesktopWeb) { + if (isWeb) { return ( <Link href={makeProfileLink(props)} diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx index e2f47ba89..a25ca4d8e 100644 --- a/src/view/com/util/ViewSelector.tsx +++ b/src/view/com/util/ViewSelector.tsx @@ -1,5 +1,11 @@ import React, {useEffect, useState} from 'react' -import {Pressable, RefreshControl, StyleSheet, View} from 'react-native' +import { + Pressable, + RefreshControl, + StyleSheet, + View, + ScrollView, +} from 'react-native' import {FlatList} from './Views' import {OnScrollCb} from 'lib/hooks/useOnMainScroll' import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' @@ -140,6 +146,8 @@ export function Selector({ items: string[] onSelect?: (index: number) => void }) { + const [height, setHeight] = useState(0) + const pal = usePalette('default') const borderColor = useColorSchemeStyle( {borderColor: colors.black}, @@ -151,37 +159,56 @@ export function Selector({ } return ( - <View style={[pal.view, styles.outer]}> - {items.map((item, i) => { - const selected = i === selectedIndex - return ( - <Pressable - testID={`selector-${i}`} - key={item} - onPress={() => onPressItem(i)} - accessibilityLabel={item} - accessibilityHint={`Selects ${item}`} - // TODO: Modify the component API such that lint fails - // at the invocation site as well - > - <View - style={[ - styles.item, - selected && styles.itemSelected, - borderColor, - ]}> - <Text - style={ - selected - ? [styles.labelSelected, pal.text] - : [styles.label, pal.textLight] - }> - {item} - </Text> - </View> - </Pressable> - ) - })} + <View + style={{ + width: '100%', + position: 'relative', + overflow: 'hidden', + height, + backgroundColor: pal.colors.background, + }}> + <ScrollView + horizontal + showsHorizontalScrollIndicator={false} + style={{position: 'absolute'}}> + <View + style={[pal.view, styles.outer]} + onLayout={e => { + const {height} = e.nativeEvent.layout + setHeight(height || 60) + }}> + {items.map((item, i) => { + const selected = i === selectedIndex + return ( + <Pressable + testID={`selector-${i}`} + key={item} + onPress={() => onPressItem(i)} + accessibilityLabel={item} + accessibilityHint={`Selects ${item}`} + // TODO: Modify the component API such that lint fails + // at the invocation site as well + > + <View + style={[ + styles.item, + selected && styles.itemSelected, + borderColor, + ]}> + <Text + style={ + selected + ? [styles.labelSelected, pal.text] + : [styles.label, pal.textLight] + }> + {item} + </Text> + </View> + </Pressable> + ) + })} + </View> + </ScrollView> </View> ) } diff --git a/src/view/com/util/forms/NativeDropdown.tsx b/src/view/com/util/forms/NativeDropdown.tsx index 9e6fcaa44..082285064 100644 --- a/src/view/com/util/forms/NativeDropdown.tsx +++ b/src/view/com/util/forms/NativeDropdown.tsx @@ -60,7 +60,6 @@ export const DropdownMenuTrigger = DropdownMenu.create( icon="ellipsis" size={20} color={defaultCtrlColor} - style={styles.ellipsis} /> )} </View> @@ -252,9 +251,6 @@ const styles = StyleSheet.create({ height: 1, marginVertical: 4, }, - ellipsis: { - padding: isWeb ? 0 : 10, - }, content: { backgroundColor: '#f0f0f0', borderRadius: 8, diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index 27a1f20d0..969deb3ac 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -1,6 +1,9 @@ import React from 'react' +import {StyleProp, View, ViewStyle} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {toShareUrl} from 'lib/strings/url-helpers' import {useStores} from 'state/index' +import {useTheme} from 'lib/ThemeContext' import {shareUrl} from 'lib/sharing' import { NativeDropdown, @@ -19,6 +22,7 @@ export function PostDropdownBtn({ onOpenTranslate, onToggleThreadMute, onDeletePost, + style, }: { testID: string itemUri: string @@ -31,8 +35,11 @@ export function PostDropdownBtn({ onOpenTranslate: () => void onToggleThreadMute: () => void onDeletePost: () => void + style?: StyleProp<ViewStyle> }) { const store = useStores() + const theme = useTheme() + const defaultCtrlColor = theme.palette.default.postCtrl const dropdownItems: NativeDropdownItem[] = [ { @@ -102,9 +109,9 @@ export function PostDropdownBtn({ label: 'Report post', onPress() { store.shell.openModal({ - name: 'report-post', - postUri: itemUri, - postCid: itemCid, + name: 'report', + uri: itemUri, + cid: itemCid, }) }, testID: 'postDropdownReportBtn', @@ -146,8 +153,11 @@ export function PostDropdownBtn({ testID={testID} items={dropdownItems} accessibilityLabel="More post options" - accessibilityHint="" - /> + accessibilityHint=""> + <View style={style}> + <FontAwesomeIcon icon="ellipsis" size={20} color={defaultCtrlColor} /> + </View> + </NativeDropdown> </EventStopper> ) } diff --git a/src/view/com/util/forms/SelectableBtn.tsx b/src/view/com/util/forms/SelectableBtn.tsx index 503c49b2f..4b494264e 100644 --- a/src/view/com/util/forms/SelectableBtn.tsx +++ b/src/view/com/util/forms/SelectableBtn.tsx @@ -5,6 +5,7 @@ import {usePalette} from 'lib/hooks/usePalette' import {isDesktopWeb} from 'platform/detection' interface SelectableBtnProps { + testID?: string selected: boolean label: string left?: boolean @@ -15,6 +16,7 @@ interface SelectableBtnProps { } export function SelectableBtn({ + testID, selected, label, left, @@ -25,12 +27,15 @@ export function SelectableBtn({ }: SelectableBtnProps) { const pal = usePalette('default') const palPrimary = usePalette('inverted') + const needsWidthStyles = !style || !('width' in style || 'flex' in style) return ( <Pressable + testID={testID} style={[ - styles.selectableBtn, - left && styles.selectableBtnLeft, - right && styles.selectableBtnRight, + styles.btn, + needsWidthStyles && styles.btnWidth, + left && styles.btnLeft, + right && styles.btnRight, pal.border, selected ? palPrimary.view : pal.view, style, @@ -45,9 +50,7 @@ export function SelectableBtn({ } const styles = StyleSheet.create({ - selectableBtn: { - flex: isDesktopWeb ? undefined : 1, - width: isDesktopWeb ? 100 : undefined, + btn: { flexDirection: 'row', justifyContent: 'center', borderWidth: 1, @@ -55,12 +58,16 @@ const styles = StyleSheet.create({ paddingHorizontal: 10, paddingVertical: 10, }, - selectableBtnLeft: { + btnWidth: { + flex: isDesktopWeb ? undefined : 1, + width: isDesktopWeb ? 100 : undefined, + }, + btnLeft: { borderTopLeftRadius: 8, borderBottomLeftRadius: 8, borderLeftWidth: 1, }, - selectableBtnRight: { + btnRight: { borderTopRightRadius: 8, borderBottomRightRadius: 8, }, diff --git a/src/view/com/util/moderation/ContentHider.tsx b/src/view/com/util/moderation/ContentHider.tsx index ac5c8395d..853f7840c 100644 --- a/src/view/com/util/moderation/ContentHider.tsx +++ b/src/view/com/util/moderation/ContentHider.tsx @@ -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 {ShieldExclamation} 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,72 @@ 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' - } + onPress={() => { + if (!moderation.noOverride) { + setOverride(v => !v) + } else { + store.shell.openModal({ + name: 'moderation-details', + context: 'content', + moderation, + }) + } + }} + accessibilityRole="button" + accessibilityHint={override ? 'Hide the content' : 'Show the content'} + accessibilityLabel="" style={[ - styles.description, - pal.viewLight, - override && styles.descriptionOpen, + styles.cover, + moderation.noOverride + ? {borderWidth: 1, borderColor: pal.colors.borderDark} + : pal.viewLight, ]}> - <Text type="md" style={pal.textLight}> - {moderation.reason || 'Content warning'} + <Pressable + onPress={() => { + store.shell.openModal({ + name: 'moderation-details', + context: 'content', + moderation, + }) + }} + accessibilityRole="button" + accessibilityLabel="Learn more about this warning" + accessibilityHint=""> + <ShieldExclamation 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: {}, }) diff --git a/src/view/com/util/moderation/ImageHider.tsx b/src/view/com/util/moderation/ImageHider.tsx deleted file mode 100644 index 40c9d0a21..000000000 --- a/src/view/com/util/moderation/ImageHider.tsx +++ /dev/null @@ -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, - }, -}) diff --git a/src/view/com/util/moderation/PostAlerts.tsx b/src/view/com/util/moderation/PostAlerts.tsx new file mode 100644 index 000000000..8a6cbbb85 --- /dev/null +++ b/src/view/com/util/moderation/PostAlerts.tsx @@ -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 {ShieldExclamation} 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]}> + <ShieldExclamation style={pal.text} size={16} /> + <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', + }, +}) diff --git a/src/view/com/util/moderation/PostHider.tsx b/src/view/com/util/moderation/PostHider.tsx index f2b6dbddd..2a52561d4 100644 --- a/src/view/com/util/moderation/PostHider.tsx +++ b/src/view/com/util/moderation/PostHider.tsx @@ -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 {ShieldExclamation} 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=""> + <ShieldExclamation 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, }, }) diff --git a/src/view/com/util/moderation/ProfileHeaderAlerts.tsx b/src/view/com/util/moderation/ProfileHeaderAlerts.tsx new file mode 100644 index 000000000..b7781e06d --- /dev/null +++ b/src/view/com/util/moderation/ProfileHeaderAlerts.tsx @@ -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 {ShieldExclamation} 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]}> + <ShieldExclamation 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', + }, +}) diff --git a/src/view/com/util/moderation/ProfileHeaderWarnings.tsx b/src/view/com/util/moderation/ProfileHeaderWarnings.tsx deleted file mode 100644 index 7a1a8e295..000000000 --- a/src/view/com/util/moderation/ProfileHeaderWarnings.tsx +++ /dev/null @@ -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, - }, -}) diff --git a/src/view/com/util/moderation/ScreenHider.tsx b/src/view/com/util/moderation/ScreenHider.tsx index 2e7b07e1a..b76b1101c 100644 --- a/src/view/com/util/moderation/ScreenHider.tsx +++ b/src/view/com/util/moderation/ScreenHider.tsx @@ -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> diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index 672e02693..c71100df0 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -6,11 +6,6 @@ import { View, ViewStyle, } from 'react-native' -// DISABLED see #135 -// import { -// TriggerableAnimated, -// TriggerableAnimatedRef, -// } from './anim/TriggerableAnimated' import {Text} from '../text/Text' import {PostDropdownBtn} from '../forms/PostDropdownBtn' import {HeartIcon, HeartIconSolid, CommentBottomArrow} from 'lib/icons' @@ -20,7 +15,6 @@ import {useTheme} from 'lib/ThemeContext' import {useStores} from 'state/index' import {RepostButton} from './RepostButton' import {Haptics} from 'lib/haptics' -import {createHitslop} from 'lib/constants' interface PostCtrlsOpts { itemUri: string @@ -53,44 +47,6 @@ interface PostCtrlsOpts { onDeletePost: () => void } -const HITSLOP = createHitslop(5) - -// DISABLED see #135 -/* -function ctrlAnimStart(interp: Animated.Value) { - return Animated.sequence([ - Animated.timing(interp, { - toValue: 1, - duration: 250, - useNativeDriver: true, - }), - Animated.delay(50), - Animated.timing(interp, { - toValue: 0, - duration: 20, - useNativeDriver: true, - }), - ]) -} - -function ctrlAnimStyle(interp: Animated.Value) { - return { - transform: [ - { - scale: interp.interpolate({ - inputRange: [0, 1.0], - outputRange: [1.0, 4.0], - }), - }, - ], - opacity: interp.interpolate({ - inputRange: [0, 1.0], - outputRange: [1.0, 0.0], - }), - } -} -*/ - export function PostCtrls(opts: PostCtrlsOpts) { const store = useStores() const theme = useTheme() @@ -100,22 +56,11 @@ export function PostCtrls(opts: PostCtrlsOpts) { }), [theme], ) as StyleProp<ViewStyle> - // DISABLED see #135 - // const repostRef = React.useRef<TriggerableAnimatedRef | null>(null) - // const likeRef = React.useRef<TriggerableAnimatedRef | null>(null) const onRepost = useCallback(() => { store.shell.closeModal() if (!opts.isReposted) { Haptics.default() opts.onPressToggleRepost().catch(_e => undefined) - // DISABLED see #135 - // repostRef.current?.trigger( - // {start: ctrlAnimStart, style: ctrlAnimStyle}, - // async () => { - // await opts.onPressToggleRepost().catch(_e => undefined) - // setRepostMod(0) - // }, - // ) } else { opts.onPressToggleRepost().catch(_e => undefined) } @@ -146,18 +91,8 @@ export function PostCtrls(opts: PostCtrlsOpts) { if (!opts.isLiked) { Haptics.default() await opts.onPressToggleLike().catch(_e => undefined) - // DISABLED see #135 - // likeRef.current?.trigger( - // {start: ctrlAnimStart, style: ctrlAnimStyle}, - // async () => { - // await opts.onPressToggleLike().catch(_e => undefined) - // setLikeMod(0) - // }, - // ) - // setIsLikedPressed(false) } else { await opts.onPressToggleLike().catch(_e => undefined) - // setIsLikedPressed(false) } } @@ -165,8 +100,7 @@ export function PostCtrls(opts: PostCtrlsOpts) { <View style={[styles.ctrls, opts.style]}> <TouchableOpacity testID="replyBtn" - style={styles.ctrl} - hitSlop={HITSLOP} + style={[styles.ctrl, !opts.big && styles.ctrlPad, {paddingLeft: 0}]} onPress={opts.onPressReply} accessibilityRole="button" accessibilityLabel={`Reply (${opts.replyCount} ${ @@ -187,8 +121,7 @@ export function PostCtrls(opts: PostCtrlsOpts) { <RepostButton {...opts} onRepost={onRepost} onQuote={onQuote} /> <TouchableOpacity testID="likeBtn" - style={styles.ctrl} - hitSlop={HITSLOP} + style={[styles.ctrl, !opts.big && styles.ctrlPad]} onPress={onPressToggleLikeWrapper} accessibilityRole="button" accessibilityLabel={`${opts.isLiked ? 'Unlike' : 'Like'} (${ @@ -232,6 +165,7 @@ export function PostCtrls(opts: PostCtrlsOpts) { onOpenTranslate={opts.onOpenTranslate} onToggleThreadMute={opts.onToggleThreadMute} onDeletePost={opts.onDeletePost} + style={styles.ctrlPad} /> )} {/* used for adding pad to the right side */} @@ -248,8 +182,12 @@ const styles = StyleSheet.create({ ctrl: { flexDirection: 'row', alignItems: 'center', - padding: 5, - margin: -5, + }, + ctrlPad: { + paddingTop: 5, + paddingBottom: 5, + paddingLeft: 5, + paddingRight: 5, }, ctrlIconLiked: { color: colors.like, diff --git a/src/view/com/util/post-ctrls/RepostButton.tsx b/src/view/com/util/post-ctrls/RepostButton.tsx index 5fe62aefe..374d06515 100644 --- a/src/view/com/util/post-ctrls/RepostButton.tsx +++ b/src/view/com/util/post-ctrls/RepostButton.tsx @@ -6,9 +6,6 @@ import {useTheme} from 'lib/ThemeContext' import {Text} from '../text/Text' import {pluralize} from 'lib/strings/helpers' import {useStores} from 'state/index' -import {createHitslop} from 'lib/constants' - -const HITSLOP = createHitslop(5) interface Props { isReposted: boolean @@ -47,9 +44,8 @@ export const RepostButton = ({ return ( <TouchableOpacity testID="repostBtn" - hitSlop={HITSLOP} onPress={onPressToggleRepostWrapper} - style={styles.control} + style={[styles.control, !big && styles.controlPad]} accessibilityRole="button" accessibilityLabel={`${ isReposted ? 'Undo repost' : 'Repost' @@ -83,8 +79,9 @@ const styles = StyleSheet.create({ control: { flexDirection: 'row', alignItems: 'center', + }, + controlPad: { padding: 5, - margin: -5, }, reposted: { color: colors.green3, diff --git a/src/view/com/util/post-ctrls/RepostButton.web.tsx b/src/view/com/util/post-ctrls/RepostButton.web.tsx index 4d2a3fcdd..eab6e2fef 100644 --- a/src/view/com/util/post-ctrls/RepostButton.web.tsx +++ b/src/view/com/util/post-ctrls/RepostButton.web.tsx @@ -52,6 +52,7 @@ export const RepostButton = ({ <View style={[ styles.control, + !big && styles.controlPad, (isReposted ? styles.reposted : defaultControlColor) as StyleProp<ViewStyle>, @@ -77,6 +78,9 @@ const styles = StyleSheet.create({ alignItems: 'center', gap: 4, }, + controlPad: { + padding: 5, + }, reposted: { color: colors.green3, }, diff --git a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx index a4cbb3e29..81f1ca560 100644 --- a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx +++ b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx @@ -1,9 +1,11 @@ import React from 'react' +import {Image} from 'expo-image' import {Text} from '../text/Text' -import {AutoSizedImage} from '../images/AutoSizedImage' import {StyleSheet, View} from 'react-native' import {usePalette} from 'lib/hooks/usePalette' import {AppBskyEmbedExternal} from '@atproto/api' +import {isDesktopWeb} from 'platform/detection' +import {toNiceDomain} from 'lib/strings/url-helpers' export const ExternalLinkEmbed = ({ link, @@ -14,44 +16,71 @@ export const ExternalLinkEmbed = ({ }) => { const pal = usePalette('default') return ( - <> + <View style={styles.extContainer}> {link.thumb ? ( - <AutoSizedImage uri={link.thumb} style={styles.extImage}> + <View style={styles.extImageContainer}> + <Image + style={styles.extImage} + source={{uri: link.thumb}} + accessibilityIgnoresInvertColors + /> {imageChild} - </AutoSizedImage> + </View> ) : undefined} <View style={styles.extInner}> - <Text type="md-bold" numberOfLines={2} style={[pal.text]}> - {link.title || link.uri} - </Text> <Text type="sm" numberOfLines={1} style={[pal.textLight, styles.extUri]}> - {link.uri} + {toNiceDomain(link.uri)} + </Text> + <Text + type="lg-bold" + numberOfLines={isDesktopWeb ? 2 : 4} + style={[pal.text]}> + {link.title || link.uri} </Text> {link.description ? ( <Text - type="sm" - numberOfLines={2} + type="md" + numberOfLines={isDesktopWeb ? 2 : 4} style={[pal.text, styles.extDescription]}> {link.description} </Text> ) : undefined} </View> - </> + </View> ) } const styles = StyleSheet.create({ + extContainer: { + flexDirection: isDesktopWeb ? 'row' : 'column', + }, extInner: { - padding: 10, + paddingHorizontal: isDesktopWeb ? 14 : 10, + paddingTop: 8, + paddingBottom: 10, + flex: isDesktopWeb ? 1 : undefined, }, + extImageContainer: isDesktopWeb + ? { + borderTopLeftRadius: 6, + borderBottomLeftRadius: 6, + width: 120, + aspectRatio: 1, + overflow: 'hidden', + } + : { + borderTopLeftRadius: 6, + borderTopRightRadius: 6, + width: '100%', + height: 200, + overflow: 'hidden', + }, extImage: { - borderTopLeftRadius: 6, - borderTopRightRadius: 6, width: '100%', - maxHeight: 200, + height: 200, }, extUri: { marginTop: 2, diff --git a/src/view/com/util/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx index 4995562ac..f82b5b7df 100644 --- a/src/view/com/util/post-embeds/QuoteEmbed.tsx +++ b/src/view/com/util/post-embeds/QuoteEmbed.tsx @@ -1,6 +1,12 @@ 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, + ModerationUI, +} from '@atproto/api' import {AtUri} from '@atproto/api' import {PostMeta} from '../PostMeta' import {Link} from '../Link' @@ -8,13 +14,68 @@ import {Text} from '../text/Text' import {usePalette} from 'lib/hooks/usePalette' import {ComposerOptsQuote} from 'state/models/ui/shell' import {PostEmbeds} from '.' +import {PostAlerts} from '../moderation/PostAlerts' import {makeProfileLink} from 'lib/routes/links' +import {InfoCircleIcon} from 'lib/icons' + +export function MaybeQuoteEmbed({ + embed, + moderation, + style, +}: { + embed: AppBskyEmbedRecord.View + moderation: ModerationUI + 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, + }} + moderation={moderation} + 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, + moderation, style, }: { quote: ComposerOptsQuote + moderation?: ModerationUI style?: StyleProp<ViewStyle> }) { const pal = usePalette('default') @@ -46,16 +107,19 @@ export function QuoteEmbed({ postHref={itemHref} timestamp={quote.indexedAt} /> + {moderation ? ( + <PostAlerts moderation={moderation} style={styles.alert} /> + ) : null} {!isEmpty ? ( <Text type="post-text" style={pal.text} numberOfLines={6}> {quote.text} </Text> ) : null} {AppBskyEmbedImages.isView(imagesEmbed) && ( - <PostEmbeds embed={imagesEmbed} /> + <PostEmbeds embed={imagesEmbed} moderation={{}} /> )} {AppBskyEmbedRecordWithMedia.isView(imagesEmbed) && ( - <PostEmbeds embed={imagesEmbed.media} /> + <PostEmbeds embed={imagesEmbed.media} moderation={{}} /> )} </Link> ) @@ -76,4 +140,17 @@ 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, + }, + alert: { + marginBottom: 6, + }, }) diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index 7ffebff54..5d0090434 100644 --- a/src/view/com/util/post-embeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -4,17 +4,18 @@ import { StyleProp, View, ViewStyle, - Image as RNImage, Text, + InteractionManager, } from 'react-native' +import {Image} from 'expo-image' import { AppBskyEmbedImages, AppBskyEmbedExternal, AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, - AppBskyFeedPost, AppBskyFeedDefs, AppBskyGraphDefs, + ModerationUI, } from '@atproto/api' import {Link} from '../Link' import {ImageLayoutGrid} from '../images/ImageLayoutGrid' @@ -24,11 +25,12 @@ 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' import {isDesktopWeb} from 'platform/detection' +import {isCauseALabelOnUri} from 'lib/moderation' type Embed = | AppBskyEmbedRecord.View @@ -39,9 +41,11 @@ type Embed = export function PostEmbeds({ embed, + moderation, style, }: { embed?: Embed + moderation: ModerationUI style?: StyleProp<ViewStyle> }) { const pal = usePalette('default') @@ -49,51 +53,37 @@ 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)) { + const isModOnQuote = + AppBskyEmbedRecord.isViewRecord(embed.record.record) && + isCauseALabelOnUri(moderation.cause, embed.record.record.uri) + const mediaModeration = isModOnQuote ? {} : moderation + const quoteModeration = isModOnQuote ? moderation : {} 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, - }} - /> + <PostEmbeds embed={embed.media} moderation={mediaModeration} /> + <MaybeQuoteEmbed embed={embed.record} moderation={quoteModeration} /> </View> ) } - // 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} - /> - ) + // custom feed embed (i.e. generator view) + // = + if (AppBskyFeedDefs.isGeneratorView(embed.record)) { + return <CustomFeedEmbed record={embed.record} /> } + + // list embed (e.g. mute lists; i.e. ListView) + if (AppBskyGraphDefs.isListView(embed.record)) { + return <ListEmbed item={embed.record} /> + } + + // quote post + // = + return ( + <MaybeQuoteEmbed embed={embed} style={style} moderation={moderation} /> + ) } // image embed @@ -106,14 +96,9 @@ export function PostEmbeds({ const openLightbox = (index: number) => { store.shell.openLightbox(new ImagesLightbox(items, index)) } - const onPressIn = (index: number) => { - const firstImageToShow = items[index].uri - RNImage.prefetch(firstImageToShow) - items.forEach(item => { - if (firstImageToShow !== item.uri) { - // First image already prefetched above - RNImage.prefetch(item.uri) - } + const onPressIn = (_: number) => { + InteractionManager.runAfterInteractions(() => { + Image.prefetch(items.map(i => i.uri)) }) } @@ -152,23 +137,6 @@ export function PostEmbeds({ } } - // custom feed embed (i.e. generator view) - // = - if ( - AppBskyEmbedRecord.isView(embed) && - AppBskyFeedDefs.isGeneratorView(embed.record) - ) { - return <CustomFeedEmbed record={embed.record} /> - } - - // list embed (e.g. mute lists; i.e. ListView) - if ( - AppBskyEmbedRecord.isView(embed) && - AppBskyGraphDefs.isListView(embed.record) - ) { - return <ListEmbed item={embed.record} /> - } - // external link embed // = if (AppBskyEmbedExternal.isView(embed)) { |