feat(desktop): integrate AvatarGroup component for enhanced user read history display
- Added AvatarGroup component to encapsulate user avatars in EntryReadHistory for improved layout and interaction. - Refactored EntryReadHistory to utilize AvatarGroup, simplifying the rendering of user avatars. - Updated EntryUser component to enhance user profile interaction and tooltip display. These changes enhance the visual presentation and user experience of the read history feature. Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
74f7d52277
commit
aff493f38f
|
|
@ -1,11 +1,4 @@
|
|||
import { ScrollArea } from "@follow/components/ui/scroll-area/index.js"
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardPortal,
|
||||
HoverCardTrigger,
|
||||
} from "@radix-ui/react-hover-card"
|
||||
import clsx from "clsx"
|
||||
import { AvatarGroup } from "@follow/components/ui/avatar-group/index.js"
|
||||
|
||||
import { useWhoami } from "~/atoms/user"
|
||||
import { useAuthQuery } from "~/hooks/common"
|
||||
|
|
@ -13,7 +6,7 @@ import { useAppLayoutGridContainerWidth } from "~/providers/app-grid-layout-cont
|
|||
import { Queries } from "~/queries"
|
||||
import { useEntryReadHistory } from "~/store/entry"
|
||||
|
||||
import { EntryUser, EntryUserRow, getLimit } from "./EntryReadHistory.shared"
|
||||
import { EntryUser, getLimit } from "./EntryReadHistory.shared"
|
||||
|
||||
export const EntryReadHistory: Component<{ entryId: string }> = ({ entryId }) => {
|
||||
const me = useWhoami()
|
||||
|
|
@ -37,64 +30,27 @@ export const EntryReadHistory: Component<{ entryId: string }> = ({ entryId }) =>
|
|||
className="animate-in fade-in @md:flex hidden items-center duration-200"
|
||||
data-hide-in-print
|
||||
>
|
||||
{entryHistory.userIds
|
||||
.filter((id) => id !== me?.id)
|
||||
.slice(0, LIMIT)
|
||||
<AvatarGroup>
|
||||
{entryHistory.userIds
|
||||
.filter((id) => id !== me?.id)
|
||||
.slice(0, LIMIT)
|
||||
|
||||
.map((userId, i) => (
|
||||
<EntryUser userId={userId} i={i} key={userId} />
|
||||
))}
|
||||
.map((userId) => (
|
||||
<EntryUser userId={userId} key={userId} />
|
||||
))}
|
||||
</AvatarGroup>
|
||||
|
||||
{!!totalCount && totalCount > LIMIT && (
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
marginLeft: `-${LIMIT * 8}px`,
|
||||
zIndex: LIMIT + 1,
|
||||
}}
|
||||
className="no-drag-region border-border bg-material-opaque ring-background relative flex size-7 items-center justify-center rounded-full border ring-2"
|
||||
>
|
||||
<span className="text-text-secondary text-[10px] font-medium tabular-nums">
|
||||
+{Math.min(totalCount - LIMIT, 99)}
|
||||
</span>
|
||||
</button>
|
||||
</HoverCardTrigger>
|
||||
|
||||
{totalCount > LIMIT && (
|
||||
<HoverCardPortal>
|
||||
<HoverCardContent
|
||||
sideOffset={12}
|
||||
align="start"
|
||||
side="right"
|
||||
className={clsx(
|
||||
"shadow-menu bg-material-medium shadow-context-menu backdrop-blur-background rounded-md border",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
"data-[state=closed]:slide-out-to-left-5 data-[state=open]:slide-in-from-left-5",
|
||||
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
||||
"transition-all duration-200 ease-in-out",
|
||||
)}
|
||||
>
|
||||
<ScrollArea.ScrollArea
|
||||
rootClassName="max-h-[300px] max-w-[20ch] flex flex-col"
|
||||
flex
|
||||
>
|
||||
<ul>
|
||||
{entryHistory.userIds
|
||||
.filter((id) => id !== me?.id)
|
||||
.slice(LIMIT)
|
||||
.map((userId) => (
|
||||
<EntryUserRow userId={userId} key={userId} />
|
||||
))}
|
||||
</ul>
|
||||
</ScrollArea.ScrollArea>
|
||||
</HoverCardContent>
|
||||
</HoverCardPortal>
|
||||
)}
|
||||
</HoverCard>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
margin: "-8px",
|
||||
zIndex: LIMIT + 1,
|
||||
}}
|
||||
className="no-drag-region border-border bg-material-opaque ring-background relative flex size-7 items-center justify-center rounded-full border ring-2"
|
||||
>
|
||||
<span className="text-text-secondary text-[10px] font-medium tabular-nums">
|
||||
+{Math.min(totalCount - LIMIT, 99)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,5 @@
|
|||
import { Avatar, AvatarFallback, AvatarImage } from "@follow/components/ui/avatar/index.jsx"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipPortal,
|
||||
TooltipTrigger,
|
||||
} from "@follow/components/ui/tooltip/index.jsx"
|
||||
import { EllipsisHorizontalTextWithTooltip } from "@follow/components/ui/typography/index.js"
|
||||
import { TooltipContent, TooltipPortal } from "@follow/components/ui/tooltip/index.jsx"
|
||||
import { FeedViewType } from "@follow/constants"
|
||||
import { getNameInitials } from "@follow/utils/cjk"
|
||||
import { m } from "motion/react"
|
||||
|
|
@ -36,74 +30,37 @@ export const getLimit = (width: number): number => {
|
|||
return 5
|
||||
}
|
||||
|
||||
export const EntryUserRow: Component<{ userId: string }> = memo(({ userId }) => {
|
||||
const user = useUserById(userId)
|
||||
const presentUserProfile = usePresentUserProfileModal("drawer")
|
||||
if (!user) return null
|
||||
|
||||
return (
|
||||
<li
|
||||
onClick={() => {
|
||||
presentUserProfile(userId)
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="cursor-button hover:bg-theme-selection-hover hover:text-theme-selection-foreground relative flex min-w-0 max-w-[50ch] shrink-0 items-center gap-2 truncate rounded-md p-1 px-2"
|
||||
>
|
||||
<Avatar className="border-border ring-background block aspect-square size-7 overflow-hidden rounded-full border ring-1">
|
||||
<AvatarImage src={replaceImgUrlIfNeed(user?.image || undefined)} />
|
||||
<AvatarFallback>{getNameInitials(user.name || "")}</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
{user.name && (
|
||||
<EllipsisHorizontalTextWithTooltip className="pr-8 text-xs font-medium">
|
||||
<span>{user.name}</span>
|
||||
</EllipsisHorizontalTextWithTooltip>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
})
|
||||
|
||||
export const EntryUser: Component<{
|
||||
userId: string
|
||||
i: number
|
||||
}> = memo(({ userId, i }) => {
|
||||
ref?: React.Ref<HTMLDivElement>
|
||||
}> = memo(({ userId, ref }) => {
|
||||
const user = useUserById(userId)
|
||||
const { t } = useTranslation()
|
||||
const presentUserProfile = usePresentUserProfileModal("drawer")
|
||||
if (!user) return null
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
className="no-drag-region relative cursor-pointer"
|
||||
style={{
|
||||
right: `${i * 8}px`,
|
||||
zIndex: i,
|
||||
<div className="no-drag-region relative cursor-pointer hover:!z-[99999]" ref={ref}>
|
||||
<m.button
|
||||
layout="position"
|
||||
layoutId={userId}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
presentUserProfile(userId)
|
||||
}}
|
||||
asChild
|
||||
>
|
||||
<m.button
|
||||
layout="position"
|
||||
layoutId={userId}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
presentUserProfile(userId)
|
||||
}}
|
||||
>
|
||||
<Avatar className="border-border ring-background aspect-square size-7 border ring-1">
|
||||
<AvatarImage
|
||||
src={replaceImgUrlIfNeed(user?.image || undefined)}
|
||||
className="bg-material-ultra-thick"
|
||||
/>
|
||||
<AvatarFallback>{getNameInitials(user.name || "")}</AvatarFallback>
|
||||
</Avatar>
|
||||
</m.button>
|
||||
</TooltipTrigger>
|
||||
<Avatar className="border-border ring-background aspect-square size-7 border ring-1">
|
||||
<AvatarImage
|
||||
src={replaceImgUrlIfNeed(user?.image || undefined)}
|
||||
className="bg-material-ultra-thick"
|
||||
/>
|
||||
<AvatarFallback>{getNameInitials(user.name || "")}</AvatarFallback>
|
||||
</Avatar>
|
||||
</m.button>
|
||||
<TooltipPortal>
|
||||
<TooltipContent side="top">
|
||||
{t("entry_actions.recent_reader")} {user.name}
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -148,6 +148,7 @@ export const EntryContent: Component<EntryContentProps> = ({
|
|||
/>
|
||||
<EntryTimelineSidebar entryId={entry.entries.id} />
|
||||
<EntryScrollArea className={className} scrollerRef={scrollerRef}>
|
||||
{/* Indicator for the entry */}
|
||||
<div
|
||||
className="animate-in fade-in slide-in-from-bottom-24 f-motion-reduce:fade-in-0 f-motion-reduce:slide-in-from-bottom-0 select-text duration-200 ease-in-out"
|
||||
key={entry.entries.id}
|
||||
|
|
@ -156,7 +157,8 @@ export const EntryContent: Component<EntryContentProps> = ({
|
|||
<>
|
||||
<div className="absolute inset-y-0 left-0 flex w-12 items-center justify-center opacity-0 duration-200 hover:opacity-100">
|
||||
<MotionButtonBase
|
||||
className="absolute left-0 shrink-0 cursor-pointer"
|
||||
// -12: Visual center point
|
||||
className="absolute left-0 shrink-0 !-translate-y-12 cursor-pointer"
|
||||
onClick={() => {
|
||||
EventBus.dispatch(COMMAND_ID.timeline.switchToPrevious)
|
||||
}}
|
||||
|
|
@ -167,7 +169,7 @@ export const EntryContent: Component<EntryContentProps> = ({
|
|||
|
||||
<div className="absolute inset-y-0 right-0 flex w-12 items-center justify-center opacity-0 duration-200 hover:opacity-100">
|
||||
<MotionButtonBase
|
||||
className="absolute right-0 shrink-0 cursor-pointer"
|
||||
className="absolute right-0 shrink-0 !-translate-y-12 cursor-pointer"
|
||||
onClick={() => {
|
||||
EventBus.dispatch(COMMAND_ID.timeline.switchToNext)
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
import { Spring } from "@follow/components/constants/spring"
|
||||
import { cn } from "@follow/utils/utils"
|
||||
import type { Transition } from "motion/react"
|
||||
import { m as motion } from "motion/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Tooltip, TooltipTrigger } from "../tooltip"
|
||||
|
||||
type AvatarProps = {
|
||||
children: React.ReactNode
|
||||
zIndex: number
|
||||
transition: Transition
|
||||
translate: string | number
|
||||
}
|
||||
|
||||
const AvatarContainer = React.memo(({ children, zIndex, transition, translate }: AvatarProps) => {
|
||||
return (
|
||||
<TooltipTrigger>
|
||||
<motion.div
|
||||
data-slot="avatar-container"
|
||||
initial="initial"
|
||||
whileHover="hover"
|
||||
whileTap="hover"
|
||||
className="relative"
|
||||
style={{ zIndex }}
|
||||
>
|
||||
<motion.div
|
||||
variants={{
|
||||
initial: { translateY: 0 },
|
||||
hover: { translateY: translate },
|
||||
}}
|
||||
transition={transition}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</TooltipTrigger>
|
||||
)
|
||||
})
|
||||
|
||||
type AvatarGroupProps = Omit<React.ComponentProps<"div">, "translate"> & {
|
||||
children: React.ReactElement[]
|
||||
transition?: Transition
|
||||
translate?: string | number
|
||||
}
|
||||
|
||||
function AvatarGroup({
|
||||
ref,
|
||||
children,
|
||||
className,
|
||||
transition = Spring.presets.smooth,
|
||||
translate = "-30%",
|
||||
|
||||
...props
|
||||
}: AvatarGroupProps) {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-slot="avatar-group"
|
||||
className={cn("flex h-8 flex-row items-center -space-x-2", className)}
|
||||
{...props}
|
||||
>
|
||||
{children?.map((child, index) => (
|
||||
<Tooltip delayDuration={0} key={index}>
|
||||
<AvatarContainer zIndex={index} transition={transition} translate={translate}>
|
||||
{child}
|
||||
</AvatarContainer>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { AvatarGroup, type AvatarGroupProps }
|
||||
Loading…
Reference in New Issue