[APP-639] Improve nsfw handling & force hidden on iOS (#605)
* Identify adult content labels and handle them more specifically * Change adult content defaults to more conservative settings * Add an adultcontentenabled override that prohibits access on iOS * Improve usability of the content hider * Fix lint
This commit is contained in:
parent
7a176b3fdf
commit
b756a27958
10 changed files with 223 additions and 62 deletions
|
@ -24,10 +24,22 @@ export function Component({}: {}) {
|
|||
<View testID="contentModerationModal" style={[pal.view, styles.container]}>
|
||||
<Text style={[pal.text, styles.title]}>Content Moderation</Text>
|
||||
<ScrollView style={styles.scrollContainer}>
|
||||
<ContentLabelPref group="nsfw" />
|
||||
<ContentLabelPref group="nudity" />
|
||||
<ContentLabelPref group="suggestive" />
|
||||
<ContentLabelPref group="gore" />
|
||||
<ContentLabelPref
|
||||
group="nsfw"
|
||||
disabled={!store.preferences.adultContentEnabled}
|
||||
/>
|
||||
<ContentLabelPref
|
||||
group="nudity"
|
||||
disabled={!store.preferences.adultContentEnabled}
|
||||
/>
|
||||
<ContentLabelPref
|
||||
group="suggestive"
|
||||
disabled={!store.preferences.adultContentEnabled}
|
||||
/>
|
||||
<ContentLabelPref
|
||||
group="gore"
|
||||
disabled={!store.preferences.adultContentEnabled}
|
||||
/>
|
||||
<ContentLabelPref group="hate" />
|
||||
<ContentLabelPref group="spam" />
|
||||
<ContentLabelPref group="impersonation" />
|
||||
|
@ -55,7 +67,13 @@ export function Component({}: {}) {
|
|||
|
||||
// TODO: Refactor this component to pass labels down to each tab
|
||||
const ContentLabelPref = observer(
|
||||
({group}: {group: keyof typeof CONFIGURABLE_LABEL_GROUPS}) => {
|
||||
({
|
||||
group,
|
||||
disabled,
|
||||
}: {
|
||||
group: keyof typeof CONFIGURABLE_LABEL_GROUPS
|
||||
disabled?: boolean
|
||||
}) => {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
return (
|
||||
|
@ -70,11 +88,17 @@ const ContentLabelPref = observer(
|
|||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<SelectGroup
|
||||
current={store.preferences.contentLabels[group]}
|
||||
onChange={v => store.preferences.setContentLabelPref(group, v)}
|
||||
group={group}
|
||||
/>
|
||||
{disabled ? (
|
||||
<Text type="sm-bold" style={pal.textLight}>
|
||||
Hide
|
||||
</Text>
|
||||
) : (
|
||||
<SelectGroup
|
||||
current={store.preferences.contentLabels[group]}
|
||||
onChange={v => store.preferences.setContentLabelPref(group, v)}
|
||||
group={group}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
},
|
||||
|
|
|
@ -24,6 +24,7 @@ import {PostEmbeds} from '../util/post-embeds'
|
|||
import {PostCtrls} from '../util/PostCtrls'
|
||||
import {PostHider} from '../util/moderation/PostHider'
|
||||
import {ContentHider} from '../util/moderation/ContentHider'
|
||||
import {ImageHider} from '../util/moderation/ImageHider'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {formatCount} from '../util/numeric/format'
|
||||
|
@ -234,7 +235,9 @@ export const PostThreadItem = observer(function PostThreadItem({
|
|||
/>
|
||||
</View>
|
||||
) : undefined}
|
||||
<PostEmbeds embed={item.post.embed} style={s.mb10} />
|
||||
<ImageHider moderation={item.moderation.view} style={s.mb10}>
|
||||
<PostEmbeds embed={item.post.embed} style={s.mb10} />
|
||||
</ImageHider>
|
||||
</ContentHider>
|
||||
<View style={[s.mt2, s.mb10]}>
|
||||
<Text style={pal.textLight}>{niceDate(item.post.indexedAt)}</Text>
|
||||
|
@ -366,7 +369,9 @@ export const PostThreadItem = observer(function PostThreadItem({
|
|||
/>
|
||||
</View>
|
||||
) : undefined}
|
||||
<PostEmbeds embed={item.post.embed} style={s.mb10} />
|
||||
<ImageHider style={s.mb10} moderation={item.moderation.thread}>
|
||||
<PostEmbeds embed={item.post.embed} style={s.mb10} />
|
||||
</ImageHider>
|
||||
</ContentHider>
|
||||
<PostCtrls
|
||||
itemUri={itemUri}
|
||||
|
|
|
@ -23,6 +23,7 @@ import {PostEmbeds} from '../util/post-embeds'
|
|||
import {PostCtrls} from '../util/PostCtrls'
|
||||
import {PostHider} from '../util/moderation/PostHider'
|
||||
import {ContentHider} from '../util/moderation/ContentHider'
|
||||
import {ImageHider} from '../util/moderation/ImageHider'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {RichText} from '../util/text/RichText'
|
||||
import * as Toast from '../util/Toast'
|
||||
|
@ -258,7 +259,9 @@ const PostLoaded = observer(
|
|||
/>
|
||||
</View>
|
||||
) : undefined}
|
||||
<PostEmbeds embed={item.post.embed} style={s.mb10} />
|
||||
<ImageHider moderation={item.moderation.list} style={s.mb10}>
|
||||
<PostEmbeds embed={item.post.embed} style={s.mb10} />
|
||||
</ImageHider>
|
||||
</ContentHider>
|
||||
<PostCtrls
|
||||
itemUri={itemUri}
|
||||
|
|
|
@ -16,6 +16,7 @@ import {PostCtrls} from '../util/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 {RichText} from '../util/text/RichText'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
|
@ -243,7 +244,9 @@ export const FeedItem = observer(function ({
|
|||
/>
|
||||
</View>
|
||||
) : undefined}
|
||||
<PostEmbeds embed={item.post.embed} style={styles.embed} />
|
||||
<ImageHider moderation={item.moderation.list} style={styles.embed}>
|
||||
<PostEmbeds embed={item.post.embed} style={styles.embed} />
|
||||
</ImageHider>
|
||||
</ContentHider>
|
||||
<PostCtrls
|
||||
style={styles.ctrls}
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
import React from 'react'
|
||||
import {
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {Text} from '../text/Text'
|
||||
import {addStyle} from 'lib/styles'
|
||||
|
@ -25,6 +19,12 @@ export function ContentHider({
|
|||
}>) {
|
||||
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 ||
|
||||
|
@ -44,7 +44,15 @@ export function ContentHider({
|
|||
|
||||
return (
|
||||
<View style={[styles.container, pal.view, pal.border, containerStyle]}>
|
||||
<View
|
||||
<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,
|
||||
|
@ -53,21 +61,12 @@ export function ContentHider({
|
|||
<Text type="md" style={pal.textLight}>
|
||||
{moderation.reason || 'Content warning'}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.showBtn}
|
||||
onPress={() => setOverride(v => !v)}
|
||||
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'
|
||||
}>
|
||||
<Text type="md" style={pal.link}>
|
||||
<View style={styles.showBtn}>
|
||||
<Text type="md-medium" style={pal.link}>
|
||||
{override ? 'Hide' : 'Show'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
{override && (
|
||||
<View style={[styles.childrenContainer, pal.border]}>
|
||||
<View testID={testID} style={addStyle(style, styles.child)}>
|
||||
|
|
128
src/view/com/util/moderation/ImageHider.tsx
Normal file
128
src/view/com/util/moderation/ImageHider.tsx
Normal file
|
@ -0,0 +1,128 @@
|
|||
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 {BlurView} from '../BlurView'
|
||||
import {ModerationBehavior, ModerationBehaviorCode} from 'lib/labeling/types'
|
||||
import {isAndroid} from 'platform/detection'
|
||||
|
||||
export function ImageHider({
|
||||
testID,
|
||||
moderation,
|
||||
style,
|
||||
containerStyle,
|
||||
children,
|
||||
}: React.PropsWithChildren<{
|
||||
testID?: string
|
||||
moderation: ModerationBehavior
|
||||
style?: StyleProp<ViewStyle>
|
||||
containerStyle?: StyleProp<ViewStyle>
|
||||
}>) {
|
||||
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.WarnImages) {
|
||||
return (
|
||||
<View testID={testID} style={style}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
if (moderation.behavior === ModerationBehaviorCode.Hide) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container, containerStyle]}>
|
||||
<View testID={testID} style={style}>
|
||||
{children}
|
||||
</View>
|
||||
{override ? (
|
||||
<Pressable
|
||||
onPress={onPressHide}
|
||||
style={[styles.hideBtn, pal.view]}
|
||||
accessibilityLabel="Hide image"
|
||||
accessibilityHint="Rehides the image">
|
||||
<Text type="xl-bold" style={pal.link}>
|
||||
Hide
|
||||
</Text>
|
||||
</Pressable>
|
||||
) : (
|
||||
<>
|
||||
{isAndroid ? (
|
||||
/* android has an issue that breaks the blurview */
|
||||
/* see https://github.com/Kureev/react-native-blur/issues/486 */
|
||||
<View style={[pal.viewLight, styles.overlay, styles.coverView]} />
|
||||
) : (
|
||||
<BlurView
|
||||
style={[styles.overlay, styles.blurView]}
|
||||
blurType="light"
|
||||
blurAmount={100}
|
||||
reducedTransparencyFallbackColor="white"
|
||||
/>
|
||||
)}
|
||||
<View style={[styles.overlay, styles.info]}>
|
||||
<Pressable
|
||||
onPress={onPressShow}
|
||||
style={[styles.showBtn, pal.view]}
|
||||
accessibilityLabel="Show image"
|
||||
accessibilityHint="Shows image hidden based on your moderation settings">
|
||||
<Text type="xl" style={pal.text}>
|
||||
{moderation.reason || 'Content warning'}
|
||||
</Text>
|
||||
<Text type="xl-bold" style={pal.link}>
|
||||
Show
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: 'relative',
|
||||
marginBottom: 10,
|
||||
},
|
||||
overlay: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
blurView: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
coverView: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
info: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
showBtn: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
paddingHorizontal: 18,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 24,
|
||||
},
|
||||
hideBtn: {
|
||||
position: 'absolute',
|
||||
left: 8,
|
||||
bottom: 20,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 8,
|
||||
},
|
||||
})
|
Loading…
Add table
Add a link
Reference in a new issue