about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib/labeling/const.ts15
-rw-r--r--src/lib/labeling/helpers.ts21
-rw-r--r--src/lib/labeling/types.ts2
-rw-r--r--src/state/models/ui/preferences.ts15
-rw-r--r--src/view/com/modals/ContentFilteringSettings.tsx44
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx9
-rw-r--r--src/view/com/post/Post.tsx5
-rw-r--r--src/view/com/posts/FeedItem.tsx5
-rw-r--r--src/view/com/util/moderation/ContentHider.tsx41
-rw-r--r--src/view/com/util/moderation/ImageHider.tsx128
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,
+  },
+})