diff --git a/src/lib/labeling/const.ts b/src/lib/labeling/const.ts index 54cc732b..2a9b921d 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 71ea43c0..baac0ed5 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 123c5d1f..07804307 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 7b41fa74..fcd33af8 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 cfba2575..30b46556 100644 --- a/src/view/com/modals/ContentFilteringSettings.tsx +++ b/src/view/com/modals/ContentFilteringSettings.tsx @@ -24,10 +24,22 @@ export function Component({}: {}) { Content Moderation - - - - + + + + @@ -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( )} - store.preferences.setContentLabelPref(group, v)} - group={group} - /> + {disabled ? ( + + Hide + + ) : ( + store.preferences.setContentLabelPref(group, v)} + group={group} + /> + )} ) }, diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index d657c92c..563a3ead 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({ /> ) : undefined} - + + + {niceDate(item.post.indexedAt)} @@ -366,7 +369,9 @@ export const PostThreadItem = observer(function PostThreadItem({ /> ) : undefined} - + + + ) : undefined} - + + + ) : undefined} - + + + ) { 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 ( - + + {override && ( diff --git a/src/view/com/util/moderation/ImageHider.tsx b/src/view/com/util/moderation/ImageHider.tsx new file mode 100644 index 00000000..b42c6397 --- /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 + containerStyle?: StyleProp +}>) { + 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 ( + + {children} + + ) + } + + if (moderation.behavior === ModerationBehaviorCode.Hide) { + return null + } + + return ( + + + {children} + + {override ? ( + + + Hide + + + ) : ( + <> + {isAndroid ? ( + /* android has an issue that breaks the blurview */ + /* see https://github.com/Kureev/react-native-blur/issues/486 */ + + ) : ( + + )} + + + + {moderation.reason || 'Content warning'} + + + Show + + + + + )} + + ) +} + +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, + }, +})