diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/lib/labeling/const.ts | 15 | ||||
-rw-r--r-- | src/lib/labeling/helpers.ts | 21 | ||||
-rw-r--r-- | src/lib/labeling/types.ts | 2 | ||||
-rw-r--r-- | src/state/models/ui/preferences.ts | 15 | ||||
-rw-r--r-- | src/view/com/modals/ContentFilteringSettings.tsx | 44 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThreadItem.tsx | 9 | ||||
-rw-r--r-- | src/view/com/post/Post.tsx | 5 | ||||
-rw-r--r-- | src/view/com/posts/FeedItem.tsx | 5 | ||||
-rw-r--r-- | src/view/com/util/moderation/ContentHider.tsx | 41 | ||||
-rw-r--r-- | src/view/com/util/moderation/ImageHider.tsx | 128 |
10 files changed, 223 insertions, 62 deletions
diff --git a/src/lib/labeling/const.ts b/src/lib/labeling/const.ts index 54cc732b9..2a9b921db 100644 --- a/src/lib/labeling/const.ts +++ b/src/lib/labeling/const.ts @@ -6,7 +6,6 @@ export const ILLEGAL_LABEL_GROUP: LabelValGroup = { title: 'Illegal Content', warning: 'Illegal Content', values: ['csam', 'dmca-violation', 'nudity-nonconsentual'], - imagesOnly: false, } export const ALWAYS_FILTER_LABEL_GROUP: LabelValGroup = { @@ -14,7 +13,6 @@ export const ALWAYS_FILTER_LABEL_GROUP: LabelValGroup = { title: 'Content Warning', warning: 'Content Warning', values: ['!filter'], - imagesOnly: false, } export const ALWAYS_WARN_LABEL_GROUP: LabelValGroup = { @@ -22,7 +20,6 @@ export const ALWAYS_WARN_LABEL_GROUP: LabelValGroup = { title: 'Content Warning', warning: 'Content Warning', values: ['!warn'], - imagesOnly: false, } export const UNKNOWN_LABEL_GROUP: LabelValGroup = { @@ -30,7 +27,6 @@ export const UNKNOWN_LABEL_GROUP: LabelValGroup = { title: 'Unknown Label', warning: 'Content Warning', values: [], - imagesOnly: false, } export const CONFIGURABLE_LABEL_GROUPS: Record< @@ -43,7 +39,7 @@ export const CONFIGURABLE_LABEL_GROUPS: Record< subtitle: 'i.e. Pornography', warning: 'Sexually Explicit', values: ['porn'], - imagesOnly: false, // apply to whole thing + isAdultImagery: true, }, nudity: { id: 'nudity', @@ -51,7 +47,7 @@ export const CONFIGURABLE_LABEL_GROUPS: Record< subtitle: 'Including non-sexual and artistic', warning: 'Nudity', values: ['nudity'], - imagesOnly: true, + isAdultImagery: true, }, suggestive: { id: 'suggestive', @@ -59,7 +55,7 @@ export const CONFIGURABLE_LABEL_GROUPS: Record< subtitle: 'Does not include nudity', warning: 'Sexually Suggestive', values: ['sexual'], - imagesOnly: true, + isAdultImagery: true, }, gore: { id: 'gore', @@ -67,14 +63,13 @@ export const CONFIGURABLE_LABEL_GROUPS: Record< subtitle: 'Gore, self-harm, torture', warning: 'Violence', values: ['gore', 'self-harm', 'torture'], - imagesOnly: true, + isAdultImagery: true, }, hate: { id: 'hate', title: 'Political Hate-Groups', warning: 'Hate', values: ['icon-kkk', 'icon-nazi', 'icon-intolerant', 'behavior-intolerant'], - imagesOnly: false, }, spam: { id: 'spam', @@ -82,7 +77,6 @@ export const CONFIGURABLE_LABEL_GROUPS: Record< subtitle: 'Excessive low-quality posts', warning: 'Spam', values: ['spam'], - imagesOnly: false, }, impersonation: { id: 'impersonation', @@ -90,6 +84,5 @@ export const CONFIGURABLE_LABEL_GROUPS: Record< subtitle: 'Accounts falsely claiming to be people or orgs', warning: 'Impersonation', values: ['impersonation'], - imagesOnly: false, }, } diff --git a/src/lib/labeling/helpers.ts b/src/lib/labeling/helpers.ts index 71ea43c08..baac0ed5a 100644 --- a/src/lib/labeling/helpers.ts +++ b/src/lib/labeling/helpers.ts @@ -137,12 +137,12 @@ export function getPostModeration( // warning cases if (postPref.pref === 'warn') { - if (postPref.desc.imagesOnly) { + if (postPref.desc.isAdultImagery) { return { avatar, - list: warnContent(postPref.desc.warning), // TODO make warnImages when there's time - thread: warnContent(postPref.desc.warning), // TODO make warnImages when there's time - view: warnContent(postPref.desc.warning), // TODO make warnImages when there's time + list: warnImages(postPref.desc.warning), + thread: warnImages(postPref.desc.warning), + view: warnImages(postPref.desc.warning), } } return { @@ -401,10 +401,9 @@ function warnContent(reason: string) { } } -// TODO -// function warnImages(reason: string) { -// return { -// behavior: ModerationBehaviorCode.WarnImages, -// reason, -// } -// } +function warnImages(reason: string) { + return { + behavior: ModerationBehaviorCode.WarnImages, + reason, + } +} diff --git a/src/lib/labeling/types.ts b/src/lib/labeling/types.ts index 123c5d1f3..078043076 100644 --- a/src/lib/labeling/types.ts +++ b/src/lib/labeling/types.ts @@ -11,7 +11,7 @@ export interface LabelValGroup { | 'always-warn' | 'unknown' title: string - imagesOnly: boolean + isAdultImagery?: boolean subtitle?: string warning: string values: string[] diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts index 7b41fa746..fcd33af8e 100644 --- a/src/state/models/ui/preferences.ts +++ b/src/state/models/ui/preferences.ts @@ -10,15 +10,16 @@ import { ALWAYS_FILTER_LABEL_GROUP, ALWAYS_WARN_LABEL_GROUP, } from 'lib/labeling/const' +import {isIOS} from 'platform/detection' const deviceLocales = getLocales() export type LabelPreference = 'show' | 'warn' | 'hide' export class LabelPreferencesModel { - nsfw: LabelPreference = 'warn' - nudity: LabelPreference = 'show' - suggestive: LabelPreference = 'show' + nsfw: LabelPreference = 'hide' + nudity: LabelPreference = 'warn' + suggestive: LabelPreference = 'warn' gore: LabelPreference = 'warn' hate: LabelPreference = 'hide' spam: LabelPreference = 'hide' @@ -30,6 +31,7 @@ export class LabelPreferencesModel { } export class PreferencesModel { + adultContentEnabled = !isIOS contentLanguages: string[] = deviceLocales?.map?.(locale => locale.languageCode) || [] contentLabels = new LabelPreferencesModel() @@ -102,7 +104,9 @@ export class PreferencesModel { } else if (group.id === 'always-filter') { return {pref: 'hide', desc: ALWAYS_FILTER_LABEL_GROUP} } 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') { continue } @@ -115,6 +119,9 @@ export class PreferencesModel { res.desc = group } } + if (res.desc.isAdultImagery && !this.adultContentEnabled) { + res.pref = 'hide' + } return res } } diff --git a/src/view/com/modals/ContentFilteringSettings.tsx b/src/view/com/modals/ContentFilteringSettings.tsx index cfba2575a..30b465562 100644 --- a/src/view/com/modals/ContentFilteringSettings.tsx +++ b/src/view/com/modals/ContentFilteringSettings.tsx @@ -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> ) }, diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index d657c92c3..563a3ead6 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -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} diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index af78a951b..90698ab31 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -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} diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 10fc775c5..ff1f46db1 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -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} diff --git a/src/view/com/util/moderation/ContentHider.tsx b/src/view/com/util/moderation/ContentHider.tsx index 0f3e47d61..ac5c8395d 100644 --- a/src/view/com/util/moderation/ContentHider.tsx +++ b/src/view/com/util/moderation/ContentHider.tsx @@ -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)}> diff --git a/src/view/com/util/moderation/ImageHider.tsx b/src/view/com/util/moderation/ImageHider.tsx new file mode 100644 index 000000000..b42c6397d --- /dev/null +++ b/src/view/com/util/moderation/ImageHider.tsx @@ -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, + }, +}) |