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>
This commit is contained in:
parent
f91aa37c6b
commit
1f61109cfa
17 changed files with 576 additions and 146 deletions
|
@ -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({
|
|||
<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)}
|
||||
|
||||
<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)}
|
||||
|
||||
<Text style={[pal.textLight]} lineHeight={1.2}>
|
||||
{sanitizeHandle(author.handle)}
|
||||
</Text>
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</UserPreviewLink>
|
||||
</NewLink>
|
||||
))}
|
||||
</Animated.View>
|
||||
)
|
||||
|
|
|
@ -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'}
|
||||
/>
|
||||
|
|
|
@ -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')}
|
||||
/>
|
||||
|
|
|
@ -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'}
|
||||
/>
|
||||
|
|
|
@ -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 (
|
||||
<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)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue