From 1f61109cfa8307cbbceea604b1daec7486dd3393 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 12 Apr 2024 17:01:32 -0500 Subject: 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 Co-authored-by: Hailey --- src/components/ProfileHoverCard/index.tsx | 5 + src/components/ProfileHoverCard/index.web.tsx | 290 +++++++++++++++++++++ src/components/ProfileHoverCard/types.ts | 6 + src/components/RichText.tsx | 10 +- src/components/hooks/useFollowMethods.ts | 60 +++++ src/components/hooks/useRichText.ts | 33 +++ src/lib/statsig/events.ts | 2 + src/screens/Profile/Header/Handle.tsx | 7 +- src/state/queries/profile.ts | 4 +- src/view/com/notifications/FeedItem.tsx | 101 +++---- src/view/com/profile/ProfileCard.tsx | 37 +-- .../com/profile/ProfileHeaderSuggestedFollows.tsx | 32 +-- src/view/com/util/PostMeta.tsx | 23 +- src/view/com/util/UserAvatar.tsx | 42 +-- src/view/com/util/UserPreviewLink.tsx | 31 --- 15 files changed, 542 insertions(+), 141 deletions(-) create mode 100644 src/components/ProfileHoverCard/index.tsx create mode 100644 src/components/ProfileHoverCard/index.web.tsx create mode 100644 src/components/ProfileHoverCard/types.ts create mode 100644 src/components/hooks/useFollowMethods.ts create mode 100644 src/components/hooks/useRichText.ts delete mode 100644 src/view/com/util/UserPreviewLink.tsx (limited to 'src') diff --git a/src/components/ProfileHoverCard/index.tsx b/src/components/ProfileHoverCard/index.tsx new file mode 100644 index 000000000..980336ee4 --- /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 000000000..cfb8cf2fc --- /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 000000000..4e70df5f0 --- /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 5cfa0b24f..17f36c141 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 000000000..1e91a1f38 --- /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 000000000..e363ae5a9 --- /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 3d650b8b7..1231c5de5 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 fd1cbe533..9ab24fbbe 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 a962fecff..7842d53d4 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 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({ {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 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> - ( - 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, }, ]}> - 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 => { {opts.showAvatar && ( - 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 ( - - - + + + + + ) } 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 -} -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} - - ) -} -- cgit 1.4.1