about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2024-07-02 18:15:20 -0500
committerGitHub <noreply@github.com>2024-07-03 00:15:20 +0100
commit14c2d75d49c492e9625a6e7b139f2e8dbc66668f (patch)
tree154c60c485302f8dc66dbb2181d46067125f784e
parentc13366176845d20775fd1d15d1a15bcfb160b39e (diff)
downloadvoidsky-14c2d75d49c492e9625a6e7b139f2e8dbc66668f.tar.zst
Unify label pills (#4676)
* New label pills

* Fix type errors, add default case

* Remove negative margin, only works in some places

* Fix alignment edge case

* Add a bit of padding

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
-rw-r--r--src/components/Pills.tsx169
-rw-r--r--src/components/ProfileHoverCard/index.web.tsx5
-rw-r--r--src/components/dms/MessagesListHeader.tsx2
-rw-r--r--src/components/moderation/ContentHider.tsx4
-rw-r--r--src/components/moderation/PostAlerts.tsx121
-rw-r--r--src/components/moderation/ProfileHeaderAlerts.tsx103
-rw-r--r--src/screens/Messages/List/ChatListItem.tsx2
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx2
-rw-r--r--src/view/com/profile/ProfileCard.tsx52
9 files changed, 226 insertions, 234 deletions
diff --git a/src/components/Pills.tsx b/src/components/Pills.tsx
new file mode 100644
index 000000000..2fff99937
--- /dev/null
+++ b/src/components/Pills.tsx
@@ -0,0 +1,169 @@
+import React from 'react'
+import {View} from 'react-native'
+import {BSKY_LABELER_DID, ModerationCause} from '@atproto/api'
+import {Trans} from '@lingui/macro'
+
+import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
+import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
+import {Button} from '#/components/Button'
+import {
+  ModerationDetailsDialog,
+  useModerationDetailsDialogControl,
+} from '#/components/moderation/ModerationDetailsDialog'
+import {Text} from '#/components/Typography'
+
+export type CommonProps = {
+  size?: 'sm' | 'lg'
+}
+
+export function Row({
+  children,
+  style,
+  size = 'sm',
+}: {children: React.ReactNode | React.ReactNode[]} & CommonProps &
+  ViewStyleProp) {
+  const styles = React.useMemo(() => {
+    switch (size) {
+      case 'lg':
+        return [{gap: 5}]
+      case 'sm':
+      default:
+        return [{gap: 3}]
+    }
+  }, [size])
+  return (
+    <View style={[a.flex_row, a.flex_wrap, a.gap_xs, styles, style]}>
+      {children}
+    </View>
+  )
+}
+
+export type LabelProps = {
+  cause: ModerationCause
+  disableDetailsDialog?: boolean
+  noBg?: boolean
+} & CommonProps
+
+export function Label({
+  cause,
+  size = 'sm',
+  disableDetailsDialog,
+  noBg,
+}: LabelProps) {
+  const t = useTheme()
+  const control = useModerationDetailsDialogControl()
+  const desc = useModerationCauseDescription(cause)
+  const isLabeler = Boolean(desc.sourceType && desc.sourceDid)
+  const isBlueskyLabel =
+    desc.sourceType === 'labeler' && desc.sourceDid === BSKY_LABELER_DID
+
+  const {outer, avi, text} = React.useMemo(() => {
+    switch (size) {
+      case 'lg': {
+        return {
+          outer: [
+            t.atoms.bg_contrast_25,
+            {
+              gap: 5,
+              paddingHorizontal: 5,
+              paddingVertical: 5,
+            },
+          ],
+          avi: 16,
+          text: [a.text_sm],
+        }
+      }
+      case 'sm':
+      default: {
+        return {
+          outer: [
+            !noBg && t.atoms.bg_contrast_25,
+            {
+              gap: 3,
+              paddingHorizontal: 3,
+              paddingVertical: 3,
+            },
+          ],
+          avi: 12,
+          text: [a.text_xs],
+        }
+      }
+    }
+  }, [t, size, noBg])
+
+  return (
+    <>
+      <Button
+        disabled={disableDetailsDialog}
+        label={desc.name}
+        onPress={e => {
+          e.preventDefault()
+          e.stopPropagation()
+          control.open()
+        }}>
+        {({hovered, pressed}) => (
+          <View
+            style={[
+              a.flex_row,
+              a.align_center,
+              a.rounded_full,
+              outer,
+              (hovered || pressed) && t.atoms.bg_contrast_50,
+            ]}>
+            {isBlueskyLabel || !isLabeler ? (
+              <desc.icon
+                width={avi}
+                fill={t.atoms.text_contrast_medium.color}
+              />
+            ) : (
+              <UserAvatar avatar={desc.sourceAvi} size={avi} />
+            )}
+
+            <Text
+              style={[
+                text,
+                a.font_semibold,
+                a.leading_tight,
+                t.atoms.text_contrast_medium,
+                {paddingRight: 3},
+              ]}>
+              {desc.name}
+            </Text>
+          </View>
+        )}
+      </Button>
+
+      {!disableDetailsDialog && (
+        <ModerationDetailsDialog control={control} modcause={cause} />
+      )}
+    </>
+  )
+}
+
+export function FollowsYou({size = 'sm'}: CommonProps) {
+  const t = useTheme()
+
+  const variantStyles = React.useMemo(() => {
+    switch (size) {
+      case 'sm':
+      case 'lg':
+      default:
+        return [
+          {
+            paddingHorizontal: 6,
+            paddingVertical: 3,
+            borderRadius: 4,
+          },
+        ]
+    }
+  }, [size])
+
+  return (
+    <View style={[variantStyles, a.justify_center, t.atoms.bg_contrast_25]}>
+      <Text style={[a.text_xs, a.leading_tight]}>
+        <Trans>Follows You</Trans>
+      </Text>
+    </View>
+  )
+}
diff --git a/src/components/ProfileHoverCard/index.web.tsx b/src/components/ProfileHoverCard/index.web.tsx
index 4db9c4f8e..84b1d6d24 100644
--- a/src/components/ProfileHoverCard/index.web.tsx
+++ b/src/components/ProfileHoverCard/index.web.tsx
@@ -29,10 +29,10 @@ import {
 } from '#/components/KnownFollowers'
 import {InlineLinkText, Link} from '#/components/Link'
 import {Loader} from '#/components/Loader'
+import * as Pills from '#/components/Pills'
 import {Portal} from '#/components/Portal'
 import {RichText} from '#/components/RichText'
 import {Text} from '#/components/Typography'
-import {ProfileLabel} from '../moderation/ProfileHeaderAlerts'
 import {ProfileHoverCardProps} from './types'
 
 const floatingMiddlewares = [
@@ -476,8 +476,9 @@ function Inner({
       {isBlockedUser && (
         <View style={[a.flex_row, a.flex_wrap, a.gap_xs]}>
           {moderation.ui('profileView').alerts.map(cause => (
-            <ProfileLabel
+            <Pills.Label
               key={getModerationCauseKey(cause)}
+              size="lg"
               cause={cause}
               disableDetailsDialog
             />
diff --git a/src/components/dms/MessagesListHeader.tsx b/src/components/dms/MessagesListHeader.tsx
index 0aeac3628..8bf673d30 100644
--- a/src/components/dms/MessagesListHeader.tsx
+++ b/src/components/dms/MessagesListHeader.tsx
@@ -214,7 +214,7 @@ function HeaderReady({
         ]}>
         <PostAlerts
           modui={moderation.ui('contentList')}
-          size="large"
+          size="lg"
           style={[a.pt_xs]}
         />
       </View>
diff --git a/src/components/moderation/ContentHider.tsx b/src/components/moderation/ContentHider.tsx
index fd71ec838..45122a4ef 100644
--- a/src/components/moderation/ContentHider.tsx
+++ b/src/components/moderation/ContentHider.tsx
@@ -165,9 +165,7 @@ export function ContentHider({
 }
 
 const styles = StyleSheet.create({
-  outer: {
-    overflow: 'hidden',
-  },
+  outer: {},
   cover: {
     flexDirection: 'row',
     alignItems: 'center',
diff --git a/src/components/moderation/PostAlerts.tsx b/src/components/moderation/PostAlerts.tsx
index ec7529a4f..efbf18219 100644
--- a/src/components/moderation/PostAlerts.tsx
+++ b/src/components/moderation/PostAlerts.tsx
@@ -1,25 +1,17 @@
 import React from 'react'
-import {StyleProp, View, ViewStyle} from 'react-native'
-import {BSKY_LABELER_DID, ModerationCause, ModerationUI} from '@atproto/api'
+import {StyleProp, ViewStyle} from 'react-native'
+import {ModerationUI} from '@atproto/api'
 
 import {getModerationCauseKey} from '#/lib/moderation'
-import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
-import {UserAvatar} from '#/view/com/util/UserAvatar'
-import {atoms as a, useTheme} from '#/alf'
-import {Button} from '#/components/Button'
-import {
-  ModerationDetailsDialog,
-  useModerationDetailsDialogControl,
-} from '#/components/moderation/ModerationDetailsDialog'
-import {Text} from '#/components/Typography'
+import * as Pills from '#/components/Pills'
 
 export function PostAlerts({
   modui,
-  size,
+  size = 'sm',
   style,
 }: {
   modui: ModerationUI
-  size?: 'medium' | 'large'
+  size?: Pills.CommonProps['size']
   includeMute?: boolean
   style?: StyleProp<ViewStyle>
 }) {
@@ -28,90 +20,23 @@ export function PostAlerts({
   }
 
   return (
-    <View style={[a.flex_col, a.gap_xs, style]}>
-      <View style={[a.flex_row, a.flex_wrap, a.gap_xs]}>
-        {modui.alerts.map(cause => (
-          <PostLabel
-            key={getModerationCauseKey(cause)}
-            cause={cause}
-            size={size}
-          />
-        ))}
-        {modui.informs.map(cause => (
-          <PostLabel
-            key={getModerationCauseKey(cause)}
-            cause={cause}
-            size={size}
-          />
-        ))}
-      </View>
-    </View>
-  )
-}
-
-function PostLabel({
-  cause,
-  size,
-}: {
-  cause: ModerationCause
-  size?: 'medium' | 'large'
-}) {
-  const control = useModerationDetailsDialogControl()
-  const desc = useModerationCauseDescription(cause)
-  const t = useTheme()
-
-  return (
-    <>
-      <Button
-        label={desc.name}
-        onPress={e => {
-          e.preventDefault()
-          e.stopPropagation()
-          control.open()
-        }}>
-        {({hovered, pressed}) => (
-          <View
-            style={[
-              a.flex_row,
-              a.align_center,
-              a.gap_xs,
-              a.rounded_sm,
-              hovered || pressed
-                ? size === 'large'
-                  ? t.atoms.bg_contrast_50
-                  : t.atoms.bg_contrast_25
-                : size === 'large'
-                ? t.atoms.bg_contrast_25
-                : undefined,
-              size === 'large'
-                ? {paddingLeft: 4, paddingRight: 6, paddingVertical: 2}
-                : {paddingRight: 4, paddingVertical: 1},
-            ]}>
-            {desc.sourceType === 'labeler' &&
-            desc.sourceDid !== BSKY_LABELER_DID ? (
-              <UserAvatar
-                avatar={desc.sourceAvi}
-                size={size === 'large' ? 16 : 12}
-                type="labeler"
-                shape="circle"
-              />
-            ) : (
-              <desc.icon size="sm" fill={t.atoms.text_contrast_medium.color} />
-            )}
-            <Text
-              style={[
-                a.text_left,
-                a.leading_snug,
-                size === 'large' ? {fontSize: 13} : a.text_xs,
-                size === 'large' ? t.atoms.text : t.atoms.text_contrast_high,
-              ]}>
-              {desc.name}
-            </Text>
-          </View>
-        )}
-      </Button>
-
-      <ModerationDetailsDialog control={control} modcause={cause} />
-    </>
+    <Pills.Row size={size} style={[size === 'sm' && {marginLeft: -3}, style]}>
+      {modui.alerts.map(cause => (
+        <Pills.Label
+          key={getModerationCauseKey(cause)}
+          cause={cause}
+          size={size}
+          noBg={size === 'sm'}
+        />
+      ))}
+      {modui.informs.map(cause => (
+        <Pills.Label
+          key={getModerationCauseKey(cause)}
+          cause={cause}
+          size={size}
+          noBg={size === 'sm'}
+        />
+      ))}
+    </Pills.Row>
   )
 }
diff --git a/src/components/moderation/ProfileHeaderAlerts.tsx b/src/components/moderation/ProfileHeaderAlerts.tsx
index 4b48b142d..94779697f 100644
--- a/src/components/moderation/ProfileHeaderAlerts.tsx
+++ b/src/components/moderation/ProfileHeaderAlerts.tsx
@@ -1,25 +1,12 @@
 import React from 'react'
-import {StyleProp, View, ViewStyle} from 'react-native'
-import {
-  BSKY_LABELER_DID,
-  ModerationCause,
-  ModerationDecision,
-} from '@atproto/api'
+import {StyleProp, ViewStyle} from 'react-native'
+import {ModerationDecision} from '@atproto/api'
 
-import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
 import {getModerationCauseKey} from 'lib/moderation'
-import {UserAvatar} from '#/view/com/util/UserAvatar'
-import {atoms as a, useTheme} from '#/alf'
-import {Button} from '#/components/Button'
-import {
-  ModerationDetailsDialog,
-  useModerationDetailsDialogControl,
-} from '#/components/moderation/ModerationDetailsDialog'
-import {Text} from '#/components/Typography'
+import * as Pills from '#/components/Pills'
 
 export function ProfileHeaderAlerts({
   moderation,
-  style,
 }: {
   moderation: ModerationDecision
   style?: StyleProp<ViewStyle>
@@ -30,73 +17,21 @@ export function ProfileHeaderAlerts({
   }
 
   return (
-    <View style={[a.flex_col, a.gap_xs, style]}>
-      <View style={[a.flex_row, a.flex_wrap, a.gap_xs]}>
-        {modui.alerts.map(cause => (
-          <ProfileLabel key={getModerationCauseKey(cause)} cause={cause} />
-        ))}
-        {modui.informs.map(cause => (
-          <ProfileLabel key={getModerationCauseKey(cause)} cause={cause} />
-        ))}
-      </View>
-    </View>
-  )
-}
-
-export function ProfileLabel({
-  cause,
-  disableDetailsDialog,
-}: {
-  cause: ModerationCause
-  disableDetailsDialog?: boolean
-}) {
-  const t = useTheme()
-  const control = useModerationDetailsDialogControl()
-  const desc = useModerationCauseDescription(cause)
-
-  return (
-    <>
-      <Button
-        disabled={disableDetailsDialog}
-        label={desc.name}
-        onPress={() => {
-          control.open()
-        }}>
-        {({hovered, pressed}) => (
-          <View
-            style={[
-              a.flex_row,
-              a.align_center,
-              {paddingLeft: 6, paddingRight: 8, paddingVertical: 4},
-              a.gap_xs,
-              a.rounded_md,
-              hovered || pressed
-                ? t.atoms.bg_contrast_50
-                : t.atoms.bg_contrast_25,
-            ]}>
-            {desc.sourceType === 'labeler' &&
-            desc.sourceDid !== BSKY_LABELER_DID ? (
-              <UserAvatar avatar={desc.sourceAvi} size={16} />
-            ) : (
-              <desc.icon size="sm" fill={t.atoms.text_contrast_medium.color} />
-            )}
-            <Text
-              style={[
-                a.text_left,
-                a.leading_snug,
-                a.text_sm,
-                t.atoms.text_contrast_medium,
-                a.font_semibold,
-              ]}>
-              {desc.name}
-            </Text>
-          </View>
-        )}
-      </Button>
-
-      {!disableDetailsDialog && (
-        <ModerationDetailsDialog control={control} modcause={cause} />
-      )}
-    </>
+    <Pills.Row size="lg">
+      {modui.alerts.map(cause => (
+        <Pills.Label
+          size="lg"
+          key={getModerationCauseKey(cause)}
+          cause={cause}
+        />
+      ))}
+      {modui.informs.map(cause => (
+        <Pills.Label
+          size="lg"
+          key={getModerationCauseKey(cause)}
+          cause={cause}
+        />
+      ))}
+    </Pills.Row>
   )
 }
diff --git a/src/screens/Messages/List/ChatListItem.tsx b/src/screens/Messages/List/ChatListItem.tsx
index 8ebf8b00b..c45cc28d7 100644
--- a/src/screens/Messages/List/ChatListItem.tsx
+++ b/src/screens/Messages/List/ChatListItem.tsx
@@ -315,7 +315,7 @@ function ChatListItemReady({
 
               <PostAlerts
                 modui={moderation.ui('contentList')}
-                size="large"
+                size="lg"
                 style={[a.pt_xs]}
               />
             </View>
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 46c6c958e..0f5350e79 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -313,7 +313,7 @@ let PostThreadItemLoaded = ({
               childContainerStyle={styles.contentHiderChild}>
               <PostAlerts
                 modui={moderation.ui('contentView')}
-                size="large"
+                size="lg"
                 includeMute
                 style={[a.pt_2xs, a.pb_sm]}
               />
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index d7ed0dd6a..7332d452a 100644
--- a/src/view/com/profile/ProfileCard.tsx
+++ b/src/view/com/profile/ProfileCard.tsx
@@ -3,13 +3,11 @@ import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
 import {
   AppBskyActorDefs,
   moderateProfile,
-  ModerationCause,
   ModerationDecision,
 } from '@atproto/api'
 import {Trans} from '@lingui/macro'
 import {useQueryClient} from '@tanstack/react-query'
 
-import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
 import {useProfileShadow} from '#/state/cache/profile-shadow'
 import {Shadow} from '#/state/cache/types'
 import {useModerationOpts} from '#/state/preferences/moderation-opts'
@@ -26,6 +24,8 @@ import {Text} from '../util/text/Text'
 import {PreviewableUserAvatar} from '../util/UserAvatar'
 import {FollowButton} from './FollowButton'
 import hairlineWidth = StyleSheet.hairlineWidth
+import {atoms as a} from '#/alf'
+import * as Pills from '#/components/Pills'
 
 export function ProfileCard({
   testID,
@@ -137,58 +137,21 @@ export function ProfileCardPills({
   followedBy: boolean
   moderation: ModerationDecision
 }) {
-  const pal = usePalette('default')
-
   const modui = moderation.ui('profileList')
   if (!followedBy && !modui.inform && !modui.alert) {
     return null
   }
 
   return (
-    <View style={styles.pills}>
-      {followedBy && (
-        <View style={[s.mt5, pal.btn, styles.pill]}>
-          <Text type="xs" style={pal.text}>
-            <Trans>Follows You</Trans>
-          </Text>
-        </View>
-      )}
+    <Pills.Row style={[a.pt_xs]}>
+      {followedBy && <Pills.FollowsYou />}
       {modui.alerts.map(alert => (
-        <ProfileCardPillModerationCause
-          key={getModerationCauseKey(alert)}
-          cause={alert}
-          severity="alert"
-        />
+        <Pills.Label key={getModerationCauseKey(alert)} cause={alert} />
       ))}
       {modui.informs.map(inform => (
-        <ProfileCardPillModerationCause
-          key={getModerationCauseKey(inform)}
-          cause={inform}
-          severity="inform"
-        />
+        <Pills.Label key={getModerationCauseKey(inform)} cause={inform} />
       ))}
-    </View>
-  )
-}
-
-function ProfileCardPillModerationCause({
-  cause,
-  severity,
-}: {
-  cause: ModerationCause
-  severity: 'alert' | 'inform'
-}) {
-  const pal = usePalette('default')
-  const {name} = useModerationCauseDescription(cause)
-  return (
-    <View
-      style={[s.mt5, pal.btn, styles.pill]}
-      key={getModerationCauseKey(cause)}>
-      <Text type="xs" style={pal.text}>
-        {severity === 'alert' ? '⚠ ' : ''}
-        {name}
-      </Text>
-    </View>
+    </Pills.Row>
   )
 }
 
@@ -322,6 +285,7 @@ const styles = StyleSheet.create({
     paddingBottom: 10,
   },
   pills: {
+    alignItems: 'flex-start',
     flexDirection: 'row',
     flexWrap: 'wrap',
     columnGap: 6,