diff options
Diffstat (limited to 'src/view/com/util')
-rw-r--r-- | src/view/com/util/PostMeta.tsx | 2 | ||||
-rw-r--r-- | src/view/com/util/UserAvatar.tsx | 17 | ||||
-rw-r--r-- | src/view/com/util/UserBanner.tsx | 9 | ||||
-rw-r--r-- | src/view/com/util/error/ErrorScreen.tsx | 2 | ||||
-rw-r--r-- | src/view/com/util/moderation/ContentHider.tsx | 25 | ||||
-rw-r--r-- | src/view/com/util/moderation/PostHider.tsx | 85 | ||||
-rw-r--r-- | src/view/com/util/moderation/ProfileHeaderLabels.tsx | 55 | ||||
-rw-r--r-- | src/view/com/util/moderation/ProfileHeaderWarnings.tsx | 44 | ||||
-rw-r--r-- | src/view/com/util/moderation/ScreenHider.tsx | 129 |
9 files changed, 243 insertions, 125 deletions
diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index d9dd11e05..45651e4e5 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -97,7 +97,7 @@ export const PostMeta = observer(function (opts: PostMetaOpts) { <UserAvatar avatar={opts.authorAvatar} size={16} - hasWarning={opts.authorHasWarning} + // TODO moderation /> </View> )} diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 9c0fe9297..7f55bf773 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -13,8 +13,11 @@ import {useStores} from 'state/index' import {colors} from 'lib/styles' import {DropdownButton} from './forms/DropdownButton' import {usePalette} from 'lib/hooks/usePalette' -import {isWeb} from 'platform/detection' +import {isWeb, isAndroid} from 'platform/detection' import {Image as RNImage} from 'react-native-image-crop-picker' +import {AvatarModeration} from 'lib/labeling/types' + +const BLUR_AMOUNT = isWeb ? 5 : 100 function DefaultAvatar({size}: {size: number}) { return ( @@ -40,12 +43,12 @@ function DefaultAvatar({size}: {size: number}) { export function UserAvatar({ size, avatar, - hasWarning, + moderation, onSelectNewAvatar, }: { size: number avatar?: string | null - hasWarning?: boolean + moderation?: AvatarModeration onSelectNewAvatar?: (img: RNImage | null) => void }) { const store = useStores() @@ -114,7 +117,7 @@ export function UserAvatar({ ) const warning = useMemo(() => { - if (!hasWarning) { + if (!moderation?.warn) { return null } return ( @@ -126,7 +129,7 @@ export function UserAvatar({ /> </View> ) - }, [hasWarning, size, pal]) + }, [moderation?.warn, size, pal]) // onSelectNewAvatar is only passed as prop on the EditProfile component return onSelectNewAvatar ? ( @@ -159,13 +162,15 @@ export function UserAvatar({ /> </View> </DropdownButton> - ) : avatar ? ( + ) : avatar && + !((moderation?.blur && isAndroid) /* android crashes with blur */) ? ( <View style={{width: size, height: size}}> <HighPriorityImage testID="userAvatarImage" style={{width: size, height: size, borderRadius: Math.floor(size / 2)}} contentFit="cover" source={{uri: avatar}} + blurRadius={moderation?.blur ? BLUR_AMOUNT : 0} /> {warning} </View> diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx index fcd66ca7a..14459bf77 100644 --- a/src/view/com/util/UserBanner.tsx +++ b/src/view/com/util/UserBanner.tsx @@ -13,13 +13,16 @@ import { } from 'lib/hooks/usePermissions' import {DropdownButton} from './forms/DropdownButton' import {usePalette} from 'lib/hooks/usePalette' -import {isWeb} from 'platform/detection' +import {AvatarModeration} from 'lib/labeling/types' +import {isWeb, isAndroid} from 'platform/detection' export function UserBanner({ banner, + moderation, onSelectNewBanner, }: { banner?: string | null + moderation?: AvatarModeration onSelectNewBanner?: (img: TImage | null) => void }) { const store = useStores() @@ -107,12 +110,14 @@ export function UserBanner({ /> </View> </DropdownButton> - ) : banner ? ( + ) : banner && + !((moderation?.blur && isAndroid) /* android crashes with blur */) ? ( <Image testID="userBannerImage" style={styles.bannerImage} resizeMode="cover" source={{uri: banner}} + blurRadius={moderation?.blur ? 100 : 0} /> ) : ( <View diff --git a/src/view/com/util/error/ErrorScreen.tsx b/src/view/com/util/error/ErrorScreen.tsx index dee625967..c849e37db 100644 --- a/src/view/com/util/error/ErrorScreen.tsx +++ b/src/view/com/util/error/ErrorScreen.tsx @@ -35,7 +35,7 @@ export function ErrorScreen({ ]}> <FontAwesomeIcon icon="exclamation" - style={pal.textInverted} + style={pal.textInverted as FontAwesomeIconStyle} size={24} /> </View> diff --git a/src/view/com/util/moderation/ContentHider.tsx b/src/view/com/util/moderation/ContentHider.tsx index 42a97cd34..74fb479ad 100644 --- a/src/view/com/util/moderation/ContentHider.tsx +++ b/src/view/com/util/moderation/ContentHider.tsx @@ -6,32 +6,31 @@ import { View, ViewStyle, } from 'react-native' -import {ComAtprotoLabelDefs} from '@atproto/api' import {usePalette} from 'lib/hooks/usePalette' -import {useStores} from 'state/index' import {Text} from '../text/Text' import {addStyle} from 'lib/styles' +import {ModerationBehavior, ModerationBehaviorCode} from 'lib/labeling/types' export function ContentHider({ testID, - isMuted, - labels, + moderation, style, containerStyle, children, }: React.PropsWithChildren<{ testID?: string - isMuted?: boolean - labels: ComAtprotoLabelDefs.Label[] | undefined + moderation: ModerationBehavior style?: StyleProp<ViewStyle> containerStyle?: StyleProp<ViewStyle> }>) { const pal = usePalette('default') const [override, setOverride] = React.useState(false) - const store = useStores() - const labelPref = store.preferences.getLabelPreference(labels) - if (!isMuted && labelPref.pref === 'show') { + if ( + moderation.behavior === ModerationBehaviorCode.Show || + moderation.behavior === ModerationBehaviorCode.Warn || + moderation.behavior === ModerationBehaviorCode.WarnImages + ) { return ( <View testID={testID} style={style}> {children} @@ -39,7 +38,7 @@ export function ContentHider({ ) } - if (labelPref.pref === 'hide') { + if (moderation.behavior === ModerationBehaviorCode.Hide) { return null } @@ -52,11 +51,7 @@ export function ContentHider({ override && styles.descriptionOpen, ]}> <Text type="md" style={pal.textLight}> - {isMuted ? ( - <>Post from an account you muted.</> - ) : ( - <>Warning: {labelPref.desc.warning || labelPref.desc.title}</> - )} + {moderation.reason || 'Content warning'} </Text> <TouchableOpacity style={styles.showBtn} diff --git a/src/view/com/util/moderation/PostHider.tsx b/src/view/com/util/moderation/PostHider.tsx index bafc7aecf..b3c4c9593 100644 --- a/src/view/com/util/moderation/PostHider.tsx +++ b/src/view/com/util/moderation/PostHider.tsx @@ -6,77 +6,72 @@ import { View, ViewStyle, } from 'react-native' -import {ComAtprotoLabelDefs} from '@atproto/api' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {usePalette} from 'lib/hooks/usePalette' import {Link} from '../Link' import {Text} from '../text/Text' import {addStyle} from 'lib/styles' -import {useStores} from 'state/index' +import {ModerationBehaviorCode, ModerationBehavior} from 'lib/labeling/types' export function PostHider({ testID, href, - isMuted, - labels, + moderation, style, children, }: React.PropsWithChildren<{ testID?: string - href: string - isMuted: boolean | undefined - labels: ComAtprotoLabelDefs.Label[] | undefined + href?: string + moderation: ModerationBehavior style: StyleProp<ViewStyle> }>) { - const store = useStores() const pal = usePalette('default') const [override, setOverride] = React.useState(false) const bg = override ? pal.viewLight : pal.view - const labelPref = store.preferences.getLabelPreference(labels) - if (labelPref.pref === 'hide') { - return <></> + if (moderation.behavior === ModerationBehaviorCode.Hide) { + return null } - if (!isMuted) { - // NOTE: any further label enforcement should occur in ContentContainer + if (moderation.behavior === ModerationBehaviorCode.Warn) { return ( - <Link testID={testID} style={style} href={href} noFeedback> - {children} - </Link> + <> + <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)}> + <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> + )} + </> ) } + // NOTE: any further label enforcement should occur in ContentContainer return ( - <> - <View style={[styles.description, bg, pal.border]}> - <FontAwesomeIcon - icon={['far', 'eye-slash']} - style={[styles.icon, pal.text]} - /> - <Text type="md" style={pal.textLight}> - Post from an account you muted. - </Text> - <TouchableOpacity - style={styles.showBtn} - onPress={() => setOverride(v => !v)}> - <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> + {children} + </Link> ) } diff --git a/src/view/com/util/moderation/ProfileHeaderLabels.tsx b/src/view/com/util/moderation/ProfileHeaderLabels.tsx deleted file mode 100644 index c6fbfaf6b..000000000 --- a/src/view/com/util/moderation/ProfileHeaderLabels.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react' -import {StyleSheet, View} from 'react-native' -import {ComAtprotoLabelDefs} from '@atproto/api' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {Text} from '../text/Text' -import {usePalette} from 'lib/hooks/usePalette' -import {getLabelValueGroup} from 'lib/labeling/helpers' - -export function ProfileHeaderLabels({ - labels, -}: { - labels: ComAtprotoLabelDefs.Label[] | undefined -}) { - const palErr = usePalette('error') - if (!labels?.length) { - return null - } - return ( - <> - {labels.map((label, i) => { - const labelGroup = getLabelValueGroup(label?.val || '') - return ( - <View - key={`${label.val}-${i}`} - 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 for{' '} - {(labelGroup.warning || labelGroup.title).toLocaleLowerCase()}. - </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/ProfileHeaderWarnings.tsx b/src/view/com/util/moderation/ProfileHeaderWarnings.tsx new file mode 100644 index 000000000..7a1a8e295 --- /dev/null +++ b/src/view/com/util/moderation/ProfileHeaderWarnings.tsx @@ -0,0 +1,44 @@ +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 new file mode 100644 index 000000000..2e7b07e1a --- /dev/null +++ b/src/view/com/util/moderation/ScreenHider.tsx @@ -0,0 +1,129 @@ +import React from 'react' +import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' +import {useNavigation} from '@react-navigation/native' +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' + +export function ScreenHider({ + testID, + screenDescription, + moderation, + style, + containerStyle, + children, +}: React.PropsWithChildren<{ + testID?: string + screenDescription: string + moderation: ModerationBehavior + style?: StyleProp<ViewStyle> + containerStyle?: StyleProp<ViewStyle> +}>) { + 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) { + return ( + <View testID={testID} style={style}> + {children} + </View> + ) + } + + return ( + <View style={[styles.container, pal.view, containerStyle]}> + <View style={styles.iconContainer}> + <View style={[styles.icon, palInverted.view]}> + <FontAwesomeIcon + icon="exclamation" + style={pal.textInverted as FontAwesomeIconStyle} + size={24} + /> + </View> + </View> + <Text type="title-2xl" style={[styles.title, pal.text]}> + Content Warning + </Text> + <Text type="2xl" style={[styles.description, pal.textLight]}> + This {screenDescription} has been flagged:{' '} + {moderation.reason || 'Content warning'} + </Text> + {!isDesktopWeb && <View style={styles.spacer} />} + <View style={styles.btnContainer}> + <Button type="inverted" onPress={onPressBack} style={styles.btn}> + <Text type="button-lg" style={pal.textInverted}> + Go back + </Text> + </Button> + {!moderation.noOverride && ( + <Button + type="default" + onPress={() => setOverride(v => !v)} + style={styles.btn}> + <Text type="button-lg" style={pal.text}> + Show anyway + </Text> + </Button> + )} + </View> + </View> + ) +} + +const styles = StyleSheet.create({ + spacer: { + flex: 1, + }, + container: { + flex: 1, + paddingTop: 100, + paddingBottom: 150, + }, + iconContainer: { + alignItems: 'center', + marginBottom: 10, + }, + icon: { + borderRadius: 25, + width: 50, + height: 50, + alignItems: 'center', + justifyContent: 'center', + }, + title: { + textAlign: 'center', + marginBottom: 10, + }, + description: { + marginBottom: 10, + paddingHorizontal: 20, + textAlign: 'center', + }, + btnContainer: { + flexDirection: 'row', + justifyContent: 'center', + marginVertical: 10, + gap: 10, + }, + btn: { + paddingHorizontal: 20, + paddingVertical: 14, + }, +}) |