feat(timeline): add feed icon to timeline header

- Added `currentFeedTitle` prop to various entry components, including `EntryItemImpl`, `EntryVirtualListItem`, and `AllItem`, to improve context awareness in rendering.
- Integrated `useFeedHeaderTitle` hook in `EntryList` to fetch the current feed title dynamically.
- Updated related types and interfaces to accommodate the new prop, enhancing type safety and clarity.

These changes aim to provide a more contextual user experience when interacting with entry items in the application.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-09-17 00:54:58 +08:00
parent d4b1333617
commit f067404151
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
8 changed files with 89 additions and 47 deletions

1
.gitignore vendored
View File

@ -28,3 +28,4 @@ buildServer.json
apps/desktop/build/appxmanifest.xml
.claude/settings.local.json
.serena

View File

@ -73,7 +73,7 @@ const entrySelector = (state: EntryModel) => {
video,
}
}
export function AllItem({ entryId, translation }: UniversalItemProps) {
export function AllItem({ entryId, translation, currentFeedTitle }: UniversalItemProps) {
const entry = useEntry(entryId, entrySelector)
const simple = true
@ -132,32 +132,37 @@ export function AllItem({ entryId, translation }: UniversalItemProps) {
const related = feed || inbox
const thisFeedTitle = getPreferredTitle(related, entry?.titleEntry)
return (
<div
className={cn(
"cursor-menu group relative flex items-center py-2",
"cursor-menu group relative flex items-start py-2",
!isRead &&
"before:bg-accent before:absolute before:-left-4 before:top-[14px] before:block before:size-2 before:rounded-full",
)}
>
<FeedIcon target={related} fallback entry={entry?.iconEntry} size={20} />
<div className={cn("flex h-fit min-w-0 flex-1 flex-row items-center text-sm leading-tight")}>
<div
className={cn(
"mr-4 flex w-20 shrink-0 gap-1 text-xs",
"text-text-secondary",
isInCollection && "text-text-secondary",
isRead && dimRead && "text-text-tertiary",
)}
>
<EllipsisHorizontalTextWithTooltip className="truncate">
<FeedTitle
feed={related}
title={getPreferredTitle(related, entry?.titleEntry)}
className="space-x-0.5"
/>
</EllipsisHorizontalTextWithTooltip>
{currentFeedTitle !== thisFeedTitle && (
<div className="flex min-w-0 shrink-0 items-center gap-1">
<FeedIcon target={related} fallback entry={entry?.iconEntry} size={20} />
<div
className={cn(
"mr-4 flex w-20 shrink-0 gap-1 text-xs",
"text-text-secondary",
isInCollection && "text-text-secondary",
isRead && dimRead && "text-text-tertiary",
)}
>
<EllipsisHorizontalTextWithTooltip className="truncate">
<FeedTitle
feed={related}
title={getPreferredTitle(related, entry?.titleEntry)}
className="space-x-0.5"
/>
</EllipsisHorizontalTextWithTooltip>
</div>
</div>
)}
<div className={cn("flex h-fit min-w-0 flex-1 flex-row items-start text-sm leading-tight")}>
{entry.firstMedia && (
<Tooltip>
<TooltipRoot>
@ -201,22 +206,26 @@ export function AllItem({ entryId, translation }: UniversalItemProps) {
isRead && dimRead && "text-text-secondary",
)}
>
{entry?.title ? (
<EntryTranslation
className={cn(
"inline-flex min-w-0 items-center hyphens-auto font-medium",
lineClamp.title,
)}
source={titleCase(entry?.title ?? "")}
target={titleCase(translation?.title ?? "")}
/>
) : (
<EntryTranslation
className={cn("inline-flex items-center hyphens-auto", lineClamp.description)}
source={entry?.description}
target={translation?.description}
/>
)}
<EllipsisHorizontalTextWithTooltip>
{entry?.title ? (
<EntryTranslation
inline={false}
className={cn(
"flex min-w-0 flex-col justify-center hyphens-auto font-medium",
lineClamp.title,
)}
source={titleCase(entry?.title ?? "")}
target={titleCase(translation?.title ?? "")}
/>
) : (
<EntryTranslation
inline={false}
className={cn("inline-flex items-center hyphens-auto", lineClamp.description)}
source={entry?.description}
target={translation?.description}
/>
)}
</EllipsisHorizontalTextWithTooltip>
{!!isInCollection && <StarIcon className="absolute right-0 top-0" />}
</div>
{!simple && (

View File

@ -18,6 +18,7 @@ interface VirtualRowItemProps {
isStickyItem: boolean
isActiveStickyItem: boolean
measureElement: (element: Element | null) => void
currentFeedTitle?: string
}
const EntryHeadDateItem: FC<{
@ -50,6 +51,7 @@ export const VirtualRowItem: FC<VirtualRowItemProps> = memo(
isStickyItem,
isActiveStickyItem,
measureElement,
currentFeedTitle,
}) => {
return (
<Fragment key={virtualRowKey}>
@ -76,6 +78,7 @@ export const VirtualRowItem: FC<VirtualRowItemProps> = memo(
)}
<EntryVirtualListItem
currentFeedTitle={currentFeedTitle}
entryId={entriesIds[virtualRowIndex]!}
view={view}
data-index={virtualRowIndex}

View File

@ -14,14 +14,13 @@ import type { EntryListItemFC } from "./types"
interface EntryItemProps {
entryId: string
view: FeedViewType
currentFeedTitle?: string
}
const EntryItemImpl = memo(function EntryItemImpl({
entryId,
view,
}: {
entryId: string
view: FeedViewType
}) {
currentFeedTitle,
}: EntryItemProps) {
const enableTranslation = useGeneralSettingKey("translation")
const actionLanguage = useActionLanguage()
const translation = useEntryTranslation({
@ -41,16 +40,16 @@ const EntryItemImpl = memo(function EntryItemImpl({
return (
<EntryItemWrapper itemClassName={Item.wrapperClassName} entryId={entryId} view={view}>
<Item entryId={entryId} translation={translation} />
<Item entryId={entryId} translation={translation} currentFeedTitle={currentFeedTitle} />
</EntryItemWrapper>
)
})
export const EntryItem: FC<EntryItemProps> = memo(({ entryId, view }) => {
export const EntryItem: FC<EntryItemProps> = memo(({ entryId, view, currentFeedTitle }) => {
const hasEntry = useHasEntry(entryId)
if (!hasEntry) return null
return <EntryItemImpl entryId={entryId} view={view} />
return <EntryItemImpl entryId={entryId} view={view} currentFeedTitle={currentFeedTitle} />
})
export const EntryVirtualListItem = ({
@ -58,6 +57,7 @@ export const EntryVirtualListItem = ({
entryId,
view,
className,
currentFeedTitle,
...props
}: EntryItemProps &
React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
@ -69,7 +69,7 @@ export const EntryVirtualListItem = ({
return (
<div className="absolute left-0 top-0 w-full will-change-transform" ref={ref} {...props}>
<EntryItemImpl entryId={entryId} view={view} />
<EntryItemImpl entryId={entryId} view={view} currentFeedTitle={currentFeedTitle} />
</div>
)
}

View File

@ -22,10 +22,11 @@ import { useFollow } from "~/hooks/biz/useFollow"
import { getRouteParams, useRouteParams } from "~/hooks/biz/useRouteParams"
import { COMMAND_ID } from "~/modules/command/commands/id"
import { useRunCommandFn } from "~/modules/command/hooks/use-command"
import { useCommandShortcuts } from "~/modules/command/hooks/use-command-binding"
import { useCommandShortcut } from "~/modules/command/hooks/use-command-binding"
import { EntryHeader } from "~/modules/entry-content/components/entry-header"
import { FeedIcon } from "~/modules/feed/feed-icon"
import { useRefreshFeedMutation } from "~/queries/feed"
import { useFeedHeaderTitle } from "~/store/feed/hooks"
import { useFeedHeaderIcon, useFeedHeaderTitle } from "~/store/feed/hooks"
import { MarkAllReadButton } from "../components/mark-all-button"
import { useIsPreviewFeed } from "../hooks/useIsPreviewFeed"
@ -47,9 +48,11 @@ export const EntryListHeader: FC<{
const isPreview = useIsPreviewFeed()
const headerTitle = useFeedHeaderTitle()
const feedIcon = useFeedHeaderIcon()
const titleInfo = !!headerTitle && (
<div className="flex min-w-0 items-center break-all text-lg font-bold leading-tight">
{feedIcon && <FeedIcon target={feedIcon} fallback size={20} />}
<EllipsisHorizontalTextWithTooltip className="inline-block !w-auto max-w-full">
{headerTitle}
</EllipsisHorizontalTextWithTooltip>
@ -73,7 +76,7 @@ export const EntryListHeader: FC<{
}
const feedColumnShow = useSubscriptionColumnShow()
const commandShortcuts = useCommandShortcuts()
const toggleUnreadOnlyShortcut = useCommandShortcut(COMMAND_ID.timeline.unreadOnly)
const runCmdFn = useRunCommandFn()
const aiEnabled = useFeature("ai")
@ -149,7 +152,7 @@ export const EntryListHeader: FC<{
? t("entry_list_header.show_unread_only")
: t("entry_list_header.show_all")
}
shortcut={commandShortcuts[COMMAND_ID.timeline.unreadOnly]}
shortcut={toggleUnreadOnlyShortcut}
onClick={() => runCmdFn(COMMAND_ID.timeline.unreadOnly, [!unreadOnly])()}
>
{unreadOnly ? (

View File

@ -13,6 +13,7 @@ import { useEventCallback } from "usehooks-ts"
import { useGeneralSettingKey } from "~/atoms/settings/general"
import { m } from "~/components/common/Motion"
import { useFeedHeaderTitle } from "~/store/feed/hooks"
import { VirtualRowItem } from "./components/VirtualRowItem"
import { EntryColumnShortcutHandler } from "./EntryColumnShortcutHandler"
@ -187,6 +188,8 @@ export const EntryList: FC<EntryListProps> = memo(
})
}, [])
const currentFeedTitle = useFeedHeaderTitle()!
return (
<>
<div
@ -240,6 +243,7 @@ export const EntryList: FC<EntryListProps> = memo(
isStickyItem={isStickyItem}
isActiveStickyItem={isActiveStickyItem}
measureElement={rowVirtualizer.measureElement}
currentFeedTitle={currentFeedTitle}
/>
)
})}

View File

@ -6,6 +6,7 @@ import type { FC } from "react"
export type UniversalItemProps = {
entryId: string
translation?: EntryTranslation
currentFeedTitle?: string
}
export type EntryListItemFC<P extends object = object> = FC<P & UniversalItemProps> & {

View File

@ -1,9 +1,11 @@
import { views } from "@follow/constants"
import type { EntryModel } from "@follow/store/entry/types"
import { useFeedById, usePrefetchFeed } from "@follow/store/feed/hooks"
import { feedIconSelector } from "@follow/store/feed/selectors"
import { useListById, usePrefetchListById } from "@follow/store/list/hooks"
import { getSubscriptionByFeedId } from "@follow/store/subscription/getter"
import { useTranslation } from "react-i18next"
import { useShallow } from "zustand/shallow"
import {
FEED_COLLECTION_LIST,
@ -64,3 +66,22 @@ export const useFeedHeaderTitle = () => {
}
}
}
export const useFeedHeaderIcon = () => {
const { feedId: currentFeedId, listId: currentListId } = useRouteParams()
const feedIcon = useFeedById(currentFeedId, useShallow(feedIconSelector))
const listIcon = useListById(
currentListId,
useShallow((feed) => ({
type: feed.type,
ownerUserId: feed.ownerUserId,
id: feed.id,
title: feed.title,
url: (feed as any).url || "",
image: feed.image,
})),
)
return feedIcon || listIcon
}