diff --git a/package.json b/package.json
index 85db718dc1..8b99b70cce 100644
--- a/package.json
+++ b/package.json
@@ -57,6 +57,8 @@
"@emoji-mart/react": "^1.1.1",
"@expo/html-elements": "^0.4.2",
"@expo/webpack-config": "^19.0.0",
+ "@floating-ui/dom": "^1.6.3",
+ "@floating-ui/react-dom": "^2.0.8",
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.1",
diff --git a/src/components/ProfileHoverCard/index.tsx b/src/components/ProfileHoverCard/index.tsx
new file mode 100644
index 0000000000..980336ee4a
--- /dev/null
+++ b/src/components/ProfileHoverCard/index.tsx
@@ -0,0 +1,5 @@
+import {ProfileHoverCardProps} from './types'
+
+export function ProfileHoverCard({children}: ProfileHoverCardProps) {
+ return children
+}
diff --git a/src/components/ProfileHoverCard/index.web.tsx b/src/components/ProfileHoverCard/index.web.tsx
new file mode 100644
index 0000000000..cfb8cf2fc4
--- /dev/null
+++ b/src/components/ProfileHoverCard/index.web.tsx
@@ -0,0 +1,290 @@
+import React from 'react'
+import {View} from 'react-native'
+import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
+import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
+import {flip, offset, shift, size, useFloating} from '@floating-ui/react-dom'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+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 {useModerationOpts} from '#/state/queries/preferences'
+import {usePrefetchProfileQuery, useProfileQuery} from '#/state/queries/profile'
+import {useSession} from '#/state/session'
+import {useProfileShadow} from 'state/cache/profile-shadow'
+import {formatCount} from '#/view/com/util/numeric/format'
+import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {ProfileHeaderHandle} from '#/screens/Profile/Header/Handle'
+import {atoms as a, useTheme} from '#/alf'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import {useFollowMethods} from '#/components/hooks/useFollowMethods'
+import {useRichText} from '#/components/hooks/useRichText'
+import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
+import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
+import {InlineLinkText, Link} from '#/components/Link'
+import {Loader} from '#/components/Loader'
+import {Portal} from '#/components/Portal'
+import {RichText} from '#/components/RichText'
+import {Text} from '#/components/Typography'
+import {ProfileHoverCardProps} from './types'
+
+const floatingMiddlewares = [
+ offset(4),
+ flip({padding: 16}),
+ shift({padding: 16}),
+ size({
+ padding: 16,
+ apply({availableWidth, availableHeight, elements}) {
+ Object.assign(elements.floating.style, {
+ maxWidth: `${availableWidth}px`,
+ maxHeight: `${availableHeight}px`,
+ })
+ },
+ }),
+]
+
+const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0
+
+export function ProfileHoverCard(props: ProfileHoverCardProps) {
+ return isTouchDevice ? props.children :
+}
+
+export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
+ const [hovered, setHovered] = React.useState(false)
+ const {refs, floatingStyles} = useFloating({
+ middleware: floatingMiddlewares,
+ })
+ const prefetchProfileQuery = usePrefetchProfileQuery()
+
+ const prefetchedProfile = React.useRef(false)
+ const targetHovered = React.useRef(false)
+ const cardHovered = React.useRef(false)
+ const targetClicked = React.useRef(false)
+
+ const onPointerEnterTarget = React.useCallback(() => {
+ targetHovered.current = true
+
+ if (prefetchedProfile.current) {
+ // if we're navigating
+ if (targetClicked.current) return
+ setHovered(true)
+ } else {
+ prefetchProfileQuery(props.did).then(() => {
+ if (targetHovered.current) {
+ setHovered(true)
+ }
+ prefetchedProfile.current = true
+ })
+ }
+ }, [props.did, prefetchProfileQuery])
+ const onPointerEnterCard = React.useCallback(() => {
+ cardHovered.current = true
+ // if we're navigating
+ if (targetClicked.current) return
+ setHovered(true)
+ }, [])
+ const onPointerLeaveTarget = React.useCallback(() => {
+ targetHovered.current = false
+ setTimeout(() => {
+ if (cardHovered.current) return
+ setHovered(false)
+ }, 100)
+ }, [])
+ const onPointerLeaveCard = React.useCallback(() => {
+ cardHovered.current = false
+ setTimeout(() => {
+ if (targetHovered.current) return
+ setHovered(false)
+ }, 100)
+ }, [])
+ const onClickTarget = React.useCallback(() => {
+ targetClicked.current = true
+ setHovered(false)
+ }, [])
+ const hide = React.useCallback(() => {
+ setHovered(false)
+ }, [])
+
+ return (
+
+ {props.children}
+
+ {hovered && (
+
+
+
+
+
+
+
+ )}
+
+ )
+}
+
+function Card({did, hide}: {did: string; hide: () => void}) {
+ const t = useTheme()
+
+ const profile = useProfileQuery({did})
+ const moderationOpts = useModerationOpts()
+
+ const data = profile.data
+
+ return (
+
+ {data && moderationOpts ? (
+
+ ) : (
+
+
+
+ )}
+
+ )
+}
+
+function Inner({
+ profile,
+ moderationOpts,
+ hide,
+}: {
+ profile: AppBskyActorDefs.ProfileViewDetailed
+ moderationOpts: ModerationOpts
+ hide: () => void
+}) {
+ const t = useTheme()
+ const {_} = useLingui()
+ const {currentAccount} = useSession()
+ const moderation = React.useMemo(
+ () => moderateProfile(profile, moderationOpts),
+ [profile, moderationOpts],
+ )
+ const [descriptionRT] = useRichText(profile.description ?? '')
+ const profileShadow = useProfileShadow(profile)
+ const {follow, unfollow} = useFollowMethods({
+ profile: profileShadow,
+ logContext: 'ProfileHoverCard',
+ })
+ const blockHide = profile.viewer?.blocking || profile.viewer?.blockedBy
+ const following = formatCount(profile.followsCount || 0)
+ const followers = formatCount(profile.followersCount || 0)
+ const pluralizedFollowers = pluralize(profile.followersCount || 0, 'follower')
+ const profileURL = makeProfileLink({
+ did: profile.did,
+ handle: profile.handle,
+ })
+ const isMe = React.useMemo(
+ () => currentAccount?.did === profile.did,
+ [currentAccount, profile],
+ )
+
+ return (
+
+
+
+
+
+
+ {!isMe && (
+
+ )}
+
+
+
+
+
+ {sanitizeDisplayName(
+ profile.displayName || sanitizeHandle(profile.handle),
+ moderation.ui('displayName'),
+ )}
+
+
+
+
+
+
+ {!blockHide && (
+ <>
+
+
+
+ {followers}
+
+ {pluralizedFollowers}
+
+
+
+
+
+ {following}
+ following
+
+
+
+
+ {profile.description?.trim() && !moderation.ui('profileView').blur ? (
+
+
+
+ ) : undefined}
+ >
+ )}
+
+ )
+}
diff --git a/src/components/ProfileHoverCard/types.ts b/src/components/ProfileHoverCard/types.ts
new file mode 100644
index 0000000000..4e70df5f0d
--- /dev/null
+++ b/src/components/ProfileHoverCard/types.ts
@@ -0,0 +1,6 @@
+import React from 'react'
+
+export type ProfileHoverCardProps = {
+ children: React.ReactElement
+ did: string
+}
diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx
index 5cfa0b24f9..17f36c1418 100644
--- a/src/components/RichText.tsx
+++ b/src/components/RichText.tsx
@@ -7,7 +7,7 @@ import {toShortUrl} from '#/lib/strings/url-helpers'
import {isNative} from '#/platform/detection'
import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf'
import {useInteractionState} from '#/components/hooks/useInteractionState'
-import {InlineLinkText} from '#/components/Link'
+import {InlineLinkText, LinkProps} from '#/components/Link'
import {TagMenu, useTagMenuControl} from '#/components/TagMenu'
import {Text, TextProps} from '#/components/Typography'
@@ -22,6 +22,7 @@ export function RichText({
selectable,
enableTags = false,
authorHandle,
+ onLinkPress,
}: TextStyleProp &
Pick & {
value: RichTextAPI | string
@@ -30,6 +31,7 @@ export function RichText({
disableLinks?: boolean
enableTags?: boolean
authorHandle?: string
+ onLinkPress?: LinkProps['onPress']
}) {
const richText = React.useMemo(
() =>
@@ -90,7 +92,8 @@ export function RichText({
to={`/profile/${mention.did}`}
style={[...styles, {pointerEvents: 'auto'}]}
// @ts-ignore TODO
- dataSet={WORD_WRAP}>
+ dataSet={WORD_WRAP}
+ onPress={onLinkPress}>
{segment.text}
,
)
@@ -106,7 +109,8 @@ export function RichText({
style={[...styles, {pointerEvents: 'auto'}]}
// @ts-ignore TODO
dataSet={WORD_WRAP}
- shareOnLongPress>
+ shareOnLongPress
+ onPress={onLinkPress}>
{toShortUrl(segment.text)}
,
)
diff --git a/src/components/hooks/useFollowMethods.ts b/src/components/hooks/useFollowMethods.ts
new file mode 100644
index 0000000000..1e91a1f38a
--- /dev/null
+++ b/src/components/hooks/useFollowMethods.ts
@@ -0,0 +1,60 @@
+import React from 'react'
+import {AppBskyActorDefs} from '@atproto/api'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {LogEvents} from '#/lib/statsig/statsig'
+import {logger} from '#/logger'
+import {Shadow} from '#/state/cache/types'
+import {useProfileFollowMutationQueue} from '#/state/queries/profile'
+import {useRequireAuth} from '#/state/session'
+import * as Toast from '#/view/com/util/Toast'
+
+export function useFollowMethods({
+ profile,
+ logContext,
+}: {
+ profile: Shadow
+ logContext: LogEvents['profile:follow']['logContext'] &
+ LogEvents['profile:unfollow']['logContext']
+}) {
+ const {_} = useLingui()
+ const requireAuth = useRequireAuth()
+ const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
+ profile,
+ logContext,
+ )
+
+ const follow = React.useCallback(() => {
+ requireAuth(async () => {
+ try {
+ await queueFollow()
+ } catch (e: any) {
+ logger.error(`useFollowMethods: failed to follow`, {message: String(e)})
+ if (e?.name !== 'AbortError') {
+ Toast.show(_(msg`An issue occurred, please try again.`))
+ }
+ }
+ })
+ }, [_, queueFollow, requireAuth])
+
+ const unfollow = React.useCallback(() => {
+ requireAuth(async () => {
+ try {
+ await queueUnfollow()
+ } catch (e: any) {
+ logger.error(`useFollowMethods: failed to unfollow`, {
+ message: String(e),
+ })
+ if (e?.name !== 'AbortError') {
+ Toast.show(_(msg`An issue occurred, please try again.`))
+ }
+ }
+ })
+ }, [_, queueUnfollow, requireAuth])
+
+ return {
+ follow,
+ unfollow,
+ }
+}
diff --git a/src/components/hooks/useRichText.ts b/src/components/hooks/useRichText.ts
new file mode 100644
index 0000000000..e363ae5a93
--- /dev/null
+++ b/src/components/hooks/useRichText.ts
@@ -0,0 +1,33 @@
+import React from 'react'
+import {RichText as RichTextAPI} from '@atproto/api'
+
+import {getAgent} from '#/state/session'
+
+export function useRichText(text: string): [RichTextAPI, boolean] {
+ const [prevText, setPrevText] = React.useState(text)
+ const [rawRT, setRawRT] = React.useState(() => new RichTextAPI({text}))
+ const [resolvedRT, setResolvedRT] = React.useState(null)
+ if (text !== prevText) {
+ setPrevText(text)
+ setRawRT(new RichTextAPI({text}))
+ setResolvedRT(null)
+ // This will queue an immediate re-render
+ }
+ React.useEffect(() => {
+ let ignore = false
+ async function resolveRTFacets() {
+ // new each time
+ const resolvedRT = new RichTextAPI({text})
+ await resolvedRT.detectFacets(getAgent())
+ if (!ignore) {
+ setResolvedRT(resolvedRT)
+ }
+ }
+ resolveRTFacets()
+ return () => {
+ ignore = true
+ }
+ }, [text])
+ const isResolving = resolvedRT === null
+ return [resolvedRT ?? rawRT, isResolving]
+}
diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts
index 3d650b8b73..1231c5de5d 100644
--- a/src/lib/statsig/events.ts
+++ b/src/lib/statsig/events.ts
@@ -99,6 +99,7 @@ export type LogEvents = {
| 'ProfileHeader'
| 'ProfileHeaderSuggestedFollows'
| 'ProfileMenu'
+ | 'ProfileHoverCard'
}
'profile:unfollow': {
logContext:
@@ -108,5 +109,6 @@ export type LogEvents = {
| 'ProfileHeader'
| 'ProfileHeaderSuggestedFollows'
| 'ProfileMenu'
+ | 'ProfileHoverCard'
}
}
diff --git a/src/screens/Profile/Header/Handle.tsx b/src/screens/Profile/Header/Handle.tsx
index fd1cbe5333..9ab24fbbed 100644
--- a/src/screens/Profile/Header/Handle.tsx
+++ b/src/screens/Profile/Header/Handle.tsx
@@ -1,10 +1,10 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyActorDefs} from '@atproto/api'
-import {isInvalidHandle} from 'lib/strings/handles'
-import {Shadow} from '#/state/cache/types'
import {Trans} from '@lingui/macro'
+import {Shadow} from '#/state/cache/types'
+import {isInvalidHandle} from 'lib/strings/handles'
import {atoms as a, useTheme, web} from '#/alf'
import {Text} from '#/components/Typography'
@@ -26,6 +26,7 @@ export function ProfileHeaderHandle({
) : undefined}
{invalidHandle ? ā Invalid Handle : `@${profile.handle}`}
diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts
index a962fecff7..7842d53d4d 100644
--- a/src/state/queries/profile.ts
+++ b/src/state/queries/profile.ts
@@ -90,8 +90,8 @@ export function useProfilesQuery({handles}: {handles: string[]}) {
export function usePrefetchProfileQuery() {
const queryClient = useQueryClient()
const prefetchProfileQuery = useCallback(
- (did: string) => {
- queryClient.prefetchQuery({
+ async (did: string) => {
+ await queryClient.prefetchQuery({
queryKey: RQKEY(did),
queryFn: async () => {
const res = await getAgent().getProfile({actor: did || ''})
diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx
index 78b1677c3d..e1dae6659f 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 {Post} from '../post/Post'
-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 {TimeElapsed} from '../util/TimeElapsed'
+import {niceDate} from 'lib/strings/time'
+import {colors, s} from 'lib/styles'
import {isWeb} from 'platform/detection'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
+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 {formatCount} from '../util/numeric/format'
+import {Text} from '../util/text/Text'
+import {TimeElapsed} from '../util/TimeElapsed'
+import {PreviewableUserAvatar, UserAvatar} from '../util/UserAvatar'
const MAX_AUTHORS = 5
@@ -356,8 +358,10 @@ function CondensedAuthorsList({
{authors.slice(0, MAX_AUTHORS).map(author => (
-
{authors.map(author => (
-
-
-
-
-
-
- {sanitizeDisplayName(author.displayName || author.handle)}
-
-
- {sanitizeHandle(author.handle)}
+ label={_(msg`See profile`)}
+ to={makeProfileLink({
+ did: author.did,
+ handle: author.handle,
+ })}>
+
+
+
+
+
+
+
+
+ {sanitizeDisplayName(author.displayName || author.handle)}
+
+
+ {sanitizeHandle(author.handle)}
+
-
+
-
+
))}
)
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index 235139fff0..e6df5f6d0e 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>
- (
-
diff --git a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
index 3602cdb9a8..cf35885cd2 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,
},
]}>
-
diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx
index 529fc54e01..b37c69448e 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 => {
{opts.showAvatar && (
-
diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
index 4beedbd5b4..89aa56b736 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 {
- usePhotoLibraryPermission,
- useCameraPermission,
-} from 'lib/hooks/usePermissions'
-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 {
- Camera_Stroke2_Corner0_Rounded as Camera,
+ useCameraPermission,
+ usePhotoLibraryPermission,
+} from 'lib/hooks/usePermissions'
+import {makeProfileLink} from 'lib/routes/links'
+import {colors} from 'lib/styles'
+import {isAndroid, isNative, isWeb} from 'platform/detection'
+import {HighPriorityImage} from 'view/com/util/images/Image'
+import {tokens, useTheme} from '#/alf'
+import {
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 (
-
-
-
+
+
+
+
+
)
}
PreviewableUserAvatar = memo(PreviewableUserAvatar)
diff --git a/src/view/com/util/UserPreviewLink.tsx b/src/view/com/util/UserPreviewLink.tsx
deleted file mode 100644
index a2c46afc01..0000000000
--- 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
-}
-export function UserPreviewLink(
- props: React.PropsWithChildren,
-) {
- const prefetchProfileQuery = usePrefetchProfileQuery()
- return (
- {
- if (isWeb) {
- prefetchProfileQuery(props.did)
- }
- }}
- href={makeProfileLink(props)}
- title={props.handle}
- asAnchor
- style={props.style}>
- {props.children}
-
- )
-}
diff --git a/yarn.lock b/yarn.lock
index 1a61c8b037..39bfc6a206 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3511,6 +3511,13 @@
resolved "https://registry.yarnpkg.com/@flatten-js/interval-tree/-/interval-tree-1.1.2.tgz#fcc891da48bc230392884be01c26fe8c625702e8"
integrity sha512-OwLoV9E/XM6b7bes2rSFnGNjyRy7vcoIHFTnmBR2WAaZTf0Fe4EX4GdA65vU1KgFAasti7iRSg2dZfYd1Zt00Q==
+"@floating-ui/core@^1.0.0":
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.0.tgz#fa41b87812a16bf123122bf945946bae3fdf7fc1"
+ integrity sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==
+ dependencies:
+ "@floating-ui/utils" "^0.2.1"
+
"@floating-ui/core@^1.4.1":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.4.1.tgz#0d633f4b76052668afb932492ac452f7ebe97f17"
@@ -3526,6 +3533,14 @@
"@floating-ui/core" "^1.4.1"
"@floating-ui/utils" "^0.1.1"
+"@floating-ui/dom@^1.6.1", "@floating-ui/dom@^1.6.3":
+ version "1.6.3"
+ resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.3.tgz#954e46c1dd3ad48e49db9ada7218b0985cee75ef"
+ integrity sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==
+ dependencies:
+ "@floating-ui/core" "^1.0.0"
+ "@floating-ui/utils" "^0.2.0"
+
"@floating-ui/react-dom@^2.0.0":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.1.tgz#7972a4fc488a8c746cded3cfe603b6057c308a91"
@@ -3533,11 +3548,23 @@
dependencies:
"@floating-ui/dom" "^1.3.0"
+"@floating-ui/react-dom@^2.0.8":
+ version "2.0.8"
+ resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.8.tgz#afc24f9756d1b433e1fe0d047c24bd4d9cefaa5d"
+ integrity sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==
+ dependencies:
+ "@floating-ui/dom" "^1.6.1"
+
"@floating-ui/utils@^0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.1.tgz#1a5b1959a528e374e8037c4396c3e825d6cf4a83"
integrity sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==
+"@floating-ui/utils@^0.2.0", "@floating-ui/utils@^0.2.1":
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2"
+ integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==
+
"@fortawesome/fontawesome-common-types@6.4.2":
version "6.4.2"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz#1766039cad33f8ad87f9467b98e0d18fbc8f01c5"