about summary refs log tree commit diff
path: root/src/view/com
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2025-04-15 08:13:20 -0500
committerGitHub <noreply@github.com>2025-04-15 06:13:20 -0700
commitf46336e34142e7f46bb1395f727e303e37b15d41 (patch)
treedac04a4abd8d100dc9c0d5c65ab7f75f1d3280eb /src/view/com
parent32c2b69b848050975b698067383dd24f2754cab2 (diff)
downloadvoidsky-f46336e34142e7f46bb1395f727e303e37b15d41.tar.zst
Replace old ProfileCard with new (#8195)
* Replace usages of old ProfileCard

* Replace Pills with Labels component

* Replace impl of ProfileCardWithFollowButton

* Remove never-used LikesDialog

* Handle missing mod opts

* Add missing profile hover

* use modern button in listmembers

* remove follow button from muted accounts list

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Diffstat (limited to 'src/view/com')
-rw-r--r--src/view/com/lists/ListMembers.tsx85
-rw-r--r--src/view/com/profile/ProfileCard.tsx311
2 files changed, 72 insertions, 324 deletions
diff --git a/src/view/com/lists/ListMembers.tsx b/src/view/com/lists/ListMembers.tsx
index da32c2d48..dc18cbe5d 100644
--- a/src/view/com/lists/ListMembers.tsx
+++ b/src/view/com/lists/ListMembers.tsx
@@ -1,22 +1,23 @@
 import React, {useCallback} from 'react'
 import {Dimensions, type StyleProp, View, type ViewStyle} from 'react-native'
 import {type AppBskyGraphDefs} from '@atproto/api'
-import {msg} from '@lingui/macro'
+import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {cleanError} from '#/lib/strings/errors'
 import {logger} from '#/logger'
 import {useModalControls} from '#/state/modals'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {useListMembersQuery} from '#/state/queries/list-members'
 import {useSession} from '#/state/session'
-import {ProfileCard} from '#/view/com/profile/ProfileCard'
 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
-import {Button} from '#/view/com/util/forms/Button'
 import {List, type ListRef} from '#/view/com/util/List'
 import {ProfileCardFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
 import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
+import {atoms as a, useTheme} from '#/alf'
+import {Button, ButtonText} from '#/components/Button'
 import {ListFooter} from '#/components/Lists'
+import * as ProfileCard from '#/components/ProfileCard'
 import type * as bsky from '#/types/bsky'
 
 const LOADING_ITEM = {_reactKey: '__loading__'}
@@ -47,11 +48,12 @@ export function ListMembers({
   headerOffset?: number
   desktopFixedHeightOffset?: number
 }) {
+  const t = useTheme()
   const {_} = useLingui()
   const [isRefreshing, setIsRefreshing] = React.useState(false)
-  const {isMobile} = useWebMediaQueries()
   const {openModal} = useModalControls()
   const {currentAccount} = useSession()
+  const moderationOpts = useModerationOpts()
 
   const {
     data,
@@ -131,23 +133,6 @@ export function ListMembers({
   // rendering
   // =
 
-  const renderMemberButton = React.useCallback(
-    (profile: bsky.profile.AnyProfileView) => {
-      if (!isOwner) {
-        return null
-      }
-      return (
-        <Button
-          testID={`user-${profile.handle}-editBtn`}
-          type="default"
-          label={_(msg({message: 'Edit', context: 'action'}))}
-          onPress={() => onPressEditMembership(profile)}
-        />
-      )
-    },
-    [isOwner, onPressEditMembership, _],
-  )
-
   const renderItem = React.useCallback(
     ({item}: {item: any}) => {
       if (item === EMPTY_ITEM) {
@@ -171,26 +156,60 @@ export function ListMembers({
       } else if (item === LOADING_ITEM) {
         return <ProfileCardFeedLoadingPlaceholder />
       }
+
+      const profile = (item as AppBskyGraphDefs.ListItemView).subject
+      if (!moderationOpts) return null
+
       return (
-        <ProfileCard
-          testID={`user-${
-            (item as AppBskyGraphDefs.ListItemView).subject.handle
-          }`}
-          profile={(item as AppBskyGraphDefs.ListItemView).subject}
-          renderButton={renderMemberButton}
-          style={{paddingHorizontal: isMobile ? 8 : 14, paddingVertical: 4}}
-          noModFilter
-        />
+        <View
+          style={[a.py_md, a.px_xl, a.border_t, t.atoms.border_contrast_low]}>
+          <ProfileCard.Link profile={profile}>
+            <ProfileCard.Outer>
+              <ProfileCard.Header>
+                <ProfileCard.Avatar
+                  profile={profile}
+                  moderationOpts={moderationOpts}
+                />
+                <ProfileCard.NameAndHandle
+                  profile={profile}
+                  moderationOpts={moderationOpts}
+                />
+                {isOwner && (
+                  <Button
+                    testID={`user-${profile.handle}-editBtn`}
+                    label={_(msg({message: 'Edit', context: 'action'}))}
+                    onPress={() => onPressEditMembership(profile)}
+                    size="small"
+                    variant="solid"
+                    color="secondary">
+                    <ButtonText>
+                      <Trans context="action">Edit</Trans>
+                    </ButtonText>
+                  </Button>
+                )}
+              </ProfileCard.Header>
+
+              <ProfileCard.Labels
+                profile={profile}
+                moderationOpts={moderationOpts}
+              />
+
+              <ProfileCard.Description profile={profile} />
+            </ProfileCard.Outer>
+          </ProfileCard.Link>
+        </View>
       )
     },
     [
-      renderMemberButton,
       renderEmptyState,
       error,
       onPressTryAgain,
       onPressRetryLoadMore,
-      isMobile,
+      moderationOpts,
+      isOwner,
+      onPressEditMembership,
       _,
+      t,
     ],
   )
 
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index bd09d6514..cee950703 100644
--- a/src/view/com/profile/ProfileCard.tsx
+++ b/src/view/com/profile/ProfileCard.tsx
@@ -1,307 +1,36 @@
-import React from 'react'
-import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
-import {
-  AppBskyActorDefs,
-  moderateProfile,
-  ModerationDecision,
-} from '@atproto/api'
-import {useQueryClient} from '@tanstack/react-query'
+import {View} from 'react-native'
+import {type AppBskyActorDefs} from '@atproto/api'
 
-import {usePalette} from '#/lib/hooks/usePalette'
-import {getModerationCauseKey, isJustAMute} from '#/lib/moderation'
-import {makeProfileLink} from '#/lib/routes/links'
-import {sanitizeDisplayName} from '#/lib/strings/display-names'
-import {sanitizeHandle} from '#/lib/strings/handles'
-import {s} from '#/lib/styles'
-import {useProfileShadow} from '#/state/cache/profile-shadow'
-import {Shadow} from '#/state/cache/types'
 import {useModerationOpts} from '#/state/preferences/moderation-opts'
-import {precacheProfile} from '#/state/queries/profile'
-import {useSession} from '#/state/session'
-import {atoms as a} from '#/alf'
-import {
-  KnownFollowers,
-  shouldShowKnownFollowers,
-} from '#/components/KnownFollowers'
-import * as Pills from '#/components/Pills'
-import * as bsky from '#/types/bsky'
-import {Link} from '../util/Link'
-import {Text} from '../util/text/Text'
-import {PreviewableUserAvatar} from '../util/UserAvatar'
-import {FollowButton} from './FollowButton'
-
-export function ProfileCard({
-  testID,
-  profile: profileUnshadowed,
-  noModFilter,
-  noBg,
-  noBorder,
-  renderButton,
-  onPress,
-  style,
-  showKnownFollowers,
-}: {
-  testID?: string
-  profile: bsky.profile.AnyProfileView
-  noModFilter?: boolean
-  noBg?: boolean
-  noBorder?: boolean
-  renderButton?: (
-    profile: Shadow<bsky.profile.AnyProfileView>,
-  ) => React.ReactNode
-  onPress?: () => void
-  style?: StyleProp<ViewStyle>
-  showKnownFollowers?: boolean
-}) {
-  const queryClient = useQueryClient()
-  const pal = usePalette('default')
-  const profile = useProfileShadow(profileUnshadowed)
-  const moderationOpts = useModerationOpts()
-  const isLabeler = profile?.associated?.labeler
-
-  const onBeforePress = React.useCallback(() => {
-    onPress?.()
-    precacheProfile(queryClient, profile)
-  }, [onPress, profile, queryClient])
-
-  if (!moderationOpts) {
-    return null
-  }
-  const moderation = moderateProfile(profile, moderationOpts)
-  const modui = moderation.ui('profileList')
-  if (!noModFilter && modui.filter && !isJustAMute(modui)) {
-    return null
-  }
-
-  const knownFollowersVisible =
-    showKnownFollowers &&
-    shouldShowKnownFollowers(profile.viewer?.knownFollowers) &&
-    moderationOpts
-  const hasDescription = 'description' in profile
-
-  return (
-    <Link
-      testID={testID}
-      style={[
-        styles.outer,
-        pal.border,
-        noBorder && styles.outerNoBorder,
-        !noBg && pal.view,
-        style,
-      ]}
-      href={makeProfileLink(profile)}
-      title={profile.handle}
-      asAnchor
-      onBeforePress={onBeforePress}
-      anchorNoUnderline>
-      <View style={styles.layout}>
-        <View style={styles.layoutAvi}>
-          <PreviewableUserAvatar
-            size={40}
-            profile={profile}
-            moderation={moderation.ui('avatar')}
-            type={isLabeler ? 'labeler' : 'user'}
-          />
-        </View>
-        <View style={styles.layoutContent}>
-          <Text
-            emoji
-            type="lg"
-            style={[s.bold, pal.text, a.self_start]}
-            numberOfLines={1}
-            lineHeight={1.2}>
-            {sanitizeDisplayName(
-              profile.displayName || sanitizeHandle(profile.handle),
-              moderation.ui('displayName'),
-            )}
-          </Text>
-          <Text emoji type="md" style={[pal.textLight]} numberOfLines={1}>
-            {sanitizeHandle(profile.handle, '@')}
-          </Text>
-          <ProfileCardPills
-            followedBy={!!profile.viewer?.followedBy}
-            moderation={moderation}
-          />
-          {!!profile.viewer?.followedBy && <View style={s.flexRow} />}
-        </View>
-        {renderButton && !isLabeler ? (
-          <View style={styles.layoutButton}>{renderButton(profile)}</View>
-        ) : undefined}
-      </View>
-      {hasDescription || knownFollowersVisible ? (
-        <View style={styles.details}>
-          {hasDescription && profile.description ? (
-            <Text emoji style={pal.text} numberOfLines={4}>
-              {profile.description as string}
-            </Text>
-          ) : null}
-          {knownFollowersVisible ? (
-            <View
-              style={[
-                a.flex_row,
-                a.align_center,
-                a.gap_sm,
-                !!hasDescription && a.mt_md,
-              ]}>
-              <KnownFollowers
-                minimal
-                profile={profile}
-                moderationOpts={moderationOpts}
-              />
-            </View>
-          ) : null}
-        </View>
-      ) : null}
-    </Link>
-  )
-}
-
-export function ProfileCardPills({
-  followedBy,
-  moderation,
-}: {
-  followedBy: boolean
-  moderation: ModerationDecision
-}) {
-  const modui = moderation.ui('profileList')
-  if (!followedBy && !modui.inform && !modui.alert) {
-    return null
-  }
-
-  return (
-    <Pills.Row style={[a.pt_xs]}>
-      {followedBy && <Pills.FollowsYou />}
-      {modui.alerts.map(alert => (
-        <Pills.Label key={getModerationCauseKey(alert)} cause={alert} />
-      ))}
-      {modui.informs.map(inform => (
-        <Pills.Label key={getModerationCauseKey(inform)} cause={inform} />
-      ))}
-    </Pills.Row>
-  )
-}
+import {atoms as a, useTheme} from '#/alf'
+import * as ProfileCard from '#/components/ProfileCard'
 
 export function ProfileCardWithFollowBtn({
   profile,
-  noBg,
   noBorder,
-  onPress,
-  onFollow,
   logContext = 'ProfileCard',
-  showKnownFollowers,
 }: {
   profile: AppBskyActorDefs.ProfileView
-  noBg?: boolean
   noBorder?: boolean
-  onPress?: () => void
-  onFollow?: () => void
   logContext?: 'ProfileCard' | 'StarterPackProfilesList'
-  showKnownFollowers?: boolean
 }) {
-  const {currentAccount} = useSession()
-  const isMe = profile.did === currentAccount?.did
+  const t = useTheme()
+  const moderationOpts = useModerationOpts()
+
+  if (!moderationOpts) return null
 
   return (
-    <ProfileCard
-      profile={profile}
-      noBg={noBg}
-      noBorder={noBorder}
-      renderButton={
-        isMe
-          ? undefined
-          : profileShadow => (
-              <FollowButton
-                profile={profileShadow}
-                logContext={logContext}
-                onFollow={onFollow}
-              />
-            )
-      }
-      onPress={onPress}
-      showKnownFollowers={!isMe && showKnownFollowers}
-    />
+    <View
+      style={[
+        a.py_md,
+        a.px_xl,
+        !noBorder && [a.border_t, t.atoms.border_contrast_low],
+      ]}>
+      <ProfileCard.Default
+        profile={profile}
+        moderationOpts={moderationOpts}
+        logContext={logContext}
+      />
+    </View>
   )
 }
-
-const styles = StyleSheet.create({
-  outer: {
-    borderTopWidth: StyleSheet.hairlineWidth,
-    paddingHorizontal: 6,
-    paddingVertical: 4,
-  },
-  outerNoBorder: {
-    borderTopWidth: 0,
-  },
-  layout: {
-    flexDirection: 'row',
-    alignItems: 'center',
-  },
-  layoutAvi: {
-    alignSelf: 'flex-start',
-    width: 54,
-    paddingLeft: 4,
-    paddingTop: 10,
-  },
-  avi: {
-    width: 40,
-    height: 40,
-    borderRadius: 20,
-    resizeMode: 'cover',
-  },
-  layoutContent: {
-    flex: 1,
-    paddingRight: 10,
-    paddingTop: 10,
-    paddingBottom: 10,
-  },
-  layoutButton: {
-    paddingRight: 10,
-  },
-  details: {
-    justifyContent: 'center',
-    paddingLeft: 54,
-    paddingRight: 10,
-    paddingBottom: 10,
-  },
-  pills: {
-    alignItems: 'flex-start',
-    flexDirection: 'row',
-    flexWrap: 'wrap',
-    columnGap: 6,
-    rowGap: 2,
-  },
-  pill: {
-    borderRadius: 4,
-    paddingHorizontal: 6,
-    paddingVertical: 2,
-    justifyContent: 'center',
-  },
-  btn: {
-    paddingVertical: 7,
-    borderRadius: 50,
-    marginLeft: 6,
-    paddingHorizontal: 14,
-  },
-
-  followedBy: {
-    flexDirection: 'row',
-    paddingLeft: 54,
-    paddingRight: 20,
-    marginBottom: 10,
-    marginTop: -6,
-  },
-  followedByAviContainer: {
-    width: 24,
-    height: 36,
-  },
-  followedByAvi: {
-    width: 36,
-    height: 36,
-    borderRadius: 18,
-    padding: 2,
-  },
-  followsByDesc: {
-    flex: 1,
-    paddingRight: 10,
-  },
-})