[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 lintzio/stable
parent
7a176b3fdf
commit
b756a27958
|
@ -6,7 +6,6 @@ export const ILLEGAL_LABEL_GROUP: LabelValGroup = {
|
||||||
title: 'Illegal Content',
|
title: 'Illegal Content',
|
||||||
warning: 'Illegal Content',
|
warning: 'Illegal Content',
|
||||||
values: ['csam', 'dmca-violation', 'nudity-nonconsentual'],
|
values: ['csam', 'dmca-violation', 'nudity-nonconsentual'],
|
||||||
imagesOnly: false,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ALWAYS_FILTER_LABEL_GROUP: LabelValGroup = {
|
export const ALWAYS_FILTER_LABEL_GROUP: LabelValGroup = {
|
||||||
|
@ -14,7 +13,6 @@ export const ALWAYS_FILTER_LABEL_GROUP: LabelValGroup = {
|
||||||
title: 'Content Warning',
|
title: 'Content Warning',
|
||||||
warning: 'Content Warning',
|
warning: 'Content Warning',
|
||||||
values: ['!filter'],
|
values: ['!filter'],
|
||||||
imagesOnly: false,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ALWAYS_WARN_LABEL_GROUP: LabelValGroup = {
|
export const ALWAYS_WARN_LABEL_GROUP: LabelValGroup = {
|
||||||
|
@ -22,7 +20,6 @@ export const ALWAYS_WARN_LABEL_GROUP: LabelValGroup = {
|
||||||
title: 'Content Warning',
|
title: 'Content Warning',
|
||||||
warning: 'Content Warning',
|
warning: 'Content Warning',
|
||||||
values: ['!warn'],
|
values: ['!warn'],
|
||||||
imagesOnly: false,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UNKNOWN_LABEL_GROUP: LabelValGroup = {
|
export const UNKNOWN_LABEL_GROUP: LabelValGroup = {
|
||||||
|
@ -30,7 +27,6 @@ export const UNKNOWN_LABEL_GROUP: LabelValGroup = {
|
||||||
title: 'Unknown Label',
|
title: 'Unknown Label',
|
||||||
warning: 'Content Warning',
|
warning: 'Content Warning',
|
||||||
values: [],
|
values: [],
|
||||||
imagesOnly: false,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CONFIGURABLE_LABEL_GROUPS: Record<
|
export const CONFIGURABLE_LABEL_GROUPS: Record<
|
||||||
|
@ -43,7 +39,7 @@ export const CONFIGURABLE_LABEL_GROUPS: Record<
|
||||||
subtitle: 'i.e. Pornography',
|
subtitle: 'i.e. Pornography',
|
||||||
warning: 'Sexually Explicit',
|
warning: 'Sexually Explicit',
|
||||||
values: ['porn'],
|
values: ['porn'],
|
||||||
imagesOnly: false, // apply to whole thing
|
isAdultImagery: true,
|
||||||
},
|
},
|
||||||
nudity: {
|
nudity: {
|
||||||
id: 'nudity',
|
id: 'nudity',
|
||||||
|
@ -51,7 +47,7 @@ export const CONFIGURABLE_LABEL_GROUPS: Record<
|
||||||
subtitle: 'Including non-sexual and artistic',
|
subtitle: 'Including non-sexual and artistic',
|
||||||
warning: 'Nudity',
|
warning: 'Nudity',
|
||||||
values: ['nudity'],
|
values: ['nudity'],
|
||||||
imagesOnly: true,
|
isAdultImagery: true,
|
||||||
},
|
},
|
||||||
suggestive: {
|
suggestive: {
|
||||||
id: 'suggestive',
|
id: 'suggestive',
|
||||||
|
@ -59,7 +55,7 @@ export const CONFIGURABLE_LABEL_GROUPS: Record<
|
||||||
subtitle: 'Does not include nudity',
|
subtitle: 'Does not include nudity',
|
||||||
warning: 'Sexually Suggestive',
|
warning: 'Sexually Suggestive',
|
||||||
values: ['sexual'],
|
values: ['sexual'],
|
||||||
imagesOnly: true,
|
isAdultImagery: true,
|
||||||
},
|
},
|
||||||
gore: {
|
gore: {
|
||||||
id: 'gore',
|
id: 'gore',
|
||||||
|
@ -67,14 +63,13 @@ export const CONFIGURABLE_LABEL_GROUPS: Record<
|
||||||
subtitle: 'Gore, self-harm, torture',
|
subtitle: 'Gore, self-harm, torture',
|
||||||
warning: 'Violence',
|
warning: 'Violence',
|
||||||
values: ['gore', 'self-harm', 'torture'],
|
values: ['gore', 'self-harm', 'torture'],
|
||||||
imagesOnly: true,
|
isAdultImagery: true,
|
||||||
},
|
},
|
||||||
hate: {
|
hate: {
|
||||||
id: 'hate',
|
id: 'hate',
|
||||||
title: 'Political Hate-Groups',
|
title: 'Political Hate-Groups',
|
||||||
warning: 'Hate',
|
warning: 'Hate',
|
||||||
values: ['icon-kkk', 'icon-nazi', 'icon-intolerant', 'behavior-intolerant'],
|
values: ['icon-kkk', 'icon-nazi', 'icon-intolerant', 'behavior-intolerant'],
|
||||||
imagesOnly: false,
|
|
||||||
},
|
},
|
||||||
spam: {
|
spam: {
|
||||||
id: 'spam',
|
id: 'spam',
|
||||||
|
@ -82,7 +77,6 @@ export const CONFIGURABLE_LABEL_GROUPS: Record<
|
||||||
subtitle: 'Excessive low-quality posts',
|
subtitle: 'Excessive low-quality posts',
|
||||||
warning: 'Spam',
|
warning: 'Spam',
|
||||||
values: ['spam'],
|
values: ['spam'],
|
||||||
imagesOnly: false,
|
|
||||||
},
|
},
|
||||||
impersonation: {
|
impersonation: {
|
||||||
id: 'impersonation',
|
id: 'impersonation',
|
||||||
|
@ -90,6 +84,5 @@ export const CONFIGURABLE_LABEL_GROUPS: Record<
|
||||||
subtitle: 'Accounts falsely claiming to be people or orgs',
|
subtitle: 'Accounts falsely claiming to be people or orgs',
|
||||||
warning: 'Impersonation',
|
warning: 'Impersonation',
|
||||||
values: ['impersonation'],
|
values: ['impersonation'],
|
||||||
imagesOnly: false,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,12 +137,12 @@ export function getPostModeration(
|
||||||
|
|
||||||
// warning cases
|
// warning cases
|
||||||
if (postPref.pref === 'warn') {
|
if (postPref.pref === 'warn') {
|
||||||
if (postPref.desc.imagesOnly) {
|
if (postPref.desc.isAdultImagery) {
|
||||||
return {
|
return {
|
||||||
avatar,
|
avatar,
|
||||||
list: warnContent(postPref.desc.warning), // TODO make warnImages when there's time
|
list: warnImages(postPref.desc.warning),
|
||||||
thread: warnContent(postPref.desc.warning), // TODO make warnImages when there's time
|
thread: warnImages(postPref.desc.warning),
|
||||||
view: warnContent(postPref.desc.warning), // TODO make warnImages when there's time
|
view: warnImages(postPref.desc.warning),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
@ -401,10 +401,9 @@ function warnContent(reason: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
function warnImages(reason: string) {
|
||||||
// function warnImages(reason: string) {
|
return {
|
||||||
// return {
|
behavior: ModerationBehaviorCode.WarnImages,
|
||||||
// behavior: ModerationBehaviorCode.WarnImages,
|
reason,
|
||||||
// reason,
|
}
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ export interface LabelValGroup {
|
||||||
| 'always-warn'
|
| 'always-warn'
|
||||||
| 'unknown'
|
| 'unknown'
|
||||||
title: string
|
title: string
|
||||||
imagesOnly: boolean
|
isAdultImagery?: boolean
|
||||||
subtitle?: string
|
subtitle?: string
|
||||||
warning: string
|
warning: string
|
||||||
values: string[]
|
values: string[]
|
||||||
|
|
|
@ -10,15 +10,16 @@ import {
|
||||||
ALWAYS_FILTER_LABEL_GROUP,
|
ALWAYS_FILTER_LABEL_GROUP,
|
||||||
ALWAYS_WARN_LABEL_GROUP,
|
ALWAYS_WARN_LABEL_GROUP,
|
||||||
} from 'lib/labeling/const'
|
} from 'lib/labeling/const'
|
||||||
|
import {isIOS} from 'platform/detection'
|
||||||
|
|
||||||
const deviceLocales = getLocales()
|
const deviceLocales = getLocales()
|
||||||
|
|
||||||
export type LabelPreference = 'show' | 'warn' | 'hide'
|
export type LabelPreference = 'show' | 'warn' | 'hide'
|
||||||
|
|
||||||
export class LabelPreferencesModel {
|
export class LabelPreferencesModel {
|
||||||
nsfw: LabelPreference = 'warn'
|
nsfw: LabelPreference = 'hide'
|
||||||
nudity: LabelPreference = 'show'
|
nudity: LabelPreference = 'warn'
|
||||||
suggestive: LabelPreference = 'show'
|
suggestive: LabelPreference = 'warn'
|
||||||
gore: LabelPreference = 'warn'
|
gore: LabelPreference = 'warn'
|
||||||
hate: LabelPreference = 'hide'
|
hate: LabelPreference = 'hide'
|
||||||
spam: LabelPreference = 'hide'
|
spam: LabelPreference = 'hide'
|
||||||
|
@ -30,6 +31,7 @@ export class LabelPreferencesModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PreferencesModel {
|
export class PreferencesModel {
|
||||||
|
adultContentEnabled = !isIOS
|
||||||
contentLanguages: string[] =
|
contentLanguages: string[] =
|
||||||
deviceLocales?.map?.(locale => locale.languageCode) || []
|
deviceLocales?.map?.(locale => locale.languageCode) || []
|
||||||
contentLabels = new LabelPreferencesModel()
|
contentLabels = new LabelPreferencesModel()
|
||||||
|
@ -102,7 +104,9 @@ export class PreferencesModel {
|
||||||
} else if (group.id === 'always-filter') {
|
} else if (group.id === 'always-filter') {
|
||||||
return {pref: 'hide', desc: ALWAYS_FILTER_LABEL_GROUP}
|
return {pref: 'hide', desc: ALWAYS_FILTER_LABEL_GROUP}
|
||||||
} else if (group.id === 'always-warn') {
|
} else if (group.id === 'always-warn') {
|
||||||
return {pref: 'warn', desc: ALWAYS_WARN_LABEL_GROUP}
|
res.pref = 'warn'
|
||||||
|
res.desc = ALWAYS_WARN_LABEL_GROUP
|
||||||
|
continue
|
||||||
} else if (group.id === 'unknown') {
|
} else if (group.id === 'unknown') {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -115,6 +119,9 @@ export class PreferencesModel {
|
||||||
res.desc = group
|
res.desc = group
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (res.desc.isAdultImagery && !this.adultContentEnabled) {
|
||||||
|
res.pref = 'hide'
|
||||||
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,10 +24,22 @@ export function Component({}: {}) {
|
||||||
<View testID="contentModerationModal" style={[pal.view, styles.container]}>
|
<View testID="contentModerationModal" style={[pal.view, styles.container]}>
|
||||||
<Text style={[pal.text, styles.title]}>Content Moderation</Text>
|
<Text style={[pal.text, styles.title]}>Content Moderation</Text>
|
||||||
<ScrollView style={styles.scrollContainer}>
|
<ScrollView style={styles.scrollContainer}>
|
||||||
<ContentLabelPref group="nsfw" />
|
<ContentLabelPref
|
||||||
<ContentLabelPref group="nudity" />
|
group="nsfw"
|
||||||
<ContentLabelPref group="suggestive" />
|
disabled={!store.preferences.adultContentEnabled}
|
||||||
<ContentLabelPref group="gore" />
|
/>
|
||||||
|
<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="hate" />
|
||||||
<ContentLabelPref group="spam" />
|
<ContentLabelPref group="spam" />
|
||||||
<ContentLabelPref group="impersonation" />
|
<ContentLabelPref group="impersonation" />
|
||||||
|
@ -55,7 +67,13 @@ export function Component({}: {}) {
|
||||||
|
|
||||||
// TODO: Refactor this component to pass labels down to each tab
|
// TODO: Refactor this component to pass labels down to each tab
|
||||||
const ContentLabelPref = observer(
|
const ContentLabelPref = observer(
|
||||||
({group}: {group: keyof typeof CONFIGURABLE_LABEL_GROUPS}) => {
|
({
|
||||||
|
group,
|
||||||
|
disabled,
|
||||||
|
}: {
|
||||||
|
group: keyof typeof CONFIGURABLE_LABEL_GROUPS
|
||||||
|
disabled?: boolean
|
||||||
|
}) => {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
return (
|
return (
|
||||||
|
@ -70,11 +88,17 @@ const ContentLabelPref = observer(
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<SelectGroup
|
{disabled ? (
|
||||||
current={store.preferences.contentLabels[group]}
|
<Text type="sm-bold" style={pal.textLight}>
|
||||||
onChange={v => store.preferences.setContentLabelPref(group, v)}
|
Hide
|
||||||
group={group}
|
</Text>
|
||||||
/>
|
) : (
|
||||||
|
<SelectGroup
|
||||||
|
current={store.preferences.contentLabels[group]}
|
||||||
|
onChange={v => store.preferences.setContentLabelPref(group, v)}
|
||||||
|
group={group}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {PostEmbeds} from '../util/post-embeds'
|
||||||
import {PostCtrls} from '../util/PostCtrls'
|
import {PostCtrls} from '../util/PostCtrls'
|
||||||
import {PostHider} from '../util/moderation/PostHider'
|
import {PostHider} from '../util/moderation/PostHider'
|
||||||
import {ContentHider} from '../util/moderation/ContentHider'
|
import {ContentHider} from '../util/moderation/ContentHider'
|
||||||
|
import {ImageHider} from '../util/moderation/ImageHider'
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {formatCount} from '../util/numeric/format'
|
import {formatCount} from '../util/numeric/format'
|
||||||
|
@ -234,7 +235,9 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : 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>
|
</ContentHider>
|
||||||
<View style={[s.mt2, s.mb10]}>
|
<View style={[s.mt2, s.mb10]}>
|
||||||
<Text style={pal.textLight}>{niceDate(item.post.indexedAt)}</Text>
|
<Text style={pal.textLight}>{niceDate(item.post.indexedAt)}</Text>
|
||||||
|
@ -366,7 +369,9 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : 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>
|
</ContentHider>
|
||||||
<PostCtrls
|
<PostCtrls
|
||||||
itemUri={itemUri}
|
itemUri={itemUri}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import {PostEmbeds} from '../util/post-embeds'
|
||||||
import {PostCtrls} from '../util/PostCtrls'
|
import {PostCtrls} from '../util/PostCtrls'
|
||||||
import {PostHider} from '../util/moderation/PostHider'
|
import {PostHider} from '../util/moderation/PostHider'
|
||||||
import {ContentHider} from '../util/moderation/ContentHider'
|
import {ContentHider} from '../util/moderation/ContentHider'
|
||||||
|
import {ImageHider} from '../util/moderation/ImageHider'
|
||||||
import {Text} from '../util/text/Text'
|
import {Text} from '../util/text/Text'
|
||||||
import {RichText} from '../util/text/RichText'
|
import {RichText} from '../util/text/RichText'
|
||||||
import * as Toast from '../util/Toast'
|
import * as Toast from '../util/Toast'
|
||||||
|
@ -258,7 +259,9 @@ const PostLoaded = observer(
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : 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>
|
</ContentHider>
|
||||||
<PostCtrls
|
<PostCtrls
|
||||||
itemUri={itemUri}
|
itemUri={itemUri}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {PostCtrls} from '../util/PostCtrls'
|
||||||
import {PostEmbeds} from '../util/post-embeds'
|
import {PostEmbeds} from '../util/post-embeds'
|
||||||
import {PostHider} from '../util/moderation/PostHider'
|
import {PostHider} from '../util/moderation/PostHider'
|
||||||
import {ContentHider} from '../util/moderation/ContentHider'
|
import {ContentHider} from '../util/moderation/ContentHider'
|
||||||
|
import {ImageHider} from '../util/moderation/ImageHider'
|
||||||
import {RichText} from '../util/text/RichText'
|
import {RichText} from '../util/text/RichText'
|
||||||
import * as Toast from '../util/Toast'
|
import * as Toast from '../util/Toast'
|
||||||
import {UserAvatar} from '../util/UserAvatar'
|
import {UserAvatar} from '../util/UserAvatar'
|
||||||
|
@ -243,7 +244,9 @@ export const FeedItem = observer(function ({
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : 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>
|
</ContentHider>
|
||||||
<PostCtrls
|
<PostCtrls
|
||||||
style={styles.ctrls}
|
style={styles.ctrls}
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {
|
import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
||||||
StyleProp,
|
|
||||||
StyleSheet,
|
|
||||||
TouchableOpacity,
|
|
||||||
View,
|
|
||||||
ViewStyle,
|
|
||||||
} from 'react-native'
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {Text} from '../text/Text'
|
import {Text} from '../text/Text'
|
||||||
import {addStyle} from 'lib/styles'
|
import {addStyle} from 'lib/styles'
|
||||||
|
@ -25,6 +19,12 @@ export function ContentHider({
|
||||||
}>) {
|
}>) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const [override, setOverride] = React.useState(false)
|
const [override, setOverride] = React.useState(false)
|
||||||
|
const onPressShow = React.useCallback(() => {
|
||||||
|
setOverride(true)
|
||||||
|
}, [setOverride])
|
||||||
|
const onPressHide = React.useCallback(() => {
|
||||||
|
setOverride(false)
|
||||||
|
}, [setOverride])
|
||||||
|
|
||||||
if (
|
if (
|
||||||
moderation.behavior === ModerationBehaviorCode.Show ||
|
moderation.behavior === ModerationBehaviorCode.Show ||
|
||||||
|
@ -44,7 +44,15 @@ export function ContentHider({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, pal.view, pal.border, containerStyle]}>
|
<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={[
|
style={[
|
||||||
styles.description,
|
styles.description,
|
||||||
pal.viewLight,
|
pal.viewLight,
|
||||||
|
@ -53,21 +61,12 @@ export function ContentHider({
|
||||||
<Text type="md" style={pal.textLight}>
|
<Text type="md" style={pal.textLight}>
|
||||||
{moderation.reason || 'Content warning'}
|
{moderation.reason || 'Content warning'}
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<View style={styles.showBtn}>
|
||||||
style={styles.showBtn}
|
<Text type="md-medium" style={pal.link}>
|
||||||
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}>
|
|
||||||
{override ? 'Hide' : 'Show'}
|
{override ? 'Hide' : 'Show'}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</View>
|
||||||
</View>
|
</Pressable>
|
||||||
{override && (
|
{override && (
|
||||||
<View style={[styles.childrenContainer, pal.border]}>
|
<View style={[styles.childrenContainer, pal.border]}>
|
||||||
<View testID={testID} style={addStyle(style, styles.child)}>
|
<View testID={testID} style={addStyle(style, styles.child)}>
|
||||||
|
|
|
@ -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…
Reference in New Issue