about summary refs log tree commit diff
path: root/src/view/com
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2024-04-12 17:01:32 -0500
committerGitHub <noreply@github.com>2024-04-12 17:01:32 -0500
commit1f61109cfa8307cbbceea604b1daec7486dd3393 (patch)
treedbbad83a4367555e1586f6c2d5b0450612600d44 /src/view/com
parentf91aa37c6bd900bdc4eec1095c9ecd83da2f13f2 (diff)
downloadvoidsky-1f61109cfa8307cbbceea604b1daec7486dd3393.tar.zst
Profile card hover preview (#3508)
* feat: initial user card hover

* feat: flesh it out some more

* fix: initialize middlewares once

* chore: remove floating-ui react-native

* chore: clean up

* Update moderation apis, fix lint

* Refactor profile hover card to alf

* Clean up

* Debounce, fix positioning when loading

* Fix going away

* Close on all link presses

* Tweak styles

* Disable on mobile web

* cleanup some of the changes pt. 1

* cleanup some of the changes pt. 2

* cleanup some of the changes pt. 3

* cleanup some of the changes pt. 4

* Re-revert files

* Fix handle presentation

* Don't follow yourself, silly

* Collapsed notifications group

* ProfileCard

* Tree view replies

* Suggested follows

* Fix hover-back-on-card edge case

* Moar

---------

Co-authored-by: Mary <git@mary.my.id>
Co-authored-by: Hailey <me@haileyok.com>
Diffstat (limited to 'src/view/com')
-rw-r--r--src/view/com/notifications/FeedItem.tsx101
-rw-r--r--src/view/com/profile/ProfileCard.tsx37
-rw-r--r--src/view/com/profile/ProfileHeaderSuggestedFollows.tsx32
-rw-r--r--src/view/com/util/PostMeta.tsx23
-rw-r--r--src/view/com/util/UserAvatar.tsx42
-rw-r--r--src/view/com/util/UserPreviewLink.tsx31
6 files changed, 133 insertions, 133 deletions
diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx
index 78b1677c3..e1dae6659 100644
--- a/src/view/com/notifications/FeedItem.tsx
+++ b/src/view/com/notifications/FeedItem.tsx
@@ -1,20 +1,20 @@
-import React, {memo, useMemo, useState, useEffect} from 'react'
+import React, {memo, useEffect, useMemo, useState} from 'react'
 import {
   Animated,
-  TouchableOpacity,
   Pressable,
   StyleSheet,
+  TouchableOpacity,
   View,
 } from 'react-native'
 import {
+  AppBskyActorDefs,
   AppBskyEmbedImages,
+  AppBskyEmbedRecordWithMedia,
   AppBskyFeedDefs,
   AppBskyFeedPost,
-  ModerationOpts,
-  ModerationDecision,
   moderateProfile,
-  AppBskyEmbedRecordWithMedia,
-  AppBskyActorDefs,
+  ModerationDecision,
+  ModerationOpts,
 } from '@atproto/api'
 import {AtUri} from '@atproto/api'
 import {
@@ -22,28 +22,30 @@ import {
   FontAwesomeIconStyle,
   Props,
 } from '@fortawesome/react-native-fontawesome'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
 import {FeedNotification} from '#/state/queries/notifications/feed'
-import {s, colors} from 'lib/styles'
-import {niceDate} from 'lib/strings/time'
+import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
+import {usePalette} from 'lib/hooks/usePalette'
+import {HeartIconSolid} from 'lib/icons'
+import {makeProfileLink} from 'lib/routes/links'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
 import {sanitizeHandle} from 'lib/strings/handles'
 import {pluralize} from 'lib/strings/helpers'
-import {HeartIconSolid} from 'lib/icons'
-import {Text} from '../util/text/Text'
-import {UserAvatar, PreviewableUserAvatar} from '../util/UserAvatar'
-import {UserPreviewLink} from '../util/UserPreviewLink'
-import {ImageHorzList} from '../util/images/ImageHorzList'
+import {niceDate} from 'lib/strings/time'
+import {colors, s} from 'lib/styles'
+import {isWeb} from 'platform/detection'
+import {Link as NewLink} from '#/components/Link'
+import {ProfileHoverCard} from '#/components/ProfileHoverCard'
+import {FeedSourceCard} from '../feeds/FeedSourceCard'
 import {Post} from '../post/Post'
+import {ImageHorzList} from '../util/images/ImageHorzList'
 import {Link, TextLink} from '../util/Link'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
 import {formatCount} from '../util/numeric/format'
-import {makeProfileLink} from 'lib/routes/links'
+import {Text} from '../util/text/Text'
 import {TimeElapsed} from '../util/TimeElapsed'
-import {isWeb} from 'platform/detection'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {FeedSourceCard} from '../feeds/FeedSourceCard'
+import {PreviewableUserAvatar, UserAvatar} from '../util/UserAvatar'
 
 const MAX_AUTHORS = 5
 
@@ -356,8 +358,10 @@ function CondensedAuthorsList({
       <View style={styles.avis}>
         {authors.slice(0, MAX_AUTHORS).map(author => (
           <View key={author.href} style={s.mr5}>
-            <UserAvatar
+            <PreviewableUserAvatar
               size={35}
+              did={author.did}
+              handle={author.handle}
               avatar={author.avatar}
               moderation={author.moderation.ui('avatar')}
               type={author.associated?.labeler ? 'labeler' : 'user'}
@@ -386,6 +390,7 @@ function ExpandedAuthorsList({
   visible: boolean
   authors: Author[]
 }) {
+  const {_} = useLingui()
   const pal = usePalette('default')
   const heightInterp = useAnimatedValue(visible ? 1 : 0)
   const targetHeight =
@@ -409,33 +414,39 @@ function ExpandedAuthorsList({
         visible ? s.mb10 : undefined,
       ]}>
       {authors.map(author => (
-        <UserPreviewLink
+        <NewLink
           key={author.did}
-          did={author.did}
-          handle={author.handle}
-          style={styles.expandedAuthor}>
-          <View style={styles.expandedAuthorAvi}>
-            <UserAvatar
-              size={35}
-              avatar={author.avatar}
-              moderation={author.moderation.ui('avatar')}
-              type={author.associated?.labeler ? 'labeler' : 'user'}
-            />
-          </View>
-          <View style={s.flex1}>
-            <Text
-              type="lg-bold"
-              numberOfLines={1}
-              style={pal.text}
-              lineHeight={1.2}>
-              {sanitizeDisplayName(author.displayName || author.handle)}
-              &nbsp;
-              <Text style={[pal.textLight]} lineHeight={1.2}>
-                {sanitizeHandle(author.handle)}
+          label={_(msg`See profile`)}
+          to={makeProfileLink({
+            did: author.did,
+            handle: author.handle,
+          })}>
+          <View style={styles.expandedAuthor}>
+            <View style={styles.expandedAuthorAvi}>
+              <ProfileHoverCard did={author.did}>
+                <UserAvatar
+                  size={35}
+                  avatar={author.avatar}
+                  moderation={author.moderation.ui('avatar')}
+                  type={author.associated?.labeler ? 'labeler' : 'user'}
+                />
+              </ProfileHoverCard>
+            </View>
+            <View style={s.flex1}>
+              <Text
+                type="lg-bold"
+                numberOfLines={1}
+                style={pal.text}
+                lineHeight={1.2}>
+                {sanitizeDisplayName(author.displayName || author.handle)}
+                &nbsp;
+                <Text style={[pal.textLight]} lineHeight={1.2}>
+                  {sanitizeHandle(author.handle)}
+                </Text>
               </Text>
-            </Text>
+            </View>
           </View>
-        </UserPreviewLink>
+        </NewLink>
       ))}
     </Animated.View>
   )
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index 235139fff..e6df5f6d0 100644
--- a/src/view/com/profile/ProfileCard.tsx
+++ b/src/view/com/profile/ProfileCard.tsx
@@ -6,22 +6,23 @@ import {
   ModerationCause,
   ModerationDecision,
 } from '@atproto/api'
-import {Link} from '../util/Link'
-import {Text} from '../util/text/Text'
-import {UserAvatar} from '../util/UserAvatar'
-import {s} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {FollowButton} from './FollowButton'
-import {sanitizeDisplayName} from 'lib/strings/display-names'
-import {sanitizeHandle} from 'lib/strings/handles'
-import {makeProfileLink} from 'lib/routes/links'
-import {getModerationCauseKey, isJustAMute} from 'lib/moderation'
+import {Trans} from '@lingui/macro'
+
+import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
+import {useProfileShadow} from '#/state/cache/profile-shadow'
 import {Shadow} from '#/state/cache/types'
 import {useModerationOpts} from '#/state/queries/preferences'
-import {useProfileShadow} from '#/state/cache/profile-shadow'
 import {useSession} from '#/state/session'
-import {Trans} from '@lingui/macro'
-import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
+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 {Link} from '../util/Link'
+import {Text} from '../util/text/Text'
+import {PreviewableUserAvatar} from '../util/UserAvatar'
+import {FollowButton} from './FollowButton'
 
 export function ProfileCard({
   testID,
@@ -76,8 +77,10 @@ export function ProfileCard({
       anchorNoUnderline>
       <View style={styles.layout}>
         <View style={styles.layoutAvi}>
-          <UserAvatar
+          <PreviewableUserAvatar
             size={40}
+            did={profile.did}
+            handle={profile.handle}
             avatar={profile.avatar}
             moderation={moderation.ui('avatar')}
             type={isLabeler ? 'labeler' : 'user'}
@@ -221,9 +224,11 @@ function FollowersList({
       {followersWithMods.slice(0, 3).map(({f, mod}) => (
         <View key={f.did} style={styles.followedByAviContainer}>
           <View style={[styles.followedByAvi, pal.view]}>
-            <UserAvatar
-              avatar={f.avatar}
+            <PreviewableUserAvatar
               size={32}
+              did={f.did}
+              handle={f.handle}
+              avatar={f.avatar}
               moderation={mod.ui('avatar')}
               type={f.associated?.labeler ? 'labeler' : 'user'}
             />
diff --git a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
index 3602cdb9a..cf35885cd 100644
--- a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
+++ b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
@@ -1,28 +1,28 @@
 import React from 'react'
-import {View, StyleSheet, Pressable, ScrollView} from 'react-native'
+import {Pressable, ScrollView, StyleSheet, View} from 'react-native'
 import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
 import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
 } from '@fortawesome/react-native-fontawesome'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 
-import * as Toast from '../util/Toast'
+import {useProfileShadow} from '#/state/cache/profile-shadow'
+import {useModerationOpts} from '#/state/queries/preferences'
+import {useProfileFollowMutationQueue} from '#/state/queries/profile'
+import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows'
+import {useAnalytics} from 'lib/analytics/analytics'
 import {usePalette} from 'lib/hooks/usePalette'
-import {Text} from 'view/com/util/text/Text'
-import {UserAvatar} from 'view/com/util/UserAvatar'
-import {Button} from 'view/com/util/forms/Button'
+import {makeProfileLink} from 'lib/routes/links'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
 import {sanitizeHandle} from 'lib/strings/handles'
-import {makeProfileLink} from 'lib/routes/links'
-import {Link} from 'view/com/util/Link'
-import {useAnalytics} from 'lib/analytics/analytics'
 import {isWeb} from 'platform/detection'
-import {useModerationOpts} from '#/state/queries/preferences'
-import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows'
-import {useProfileShadow} from '#/state/cache/profile-shadow'
-import {useProfileFollowMutationQueue} from '#/state/queries/profile'
-import {useLingui} from '@lingui/react'
-import {Trans, msg} from '@lingui/macro'
+import {Button} from 'view/com/util/forms/Button'
+import {Link} from 'view/com/util/Link'
+import {Text} from 'view/com/util/text/Text'
+import {PreviewableUserAvatar} from 'view/com/util/UserAvatar'
+import * as Toast from '../util/Toast'
 
 const OUTER_PADDING = 10
 const INNER_PADDING = 14
@@ -218,8 +218,10 @@ function SuggestedFollow({
             backgroundColor: pal.view.backgroundColor,
           },
         ]}>
-        <UserAvatar
+        <PreviewableUserAvatar
           size={60}
+          did={profile.did}
+          handle={profile.handle}
           avatar={profile.avatar}
           moderation={moderation.ui('avatar')}
         />
diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx
index 529fc54e0..b37c69448 100644
--- a/src/view/com/util/PostMeta.tsx
+++ b/src/view/com/util/PostMeta.tsx
@@ -1,18 +1,19 @@
 import React, {memo} from 'react'
 import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
-import {Text} from './text/Text'
-import {TextLinkOnWebOnly} from './Link'
-import {niceDate} from 'lib/strings/time'
+import {AppBskyActorDefs, ModerationDecision, ModerationUI} from '@atproto/api'
+
+import {usePrefetchProfileQuery} from '#/state/queries/profile'
 import {usePalette} from 'lib/hooks/usePalette'
-import {TypographyVariant} from 'lib/ThemeContext'
-import {UserAvatar} from './UserAvatar'
+import {makeProfileLink} from 'lib/routes/links'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
 import {sanitizeHandle} from 'lib/strings/handles'
+import {niceDate} from 'lib/strings/time'
+import {TypographyVariant} from 'lib/ThemeContext'
 import {isAndroid, isWeb} from 'platform/detection'
+import {TextLinkOnWebOnly} from './Link'
+import {Text} from './text/Text'
 import {TimeElapsed} from './TimeElapsed'
-import {makeProfileLink} from 'lib/routes/links'
-import {AppBskyActorDefs, ModerationDecision, ModerationUI} from '@atproto/api'
-import {usePrefetchProfileQuery} from '#/state/queries/profile'
+import {PreviewableUserAvatar} from './UserAvatar'
 
 interface PostMetaOpts {
   author: AppBskyActorDefs.ProfileViewBasic
@@ -38,9 +39,11 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
     <View style={[styles.container, opts.style]}>
       {opts.showAvatar && (
         <View style={styles.avatar}>
-          <UserAvatar
-            avatar={opts.author.avatar}
+          <PreviewableUserAvatar
             size={opts.avatarSize || 16}
+            did={opts.author.did}
+            handle={opts.author.handle}
+            avatar={opts.author.avatar}
             moderation={opts.avatarModeration}
             type={opts.author.associated?.labeler ? 'labeler' : 'user'}
           />
diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
index 4beedbd5b..89aa56b73 100644
--- a/src/view/com/util/UserAvatar.tsx
+++ b/src/view/com/util/UserAvatar.tsx
@@ -1,30 +1,32 @@
 import React, {memo, useMemo} from 'react'
 import {Image, StyleSheet, TouchableOpacity, View} from 'react-native'
-import Svg, {Circle, Rect, Path} from 'react-native-svg'
 import {Image as RNImage} from 'react-native-image-crop-picker'
-import {useLingui} from '@lingui/react'
-import {msg, Trans} from '@lingui/macro'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import Svg, {Circle, Path, Rect} from 'react-native-svg'
 import {ModerationUI} from '@atproto/api'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 
-import {HighPriorityImage} from 'view/com/util/images/Image'
-import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
+import {usePalette} from 'lib/hooks/usePalette'
 import {
-  usePhotoLibraryPermission,
   useCameraPermission,
+  usePhotoLibraryPermission,
 } from 'lib/hooks/usePermissions'
+import {makeProfileLink} from 'lib/routes/links'
 import {colors} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {isWeb, isAndroid, isNative} from 'platform/detection'
-import {UserPreviewLink} from './UserPreviewLink'
-import * as Menu from '#/components/Menu'
+import {isAndroid, isNative, isWeb} from 'platform/detection'
+import {HighPriorityImage} from 'view/com/util/images/Image'
+import {tokens, useTheme} from '#/alf'
 import {
-  Camera_Stroke2_Corner0_Rounded as Camera,
   Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled,
+  Camera_Stroke2_Corner0_Rounded as Camera,
 } from '#/components/icons/Camera'
 import {StreamingLive_Stroke2_Corner0_Rounded as Library} from '#/components/icons/StreamingLive'
 import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
-import {useTheme, tokens} from '#/alf'
+import {Link} from '#/components/Link'
+import * as Menu from '#/components/Menu'
+import {ProfileHoverCard} from '#/components/ProfileHoverCard'
+import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
 
 export type UserAvatarType = 'user' | 'algo' | 'list' | 'labeler'
 
@@ -372,10 +374,18 @@ export {EditableUserAvatar}
 let PreviewableUserAvatar = (
   props: PreviewableUserAvatarProps,
 ): React.ReactNode => {
+  const {_} = useLingui()
   return (
-    <UserPreviewLink did={props.did} handle={props.handle}>
-      <UserAvatar {...props} />
-    </UserPreviewLink>
+    <ProfileHoverCard did={props.did}>
+      <Link
+        label={_(msg`See profile`)}
+        to={makeProfileLink({
+          did: props.did,
+          handle: props.handle,
+        })}>
+        <UserAvatar {...props} />
+      </Link>
+    </ProfileHoverCard>
   )
 }
 PreviewableUserAvatar = memo(PreviewableUserAvatar)
diff --git a/src/view/com/util/UserPreviewLink.tsx b/src/view/com/util/UserPreviewLink.tsx
deleted file mode 100644
index a2c46afc0..000000000
--- a/src/view/com/util/UserPreviewLink.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import React from 'react'
-import {StyleProp, ViewStyle} from 'react-native'
-import {Link} from './Link'
-import {isWeb} from 'platform/detection'
-import {makeProfileLink} from 'lib/routes/links'
-import {usePrefetchProfileQuery} from '#/state/queries/profile'
-
-interface UserPreviewLinkProps {
-  did: string
-  handle: string
-  style?: StyleProp<ViewStyle>
-}
-export function UserPreviewLink(
-  props: React.PropsWithChildren<UserPreviewLinkProps>,
-) {
-  const prefetchProfileQuery = usePrefetchProfileQuery()
-  return (
-    <Link
-      onPointerEnter={() => {
-        if (isWeb) {
-          prefetchProfileQuery(props.did)
-        }
-      }}
-      href={makeProfileLink(props)}
-      title={props.handle}
-      asAnchor
-      style={props.style}>
-      {props.children}
-    </Link>
-  )
-}