about summary refs log tree commit diff
path: root/src/view/com/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/util')
-rw-r--r--src/view/com/util/PostMeta.tsx2
-rw-r--r--src/view/com/util/UserAvatar.tsx17
-rw-r--r--src/view/com/util/UserBanner.tsx9
-rw-r--r--src/view/com/util/error/ErrorScreen.tsx2
-rw-r--r--src/view/com/util/moderation/ContentHider.tsx25
-rw-r--r--src/view/com/util/moderation/PostHider.tsx85
-rw-r--r--src/view/com/util/moderation/ProfileHeaderLabels.tsx55
-rw-r--r--src/view/com/util/moderation/ProfileHeaderWarnings.tsx44
-rw-r--r--src/view/com/util/moderation/ScreenHider.tsx129
9 files changed, 243 insertions, 125 deletions
diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx
index d9dd11e05..45651e4e5 100644
--- a/src/view/com/util/PostMeta.tsx
+++ b/src/view/com/util/PostMeta.tsx
@@ -97,7 +97,7 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
           <UserAvatar
             avatar={opts.authorAvatar}
             size={16}
-            hasWarning={opts.authorHasWarning}
+            // TODO moderation
           />
         </View>
       )}
diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
index 9c0fe9297..7f55bf773 100644
--- a/src/view/com/util/UserAvatar.tsx
+++ b/src/view/com/util/UserAvatar.tsx
@@ -13,8 +13,11 @@ import {useStores} from 'state/index'
 import {colors} from 'lib/styles'
 import {DropdownButton} from './forms/DropdownButton'
 import {usePalette} from 'lib/hooks/usePalette'
-import {isWeb} from 'platform/detection'
+import {isWeb, isAndroid} from 'platform/detection'
 import {Image as RNImage} from 'react-native-image-crop-picker'
+import {AvatarModeration} from 'lib/labeling/types'
+
+const BLUR_AMOUNT = isWeb ? 5 : 100
 
 function DefaultAvatar({size}: {size: number}) {
   return (
@@ -40,12 +43,12 @@ function DefaultAvatar({size}: {size: number}) {
 export function UserAvatar({
   size,
   avatar,
-  hasWarning,
+  moderation,
   onSelectNewAvatar,
 }: {
   size: number
   avatar?: string | null
-  hasWarning?: boolean
+  moderation?: AvatarModeration
   onSelectNewAvatar?: (img: RNImage | null) => void
 }) {
   const store = useStores()
@@ -114,7 +117,7 @@ export function UserAvatar({
   )
 
   const warning = useMemo(() => {
-    if (!hasWarning) {
+    if (!moderation?.warn) {
       return null
     }
     return (
@@ -126,7 +129,7 @@ export function UserAvatar({
         />
       </View>
     )
-  }, [hasWarning, size, pal])
+  }, [moderation?.warn, size, pal])
 
   // onSelectNewAvatar is only passed as prop on the EditProfile component
   return onSelectNewAvatar ? (
@@ -159,13 +162,15 @@ export function UserAvatar({
         />
       </View>
     </DropdownButton>
-  ) : avatar ? (
+  ) : avatar &&
+    !((moderation?.blur && isAndroid) /* android crashes with blur */) ? (
     <View style={{width: size, height: size}}>
       <HighPriorityImage
         testID="userAvatarImage"
         style={{width: size, height: size, borderRadius: Math.floor(size / 2)}}
         contentFit="cover"
         source={{uri: avatar}}
+        blurRadius={moderation?.blur ? BLUR_AMOUNT : 0}
       />
       {warning}
     </View>
diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx
index fcd66ca7a..14459bf77 100644
--- a/src/view/com/util/UserBanner.tsx
+++ b/src/view/com/util/UserBanner.tsx
@@ -13,13 +13,16 @@ import {
 } from 'lib/hooks/usePermissions'
 import {DropdownButton} from './forms/DropdownButton'
 import {usePalette} from 'lib/hooks/usePalette'
-import {isWeb} from 'platform/detection'
+import {AvatarModeration} from 'lib/labeling/types'
+import {isWeb, isAndroid} from 'platform/detection'
 
 export function UserBanner({
   banner,
+  moderation,
   onSelectNewBanner,
 }: {
   banner?: string | null
+  moderation?: AvatarModeration
   onSelectNewBanner?: (img: TImage | null) => void
 }) {
   const store = useStores()
@@ -107,12 +110,14 @@ export function UserBanner({
         />
       </View>
     </DropdownButton>
-  ) : banner ? (
+  ) : banner &&
+    !((moderation?.blur && isAndroid) /* android crashes with blur */) ? (
     <Image
       testID="userBannerImage"
       style={styles.bannerImage}
       resizeMode="cover"
       source={{uri: banner}}
+      blurRadius={moderation?.blur ? 100 : 0}
     />
   ) : (
     <View
diff --git a/src/view/com/util/error/ErrorScreen.tsx b/src/view/com/util/error/ErrorScreen.tsx
index dee625967..c849e37db 100644
--- a/src/view/com/util/error/ErrorScreen.tsx
+++ b/src/view/com/util/error/ErrorScreen.tsx
@@ -35,7 +35,7 @@ export function ErrorScreen({
           ]}>
           <FontAwesomeIcon
             icon="exclamation"
-            style={pal.textInverted}
+            style={pal.textInverted as FontAwesomeIconStyle}
             size={24}
           />
         </View>
diff --git a/src/view/com/util/moderation/ContentHider.tsx b/src/view/com/util/moderation/ContentHider.tsx
index 42a97cd34..74fb479ad 100644
--- a/src/view/com/util/moderation/ContentHider.tsx
+++ b/src/view/com/util/moderation/ContentHider.tsx
@@ -6,32 +6,31 @@ import {
   View,
   ViewStyle,
 } from 'react-native'
-import {ComAtprotoLabelDefs} from '@atproto/api'
 import {usePalette} from 'lib/hooks/usePalette'
-import {useStores} from 'state/index'
 import {Text} from '../text/Text'
 import {addStyle} from 'lib/styles'
+import {ModerationBehavior, ModerationBehaviorCode} from 'lib/labeling/types'
 
 export function ContentHider({
   testID,
-  isMuted,
-  labels,
+  moderation,
   style,
   containerStyle,
   children,
 }: React.PropsWithChildren<{
   testID?: string
-  isMuted?: boolean
-  labels: ComAtprotoLabelDefs.Label[] | undefined
+  moderation: ModerationBehavior
   style?: StyleProp<ViewStyle>
   containerStyle?: StyleProp<ViewStyle>
 }>) {
   const pal = usePalette('default')
   const [override, setOverride] = React.useState(false)
-  const store = useStores()
-  const labelPref = store.preferences.getLabelPreference(labels)
 
-  if (!isMuted && labelPref.pref === 'show') {
+  if (
+    moderation.behavior === ModerationBehaviorCode.Show ||
+    moderation.behavior === ModerationBehaviorCode.Warn ||
+    moderation.behavior === ModerationBehaviorCode.WarnImages
+  ) {
     return (
       <View testID={testID} style={style}>
         {children}
@@ -39,7 +38,7 @@ export function ContentHider({
     )
   }
 
-  if (labelPref.pref === 'hide') {
+  if (moderation.behavior === ModerationBehaviorCode.Hide) {
     return null
   }
 
@@ -52,11 +51,7 @@ export function ContentHider({
           override && styles.descriptionOpen,
         ]}>
         <Text type="md" style={pal.textLight}>
-          {isMuted ? (
-            <>Post from an account you muted.</>
-          ) : (
-            <>Warning: {labelPref.desc.warning || labelPref.desc.title}</>
-          )}
+          {moderation.reason || 'Content warning'}
         </Text>
         <TouchableOpacity
           style={styles.showBtn}
diff --git a/src/view/com/util/moderation/PostHider.tsx b/src/view/com/util/moderation/PostHider.tsx
index bafc7aecf..b3c4c9593 100644
--- a/src/view/com/util/moderation/PostHider.tsx
+++ b/src/view/com/util/moderation/PostHider.tsx
@@ -6,77 +6,72 @@ import {
   View,
   ViewStyle,
 } from 'react-native'
-import {ComAtprotoLabelDefs} from '@atproto/api'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {usePalette} from 'lib/hooks/usePalette'
 import {Link} from '../Link'
 import {Text} from '../text/Text'
 import {addStyle} from 'lib/styles'
-import {useStores} from 'state/index'
+import {ModerationBehaviorCode, ModerationBehavior} from 'lib/labeling/types'
 
 export function PostHider({
   testID,
   href,
-  isMuted,
-  labels,
+  moderation,
   style,
   children,
 }: React.PropsWithChildren<{
   testID?: string
-  href: string
-  isMuted: boolean | undefined
-  labels: ComAtprotoLabelDefs.Label[] | undefined
+  href?: string
+  moderation: ModerationBehavior
   style: StyleProp<ViewStyle>
 }>) {
-  const store = useStores()
   const pal = usePalette('default')
   const [override, setOverride] = React.useState(false)
   const bg = override ? pal.viewLight : pal.view
 
-  const labelPref = store.preferences.getLabelPreference(labels)
-  if (labelPref.pref === 'hide') {
-    return <></>
+  if (moderation.behavior === ModerationBehaviorCode.Hide) {
+    return null
   }
 
-  if (!isMuted) {
-    // NOTE: any further label enforcement should occur in ContentContainer
+  if (moderation.behavior === ModerationBehaviorCode.Warn) {
     return (
-      <Link testID={testID} style={style} href={href} noFeedback>
-        {children}
-      </Link>
+      <>
+        <View style={[styles.description, bg, pal.border]}>
+          <FontAwesomeIcon
+            icon={['far', 'eye-slash']}
+            style={[styles.icon, pal.text]}
+          />
+          <Text type="md" style={pal.textLight}>
+            {moderation.reason || 'Content warning'}
+          </Text>
+          <TouchableOpacity
+            style={styles.showBtn}
+            onPress={() => setOverride(v => !v)}>
+            <Text type="md" style={pal.link}>
+              {override ? 'Hide' : 'Show'} post
+            </Text>
+          </TouchableOpacity>
+        </View>
+        {override && (
+          <View style={[styles.childrenContainer, pal.border, bg]}>
+            <Link
+              testID={testID}
+              style={addStyle(style, styles.child)}
+              href={href}
+              noFeedback>
+              {children}
+            </Link>
+          </View>
+        )}
+      </>
     )
   }
 
+  // NOTE: any further label enforcement should occur in ContentContainer
   return (
-    <>
-      <View style={[styles.description, bg, pal.border]}>
-        <FontAwesomeIcon
-          icon={['far', 'eye-slash']}
-          style={[styles.icon, pal.text]}
-        />
-        <Text type="md" style={pal.textLight}>
-          Post from an account you muted.
-        </Text>
-        <TouchableOpacity
-          style={styles.showBtn}
-          onPress={() => setOverride(v => !v)}>
-          <Text type="md" style={pal.link}>
-            {override ? 'Hide' : 'Show'} post
-          </Text>
-        </TouchableOpacity>
-      </View>
-      {override && (
-        <View style={[styles.childrenContainer, pal.border, bg]}>
-          <Link
-            testID={testID}
-            style={addStyle(style, styles.child)}
-            href={href}
-            noFeedback>
-            {children}
-          </Link>
-        </View>
-      )}
-    </>
+    <Link testID={testID} style={style} href={href} noFeedback>
+      {children}
+    </Link>
   )
 }
 
diff --git a/src/view/com/util/moderation/ProfileHeaderLabels.tsx b/src/view/com/util/moderation/ProfileHeaderLabels.tsx
deleted file mode 100644
index c6fbfaf6b..000000000
--- a/src/view/com/util/moderation/ProfileHeaderLabels.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import React from 'react'
-import {StyleSheet, View} from 'react-native'
-import {ComAtprotoLabelDefs} from '@atproto/api'
-import {
-  FontAwesomeIcon,
-  FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
-import {Text} from '../text/Text'
-import {usePalette} from 'lib/hooks/usePalette'
-import {getLabelValueGroup} from 'lib/labeling/helpers'
-
-export function ProfileHeaderLabels({
-  labels,
-}: {
-  labels: ComAtprotoLabelDefs.Label[] | undefined
-}) {
-  const palErr = usePalette('error')
-  if (!labels?.length) {
-    return null
-  }
-  return (
-    <>
-      {labels.map((label, i) => {
-        const labelGroup = getLabelValueGroup(label?.val || '')
-        return (
-          <View
-            key={`${label.val}-${i}`}
-            style={[styles.container, palErr.border, palErr.view]}>
-            <FontAwesomeIcon
-              icon="circle-exclamation"
-              style={palErr.text as FontAwesomeIconStyle}
-              size={20}
-            />
-            <Text style={palErr.text}>
-              This account has been flagged for{' '}
-              {(labelGroup.warning || labelGroup.title).toLocaleLowerCase()}.
-            </Text>
-          </View>
-        )
-      })}
-    </>
-  )
-}
-
-const styles = StyleSheet.create({
-  container: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    gap: 10,
-    borderWidth: 1,
-    borderRadius: 6,
-    paddingHorizontal: 10,
-    paddingVertical: 8,
-  },
-})
diff --git a/src/view/com/util/moderation/ProfileHeaderWarnings.tsx b/src/view/com/util/moderation/ProfileHeaderWarnings.tsx
new file mode 100644
index 000000000..7a1a8e295
--- /dev/null
+++ b/src/view/com/util/moderation/ProfileHeaderWarnings.tsx
@@ -0,0 +1,44 @@
+import React from 'react'
+import {StyleSheet, View} from 'react-native'
+import {
+  FontAwesomeIcon,
+  FontAwesomeIconStyle,
+} from '@fortawesome/react-native-fontawesome'
+import {Text} from '../text/Text'
+import {usePalette} from 'lib/hooks/usePalette'
+import {ModerationBehavior, ModerationBehaviorCode} from 'lib/labeling/types'
+
+export function ProfileHeaderWarnings({
+  moderation,
+}: {
+  moderation: ModerationBehavior
+}) {
+  const palErr = usePalette('error')
+  if (moderation.behavior === ModerationBehaviorCode.Show) {
+    return null
+  }
+  return (
+    <View style={[styles.container, palErr.border, palErr.view]}>
+      <FontAwesomeIcon
+        icon="circle-exclamation"
+        style={palErr.text as FontAwesomeIconStyle}
+        size={20}
+      />
+      <Text style={palErr.text}>
+        This account has been flagged: {moderation.reason}
+      </Text>
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    gap: 10,
+    borderWidth: 1,
+    borderRadius: 6,
+    paddingHorizontal: 10,
+    paddingVertical: 8,
+  },
+})
diff --git a/src/view/com/util/moderation/ScreenHider.tsx b/src/view/com/util/moderation/ScreenHider.tsx
new file mode 100644
index 000000000..2e7b07e1a
--- /dev/null
+++ b/src/view/com/util/moderation/ScreenHider.tsx
@@ -0,0 +1,129 @@
+import React from 'react'
+import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
+import {
+  FontAwesomeIcon,
+  FontAwesomeIconStyle,
+} from '@fortawesome/react-native-fontawesome'
+import {useNavigation} from '@react-navigation/native'
+import {usePalette} from 'lib/hooks/usePalette'
+import {NavigationProp} from 'lib/routes/types'
+import {Text} from '../text/Text'
+import {Button} from '../forms/Button'
+import {isDesktopWeb} from 'platform/detection'
+import {ModerationBehaviorCode, ModerationBehavior} from 'lib/labeling/types'
+
+export function ScreenHider({
+  testID,
+  screenDescription,
+  moderation,
+  style,
+  containerStyle,
+  children,
+}: React.PropsWithChildren<{
+  testID?: string
+  screenDescription: string
+  moderation: ModerationBehavior
+  style?: StyleProp<ViewStyle>
+  containerStyle?: StyleProp<ViewStyle>
+}>) {
+  const pal = usePalette('default')
+  const palInverted = usePalette('inverted')
+  const [override, setOverride] = React.useState(false)
+  const navigation = useNavigation<NavigationProp>()
+
+  const onPressBack = React.useCallback(() => {
+    if (navigation.canGoBack()) {
+      navigation.goBack()
+    } else {
+      navigation.navigate('Home')
+    }
+  }, [navigation])
+
+  if (moderation.behavior !== ModerationBehaviorCode.Hide || override) {
+    return (
+      <View testID={testID} style={style}>
+        {children}
+      </View>
+    )
+  }
+
+  return (
+    <View style={[styles.container, pal.view, containerStyle]}>
+      <View style={styles.iconContainer}>
+        <View style={[styles.icon, palInverted.view]}>
+          <FontAwesomeIcon
+            icon="exclamation"
+            style={pal.textInverted as FontAwesomeIconStyle}
+            size={24}
+          />
+        </View>
+      </View>
+      <Text type="title-2xl" style={[styles.title, pal.text]}>
+        Content Warning
+      </Text>
+      <Text type="2xl" style={[styles.description, pal.textLight]}>
+        This {screenDescription} has been flagged:{' '}
+        {moderation.reason || 'Content warning'}
+      </Text>
+      {!isDesktopWeb && <View style={styles.spacer} />}
+      <View style={styles.btnContainer}>
+        <Button type="inverted" onPress={onPressBack} style={styles.btn}>
+          <Text type="button-lg" style={pal.textInverted}>
+            Go back
+          </Text>
+        </Button>
+        {!moderation.noOverride && (
+          <Button
+            type="default"
+            onPress={() => setOverride(v => !v)}
+            style={styles.btn}>
+            <Text type="button-lg" style={pal.text}>
+              Show anyway
+            </Text>
+          </Button>
+        )}
+      </View>
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  spacer: {
+    flex: 1,
+  },
+  container: {
+    flex: 1,
+    paddingTop: 100,
+    paddingBottom: 150,
+  },
+  iconContainer: {
+    alignItems: 'center',
+    marginBottom: 10,
+  },
+  icon: {
+    borderRadius: 25,
+    width: 50,
+    height: 50,
+    alignItems: 'center',
+    justifyContent: 'center',
+  },
+  title: {
+    textAlign: 'center',
+    marginBottom: 10,
+  },
+  description: {
+    marginBottom: 10,
+    paddingHorizontal: 20,
+    textAlign: 'center',
+  },
+  btnContainer: {
+    flexDirection: 'row',
+    justifyContent: 'center',
+    marginVertical: 10,
+    gap: 10,
+  },
+  btn: {
+    paddingHorizontal: 20,
+    paddingVertical: 14,
+  },
+})