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/LoadLatestBtn.tsx52
-rw-r--r--src/view/com/util/LoadLatestBtn.web.tsx14
-rw-r--r--src/view/com/util/PostMeta.tsx7
-rw-r--r--src/view/com/util/PostMuted.tsx50
-rw-r--r--src/view/com/util/UserAvatar.tsx47
-rw-r--r--src/view/com/util/moderation/ContentHider.tsx109
-rw-r--r--src/view/com/util/moderation/PostHider.tsx105
-rw-r--r--src/view/com/util/moderation/ProfileHeaderLabels.tsx55
-rw-r--r--src/view/com/util/post-embeds/QuoteEmbed.tsx1
9 files changed, 354 insertions, 86 deletions
diff --git a/src/view/com/util/LoadLatestBtn.tsx b/src/view/com/util/LoadLatestBtn.tsx
index fd05ecc9c..88b6dffd9 100644
--- a/src/view/com/util/LoadLatestBtn.tsx
+++ b/src/view/com/util/LoadLatestBtn.tsx
@@ -10,31 +10,33 @@ import {useStores} from 'state/index'
 
 const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20}
 
-export const LoadLatestBtn = observer(({onPress}: {onPress: () => void}) => {
-  const store = useStores()
-  const safeAreaInsets = useSafeAreaInsets()
-  return (
-    <TouchableOpacity
-      style={[
-        styles.loadLatest,
-        !store.shell.minimalShellMode && {
-          bottom: 60 + clamp(safeAreaInsets.bottom, 15, 30),
-        },
-      ]}
-      onPress={onPress}
-      hitSlop={HITSLOP}>
-      <LinearGradient
-        colors={[gradients.blueLight.start, gradients.blueLight.end]}
-        start={{x: 0, y: 0}}
-        end={{x: 1, y: 1}}
-        style={styles.loadLatestInner}>
-        <Text type="md-bold" style={styles.loadLatestText}>
-          Load new posts
-        </Text>
-      </LinearGradient>
-    </TouchableOpacity>
-  )
-})
+export const LoadLatestBtn = observer(
+  ({onPress, label}: {onPress: () => void; label: string}) => {
+    const store = useStores()
+    const safeAreaInsets = useSafeAreaInsets()
+    return (
+      <TouchableOpacity
+        style={[
+          styles.loadLatest,
+          !store.shell.minimalShellMode && {
+            bottom: 60 + clamp(safeAreaInsets.bottom, 15, 30),
+          },
+        ]}
+        onPress={onPress}
+        hitSlop={HITSLOP}>
+        <LinearGradient
+          colors={[gradients.blueLight.start, gradients.blueLight.end]}
+          start={{x: 0, y: 0}}
+          end={{x: 1, y: 1}}
+          style={styles.loadLatestInner}>
+          <Text type="md-bold" style={styles.loadLatestText}>
+            Load new {label}
+          </Text>
+        </LinearGradient>
+      </TouchableOpacity>
+    )
+  },
+)
 
 const styles = StyleSheet.create({
   loadLatest: {
diff --git a/src/view/com/util/LoadLatestBtn.web.tsx b/src/view/com/util/LoadLatestBtn.web.tsx
index ba33f92a7..c85f44f30 100644
--- a/src/view/com/util/LoadLatestBtn.web.tsx
+++ b/src/view/com/util/LoadLatestBtn.web.tsx
@@ -6,7 +6,13 @@ import {UpIcon} from 'lib/icons'
 
 const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20}
 
-export const LoadLatestBtn = ({onPress}: {onPress: () => void}) => {
+export const LoadLatestBtn = ({
+  onPress,
+  label,
+}: {
+  onPress: () => void
+  label: string
+}) => {
   const pal = usePalette('default')
   return (
     <TouchableOpacity
@@ -15,7 +21,7 @@ export const LoadLatestBtn = ({onPress}: {onPress: () => void}) => {
       hitSlop={HITSLOP}>
       <Text type="md-bold" style={pal.text}>
         <UpIcon size={16} strokeWidth={1} style={[pal.text, styles.icon]} />
-        Load new posts
+        Load new {label}
       </Text>
     </TouchableOpacity>
   )
@@ -25,7 +31,9 @@ const styles = StyleSheet.create({
   loadLatest: {
     flexDirection: 'row',
     position: 'absolute',
-    left: 'calc(50vw - 80px)',
+    left: '50vw',
+    // @ts-ignore web only -prf
+    transform: 'translateX(-50%)',
     top: 30,
     shadowColor: '#000',
     shadowOpacity: 0.2,
diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx
index c46c16da0..d9dd11e05 100644
--- a/src/view/com/util/PostMeta.tsx
+++ b/src/view/com/util/PostMeta.tsx
@@ -15,6 +15,7 @@ interface PostMetaOpts {
   authorAvatar?: string
   authorHandle: string
   authorDisplayName: string | undefined
+  authorHasWarning: boolean
   postHref: string
   timestamp: string
   did?: string
@@ -93,7 +94,11 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
     <View style={styles.meta}>
       {typeof opts.authorAvatar !== 'undefined' && (
         <View style={[styles.metaItem, styles.avatar]}>
-          <UserAvatar avatar={opts.authorAvatar} size={16} />
+          <UserAvatar
+            avatar={opts.authorAvatar}
+            size={16}
+            hasWarning={opts.authorHasWarning}
+          />
         </View>
       )}
       <View style={[styles.metaItem, styles.maxWidth]}>
diff --git a/src/view/com/util/PostMuted.tsx b/src/view/com/util/PostMuted.tsx
deleted file mode 100644
index 539a71ecf..000000000
--- a/src/view/com/util/PostMuted.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import React from 'react'
-import {StyleSheet, TouchableOpacity, View} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {usePalette} from 'lib/hooks/usePalette'
-import {Text} from './text/Text'
-
-export function PostMutedWrapper({
-  isMuted,
-  children,
-}: React.PropsWithChildren<{isMuted?: boolean}>) {
-  const pal = usePalette('default')
-  const [override, setOverride] = React.useState(false)
-  if (!isMuted || override) {
-    return <>{children}</>
-  }
-  return (
-    <View style={[styles.container, pal.view, 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(true)}>
-        <Text type="md" style={pal.link}>
-          Show post
-        </Text>
-      </TouchableOpacity>
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  container: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    paddingVertical: 14,
-    paddingHorizontal: 18,
-    borderTopWidth: 1,
-  },
-  icon: {
-    marginRight: 10,
-  },
-  showBtn: {
-    marginLeft: 'auto',
-  },
-})
diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
index ff741cd34..d18c2d697 100644
--- a/src/view/com/util/UserAvatar.tsx
+++ b/src/view/com/util/UserAvatar.tsx
@@ -44,10 +44,12 @@ function DefaultAvatar({size}: {size: number}) {
 export function UserAvatar({
   size,
   avatar,
+  hasWarning,
   onSelectNewAvatar,
 }: {
   size: number
   avatar?: string | null
+  hasWarning?: boolean
   onSelectNewAvatar?: (img: PickedMedia | null) => void
 }) {
   const store = useStores()
@@ -105,6 +107,22 @@ export function UserAvatar({
       },
     },
   ]
+
+  const warning = React.useMemo(() => {
+    if (!hasWarning) {
+      return <></>
+    }
+    return (
+      <View style={[styles.warningIconContainer, pal.view]}>
+        <FontAwesomeIcon
+          icon="exclamation-circle"
+          style={styles.warningIcon}
+          size={Math.floor(size / 3)}
+        />
+      </View>
+    )
+  }, [hasWarning, size, pal])
+
   // onSelectNewAvatar is only passed as prop on the EditProfile component
   return onSelectNewAvatar ? (
     <DropdownButton
@@ -137,14 +155,20 @@ export function UserAvatar({
       </View>
     </DropdownButton>
   ) : avatar ? (
-    <HighPriorityImage
-      testID="userAvatarImage"
-      style={{width: size, height: size, borderRadius: Math.floor(size / 2)}}
-      resizeMode="stretch"
-      source={{uri: avatar}}
-    />
+    <View style={{width: size, height: size}}>
+      <HighPriorityImage
+        testID="userAvatarImage"
+        style={{width: size, height: size, borderRadius: Math.floor(size / 2)}}
+        resizeMode="stretch"
+        source={{uri: avatar}}
+      />
+      {warning}
+    </View>
   ) : (
-    <DefaultAvatar size={size} />
+    <View style={{width: size, height: size}}>
+      <DefaultAvatar size={size} />
+      {warning}
+    </View>
   )
 }
 
@@ -165,4 +189,13 @@ const styles = StyleSheet.create({
     height: 80,
     borderRadius: 40,
   },
+  warningIconContainer: {
+    position: 'absolute',
+    right: 0,
+    bottom: 0,
+    borderRadius: 100,
+  },
+  warningIcon: {
+    color: colors.red3,
+  },
 })
diff --git a/src/view/com/util/moderation/ContentHider.tsx b/src/view/com/util/moderation/ContentHider.tsx
new file mode 100644
index 000000000..f65635d35
--- /dev/null
+++ b/src/view/com/util/moderation/ContentHider.tsx
@@ -0,0 +1,109 @@
+import React from 'react'
+import {
+  StyleProp,
+  StyleSheet,
+  TouchableOpacity,
+  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'
+
+export function ContentHider({
+  testID,
+  isMuted,
+  labels,
+  style,
+  containerStyle,
+  children,
+}: React.PropsWithChildren<{
+  testID?: string
+  isMuted?: boolean
+  labels: ComAtprotoLabelDefs.Label[] | undefined
+  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') {
+    return (
+      <View testID={testID} style={style}>
+        {children}
+      </View>
+    )
+  }
+
+  if (labelPref.pref === 'hide') {
+    return <></>
+  }
+
+  return (
+    <View style={[styles.container, pal.view, pal.border, containerStyle]}>
+      <View
+        style={[
+          styles.description,
+          pal.viewLight,
+          override && styles.descriptionOpen,
+        ]}>
+        <Text type="md" style={pal.textLight}>
+          {isMuted ? (
+            <>Post from an account you muted.</>
+          ) : (
+            <>Warning: {labelPref.desc.title}</>
+          )}
+        </Text>
+        <TouchableOpacity
+          style={styles.showBtn}
+          onPress={() => setOverride(v => !v)}>
+          <Text type="md" style={pal.link}>
+            {override ? 'Hide' : 'Show'}
+          </Text>
+        </TouchableOpacity>
+      </View>
+      {override && (
+        <View style={[styles.childrenContainer, pal.border]}>
+          <View testID={testID} style={addStyle(style, styles.child)}>
+            {children}
+          </View>
+        </View>
+      )}
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  container: {
+    marginBottom: 10,
+    borderWidth: 1,
+    borderRadius: 12,
+  },
+  description: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingVertical: 14,
+    paddingLeft: 14,
+    paddingRight: 18,
+    borderRadius: 12,
+  },
+  descriptionOpen: {
+    borderBottomLeftRadius: 0,
+    borderBottomRightRadius: 0,
+  },
+  icon: {
+    marginRight: 10,
+  },
+  showBtn: {
+    marginLeft: 'auto',
+  },
+  childrenContainer: {
+    paddingHorizontal: 12,
+    paddingTop: 8,
+  },
+  child: {},
+})
diff --git a/src/view/com/util/moderation/PostHider.tsx b/src/view/com/util/moderation/PostHider.tsx
new file mode 100644
index 000000000..bafc7aecf
--- /dev/null
+++ b/src/view/com/util/moderation/PostHider.tsx
@@ -0,0 +1,105 @@
+import React from 'react'
+import {
+  StyleProp,
+  StyleSheet,
+  TouchableOpacity,
+  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'
+
+export function PostHider({
+  testID,
+  href,
+  isMuted,
+  labels,
+  style,
+  children,
+}: React.PropsWithChildren<{
+  testID?: string
+  href: string
+  isMuted: boolean | undefined
+  labels: ComAtprotoLabelDefs.Label[] | undefined
+  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 (!isMuted) {
+    // NOTE: any further label enforcement should occur in ContentContainer
+    return (
+      <Link testID={testID} style={style} href={href} noFeedback>
+        {children}
+      </Link>
+    )
+  }
+
+  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>
+      )}
+    </>
+  )
+}
+
+const styles = StyleSheet.create({
+  description: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingVertical: 14,
+    paddingHorizontal: 18,
+    borderTopWidth: 1,
+  },
+  icon: {
+    marginRight: 10,
+  },
+  showBtn: {
+    marginLeft: 'auto',
+  },
+  childrenContainer: {
+    paddingHorizontal: 6,
+    paddingBottom: 6,
+  },
+  child: {
+    borderWidth: 1,
+    borderRadius: 12,
+  },
+})
diff --git a/src/view/com/util/moderation/ProfileHeaderLabels.tsx b/src/view/com/util/moderation/ProfileHeaderLabels.tsx
new file mode 100644
index 000000000..e099f09a7
--- /dev/null
+++ b/src/view/com/util/moderation/ProfileHeaderLabels.tsx
@@ -0,0 +1,55 @@
+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.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/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx
index 5a8be5a14..94e837238 100644
--- a/src/view/com/util/post-embeds/QuoteEmbed.tsx
+++ b/src/view/com/util/post-embeds/QuoteEmbed.tsx
@@ -42,6 +42,7 @@ export function QuoteEmbed({
         authorAvatar={quote.author.avatar}
         authorHandle={quote.author.handle}
         authorDisplayName={quote.author.displayName}
+        authorHasWarning={false}
         postHref={itemHref}
         timestamp={quote.indexedAt}
       />