refactor(store): detach the inbox from the feed store, reduce re-rendering (#1228)
Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
5065c8e52d
commit
bc1f39bf67
|
|
@ -95,6 +95,7 @@ export function FeedPreview(props: {
|
|||
feeds: feedData,
|
||||
read: true,
|
||||
feedId: feedData.id!,
|
||||
inboxId: "",
|
||||
}}
|
||||
entryId={entry.id}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ import StarAnimationUri from "~/lottie/star.lottie?url"
|
|||
import { useTipModal } from "~/modules/wallet/hooks"
|
||||
import type { FlatEntryModel } from "~/store/entry"
|
||||
import { entryActions } from "~/store/entry"
|
||||
import { useFeedById } from "~/store/feed"
|
||||
import { getFeedById, useFeedById } from "~/store/feed"
|
||||
import { useInboxById } from "~/store/inbox"
|
||||
|
||||
import { navigateEntry } from "./useNavigateEntry"
|
||||
|
||||
|
|
@ -110,23 +111,27 @@ export const useUnCollect = (entry: Nullable<CombinedEntryModel>) => {
|
|||
|
||||
export const useRead = () =>
|
||||
useMutation({
|
||||
mutationFn: async (entry: Nullable<CombinedEntryModel>) =>
|
||||
entry &&
|
||||
entryActions.markRead({
|
||||
feedId: entry.feeds.id,
|
||||
mutationFn: async (entry: Nullable<CombinedEntryModel>) => {
|
||||
const relatedId = entry?.feeds?.id || entry?.inboxes?.id
|
||||
if (!relatedId) return
|
||||
return entryActions.markRead({
|
||||
feedId: relatedId,
|
||||
entryId: entry.entries.id,
|
||||
read: true,
|
||||
}),
|
||||
})
|
||||
},
|
||||
})
|
||||
export const useUnread = () =>
|
||||
useMutation({
|
||||
mutationFn: async (entry: Nullable<CombinedEntryModel>) =>
|
||||
entry &&
|
||||
entryActions.markRead({
|
||||
feedId: entry.feeds.id,
|
||||
mutationFn: async (entry: Nullable<CombinedEntryModel>) => {
|
||||
const relatedId = entry?.feeds?.id || entry?.inboxes?.id
|
||||
if (!relatedId) return
|
||||
return entryActions.markRead({
|
||||
feedId: relatedId,
|
||||
entryId: entry.entries.id,
|
||||
read: false,
|
||||
}),
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const useDeleteInboxEntry = () => {
|
||||
|
|
@ -155,21 +160,31 @@ export const useEntryActions = ({
|
|||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const feed = useFeedById(entry?.feedId)
|
||||
const isInbox = feed?.type === "inbox"
|
||||
const feed = useFeedById(entry?.feedId, (feed) => {
|
||||
return {
|
||||
type: feed.type,
|
||||
ownerUserId: feed.ownerUserId,
|
||||
id: feed.id,
|
||||
}
|
||||
})
|
||||
|
||||
const inbox = useInboxById(entry?.inboxId)
|
||||
const isInbox = !!inbox
|
||||
|
||||
const populatedEntry = useMemo(() => {
|
||||
if (!entry) return null
|
||||
if (!feed) return null
|
||||
if (!feed?.id && !inbox?.id) return null
|
||||
|
||||
return {
|
||||
...entry,
|
||||
feeds: feed!,
|
||||
feeds: feed ? getFeedById(feed.id) : undefined,
|
||||
inboxes: inbox,
|
||||
} as CombinedEntryModel
|
||||
}, [entry, feed])
|
||||
}, [entry, feed, inbox])
|
||||
|
||||
const openTipModal = useTipModal({
|
||||
userId: populatedEntry?.feeds.ownerUserId ?? undefined,
|
||||
feedId: populatedEntry?.feeds.id ?? undefined,
|
||||
userId: populatedEntry?.feeds?.ownerUserId ?? undefined,
|
||||
feedId: populatedEntry?.feeds?.id ?? undefined,
|
||||
entryId: populatedEntry?.entries.id ?? undefined,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -60,8 +60,15 @@ export const useFeedActions = ({
|
|||
type?: "feedList" | "entryList"
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const feed = useFeedById(feedId)
|
||||
const isInbox = feed?.type === "inbox"
|
||||
const feed = useFeedById(feedId, (feed) => {
|
||||
return {
|
||||
type: feed.type,
|
||||
ownerUserId: feed.ownerUserId,
|
||||
id: feed.id,
|
||||
}
|
||||
})
|
||||
const inbox = useInboxById(feedId)
|
||||
const isInbox = !!inbox
|
||||
const subscription = useSubscriptionByFeedId(feedId)
|
||||
const { present } = useModalStack()
|
||||
const deleteSubscription = useDeleteSubscription({})
|
||||
|
|
@ -81,7 +88,8 @@ export const useFeedActions = ({
|
|||
const isMultipleSelection = feedIds && feedIds.length > 0
|
||||
|
||||
const items = useMemo(() => {
|
||||
if (!feed) return []
|
||||
const related = feed || inbox
|
||||
if (!related) return []
|
||||
|
||||
const items: NullableNativeMenuItem[] = [
|
||||
{
|
||||
|
|
@ -95,9 +103,9 @@ export const useFeedActions = ({
|
|||
}),
|
||||
supportMultipleSelection: true,
|
||||
},
|
||||
!feed.ownerUserId &&
|
||||
!!isBizId(feed.id) &&
|
||||
feed.type === "feed" && {
|
||||
!related.ownerUserId &&
|
||||
!!isBizId(related.id) &&
|
||||
related.type === "feed" && {
|
||||
type: "text" as const,
|
||||
label: isEntryList
|
||||
? t("sidebar.feed_actions.claim_feed")
|
||||
|
|
@ -107,7 +115,7 @@ export const useFeedActions = ({
|
|||
claimFeed()
|
||||
},
|
||||
},
|
||||
...(feed.ownerUserId === whoami()?.id
|
||||
...(related.ownerUserId === whoami()?.id
|
||||
? [
|
||||
{
|
||||
type: "text" as const,
|
||||
|
|
@ -285,12 +293,13 @@ export const useFeedActions = ({
|
|||
return items
|
||||
}, [
|
||||
feed,
|
||||
inbox,
|
||||
t,
|
||||
isEntryList,
|
||||
isInbox,
|
||||
listByView,
|
||||
feedId,
|
||||
isMultipleSelection,
|
||||
feedId,
|
||||
feedIds,
|
||||
claimFeed,
|
||||
openBoostModal,
|
||||
|
|
|
|||
|
|
@ -7,9 +7,7 @@ import { FeedIcon } from "~/modules/feed/feed-icon"
|
|||
import type { EntryItemStatelessProps, UniversalItemProps } from "../types"
|
||||
|
||||
export function NotificationItem({ entryId, entryPreview, translation }: UniversalItemProps) {
|
||||
return (
|
||||
<ListItem entryId={entryId} entryPreview={entryPreview} translation={translation} withFollow />
|
||||
)
|
||||
return <ListItem entryId={entryId} entryPreview={entryPreview} translation={translation} />
|
||||
}
|
||||
|
||||
export function NotificationItemStateLess({ entry, feed }: EntryItemStatelessProps) {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ function EntryItemImpl({ entry, view }: { entry: FlatEntryModel; view?: number }
|
|||
|
||||
export const EntryItem: FC<EntryItemProps> = memo(({ entryId, view }) => {
|
||||
const entry = useEntry(entryId)
|
||||
|
||||
if (!entry) return <ReactVirtuosoItemPlaceholder />
|
||||
return <EntryItemImpl entry={entry} view={view} />
|
||||
})
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export const EntryItemWrapper: FC<
|
|||
})
|
||||
|
||||
const { items: feedItems } = useFeedActions({
|
||||
feedId: entry.feedId,
|
||||
feedId: entry.feedId || entry.inboxId,
|
||||
view,
|
||||
type: "entryList",
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
import { FollowIcon } from "@follow/components/icons/follow.jsx"
|
||||
import { Button } from "@follow/components/ui/button/index.js"
|
||||
import { EllipsisHorizontalTextWithTooltip } from "@follow/components/ui/typography/index.js"
|
||||
import { UrlBuilder } from "@follow/utils/url-builder"
|
||||
import { cn, isSafari } from "@follow/utils/utils"
|
||||
import { useDebounceCallback } from "usehooks-ts"
|
||||
|
||||
|
|
@ -9,17 +6,15 @@ import { AudioPlayer, useAudioPlayerAtomSelector } from "~/atoms/player"
|
|||
import { useUISettingKey } from "~/atoms/settings/ui"
|
||||
import { RelativeTime } from "~/components/ui/datetime"
|
||||
import { Media } from "~/components/ui/media"
|
||||
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
|
||||
import { FEED_COLLECTION_LIST } from "~/constants"
|
||||
import { useAsRead } from "~/hooks/biz/useAsRead"
|
||||
import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams"
|
||||
import { FeedForm } from "~/modules/discover/feed-form"
|
||||
import { EntryTranslation } from "~/modules/entry-column/translation"
|
||||
import { FeedIcon } from "~/modules/feed/feed-icon"
|
||||
import { Queries } from "~/queries"
|
||||
import { useEntry } from "~/store/entry/hooks"
|
||||
import { getPreferredTitle, useFeedById } from "~/store/feed"
|
||||
import { useSubscriptionStore } from "~/store/subscription"
|
||||
import { useInboxById } from "~/store/inbox"
|
||||
|
||||
import { ReactVirtuosoItemPlaceholder } from "../../../components/ui/placeholder"
|
||||
import { StarIcon } from "../star-icon"
|
||||
|
|
@ -31,11 +26,9 @@ export function ListItem({
|
|||
translation,
|
||||
withDetails,
|
||||
withAudio,
|
||||
withFollow,
|
||||
}: UniversalItemProps & {
|
||||
withDetails?: boolean
|
||||
withAudio?: boolean
|
||||
withFollow?: boolean
|
||||
}) {
|
||||
const entry = useEntry(entryId) || entryPreview
|
||||
|
||||
|
|
@ -43,11 +36,23 @@ export function ListItem({
|
|||
|
||||
const inInCollection = useRouteParamsSelector((s) => s.feedId === FEED_COLLECTION_LIST)
|
||||
|
||||
const feed = useFeedById(entry?.feedId) || entryPreview?.feeds
|
||||
const feed =
|
||||
useFeedById(entry?.feedId, (feed) => {
|
||||
return {
|
||||
type: feed.type,
|
||||
ownerUserId: feed.ownerUserId,
|
||||
id: feed.id,
|
||||
title: feed.title,
|
||||
url: (feed as any).url || "",
|
||||
image: feed.image,
|
||||
}
|
||||
}) || entryPreview?.feeds
|
||||
|
||||
const inbox = useInboxById(entry?.inboxId)
|
||||
|
||||
const handlePrefetchEntry = useDebounceCallback(
|
||||
() => {
|
||||
feed?.type === "inbox"
|
||||
inbox
|
||||
? Queries.entries.byInboxId(entryId).prefetch()
|
||||
: Queries.entries.byId(entryId).prefetch()
|
||||
},
|
||||
|
|
@ -55,22 +60,16 @@ export function ListItem({
|
|||
{ leading: false },
|
||||
)
|
||||
|
||||
const isSubscription =
|
||||
withFollow && entry?.entries.url?.startsWith(UrlBuilder.shareFeed(entry.feedId))
|
||||
const feedId = isSubscription
|
||||
? entry?.entries.url?.slice(UrlBuilder.shareFeed(entry.feedId).length)
|
||||
: undefined
|
||||
const isFollowed = !!useSubscriptionStore((state) => feedId && state.data[feedId])
|
||||
const { present } = useModalStack()
|
||||
|
||||
const settingWideMode = useUISettingKey("wideMode")
|
||||
|
||||
// NOTE: prevent 0 height element, react virtuoso will not stop render any more
|
||||
if (!entry || !feed) return <ReactVirtuosoItemPlaceholder />
|
||||
if (!entry || !(feed || inbox)) return <ReactVirtuosoItemPlaceholder />
|
||||
|
||||
const displayTime = inInCollection ? entry.collections?.createdAt : entry.entries.publishedAt
|
||||
const envIsSafari = isSafari()
|
||||
|
||||
const related = feed || inbox
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={handlePrefetchEntry}
|
||||
|
|
@ -82,7 +81,7 @@ export function ListItem({
|
|||
settingWideMode ? "py-3" : "py-4",
|
||||
)}
|
||||
>
|
||||
{!withAudio && <FeedIcon feed={feed} fallback entry={entry.entries} />}
|
||||
{!withAudio && <FeedIcon feed={related} fallback entry={entry.entries} />}
|
||||
<div
|
||||
className={cn(
|
||||
"-mt-0.5 flex-1 text-sm leading-tight",
|
||||
|
|
@ -99,7 +98,7 @@ export function ListItem({
|
|||
)}
|
||||
>
|
||||
<EllipsisHorizontalTextWithTooltip className="truncate">
|
||||
{getPreferredTitle(feed, entry.entries)}
|
||||
{getPreferredTitle(related, entry.entries)}
|
||||
</EllipsisHorizontalTextWithTooltip>
|
||||
<span>·</span>
|
||||
<span className="shrink-0">{!!displayTime && <RelativeTime date={displayTime} />}</span>
|
||||
|
|
@ -149,28 +148,6 @@ export function ListItem({
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* TODO remove This only share page needed */}
|
||||
{feedId && !isFollowed && (
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
present({
|
||||
title: `${APP_NAME}`,
|
||||
clickOutsideToDismiss: true,
|
||||
content: ({ dismiss }) => <FeedForm asWidget id={feedId} onSuccess={dismiss} />,
|
||||
})
|
||||
}}
|
||||
variant="outline"
|
||||
className="h-8"
|
||||
>
|
||||
<>
|
||||
<FollowIcon className="mr-1 size-3" />
|
||||
{APP_NAME}
|
||||
</>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{withAudio && entry.entries?.attachments?.[0].url && (
|
||||
<AudioCover
|
||||
entryId={entryId}
|
||||
|
|
@ -182,7 +159,7 @@ export function ListItem({
|
|||
feedIcon={
|
||||
<FeedIcon
|
||||
fallback={false}
|
||||
feed={feed}
|
||||
feed={feed || inbox}
|
||||
entry={entry.entries}
|
||||
size={settingWideMode ? 65 : 80}
|
||||
className="m-0 rounded"
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export type UniversalItemProps = {
|
|||
entryPreview?: CombinedEntryModel & {
|
||||
feeds: FeedOrListRespModel
|
||||
feedId: string
|
||||
inboxId: string
|
||||
}
|
||||
translation?: {
|
||||
title?: string
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { FeedIcon } from "~/modules/feed/feed-icon"
|
|||
import { Queries } from "~/queries"
|
||||
import { useEntry, useEntryReadHistory } from "~/store/entry"
|
||||
import { getPreferredTitle, useFeedById } from "~/store/feed"
|
||||
import { useInboxById } from "~/store/inbox"
|
||||
|
||||
import { EntryTranslation } from "../../entry-column/translation"
|
||||
|
||||
|
|
@ -28,10 +29,11 @@ export const EntryTitle = ({ entryId, compact }: EntryLinkProps) => {
|
|||
const user = useWhoami()
|
||||
const entry = useEntry(entryId)
|
||||
const feed = useFeedById(entry?.feedId)
|
||||
const inbox = useInboxById(entry?.inboxId)
|
||||
const entryHistory = useEntryReadHistory(entryId)
|
||||
|
||||
const populatedFullHref = useMemo(() => {
|
||||
if (feed?.type === "inbox") return entry?.entries.authorUrl
|
||||
if (inbox) return entry?.entries.authorUrl
|
||||
const href = entry?.entries.url
|
||||
if (!href) return "#"
|
||||
|
||||
|
|
@ -39,7 +41,7 @@ export const EntryTitle = ({ entryId, compact }: EntryLinkProps) => {
|
|||
const feedSiteUrl = feed?.type === "feed" ? feed.siteUrl : null
|
||||
if (href.startsWith("/") && feedSiteUrl) return safeUrl(href, feedSiteUrl)
|
||||
return href
|
||||
}, [entry?.entries.authorUrl, entry?.entries.url, feed])
|
||||
}, [entry?.entries.authorUrl, entry?.entries.url, feed?.siteUrl, feed?.type, inbox])
|
||||
|
||||
const translation = useAuthQuery(
|
||||
Queries.ai.translation({
|
||||
|
|
@ -61,10 +63,10 @@ export const EntryTitle = ({ entryId, compact }: EntryLinkProps) => {
|
|||
|
||||
return compact ? (
|
||||
<div className="-mx-6 flex cursor-button items-center gap-2 rounded-lg p-6 transition-colors @sm:-mx-3 @sm:p-3">
|
||||
<FeedIcon fallback feed={feed} entry={entry.entries} size={50} />
|
||||
<FeedIcon fallback feed={feed || inbox} entry={entry.entries} size={50} />
|
||||
<div className="leading-6">
|
||||
<div className="flex items-center gap-1 text-base font-semibold">
|
||||
<span>{entry.entries.author || feed?.title}</span>
|
||||
<span>{entry.entries.author || feed?.title || inbox?.title}</span>
|
||||
</div>
|
||||
<div className="text-zinc-500">
|
||||
<RelativeTime date={entry.entries.publishedAt} />
|
||||
|
|
@ -86,7 +88,7 @@ export const EntryTitle = ({ entryId, compact }: EntryLinkProps) => {
|
|||
/>
|
||||
</div>
|
||||
<div className="mt-2 text-[13px] font-medium text-zinc-500">
|
||||
{getPreferredTitle(feed, entry.entries)}
|
||||
{getPreferredTitle(feed || inbox, entry.entries)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-[13px] text-zinc-500">
|
||||
{entry.entries.publishedAt && new Date(entry.entries.publishedAt).toLocaleString()}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import {
|
|||
import { Queries } from "~/queries"
|
||||
import { useEntry } from "~/store/entry"
|
||||
import { useFeedById } from "~/store/feed"
|
||||
import { useInboxById } from "~/store/inbox"
|
||||
|
||||
import { EntryContentHTMLRenderer } from "../renderer/html"
|
||||
import {
|
||||
|
|
@ -93,9 +94,10 @@ export const EntryContentRender: Component<{
|
|||
|
||||
const feed = useFeedById(entry?.feedId) as FeedModel | InboxModel
|
||||
const readerRenderInlineStyle = useUISettingKey("readerRenderInlineStyle")
|
||||
const inbox = useInboxById(entry?.inboxId, (inbox) => inbox !== null)
|
||||
|
||||
const { error, data, isPending } = useAuthQuery(
|
||||
feed?.type === "inbox" ? Queries.entries.byInboxId(entryId) : Queries.entries.byId(entryId),
|
||||
inbox ? Queries.entries.byInboxId(entryId) : Queries.entries.byId(entryId),
|
||||
{
|
||||
staleTime: 300_000,
|
||||
},
|
||||
|
|
@ -186,7 +188,7 @@ export const EntryContentRender: Component<{
|
|||
})
|
||||
}
|
||||
|
||||
const isInbox = feed?.type === "inbox"
|
||||
const isInbox = !!inbox
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -266,7 +268,9 @@ export const EntryContentRender: Component<{
|
|||
{!content && (
|
||||
<div className="center mt-16 min-w-0">
|
||||
{isPending ? (
|
||||
<EntryContentLoading icon={!isInbox ? feed?.siteUrl! : undefined} />
|
||||
<EntryContentLoading
|
||||
icon={!isInbox ? (feed as FeedModel)?.siteUrl! : undefined}
|
||||
/>
|
||||
) : error ? (
|
||||
<div className="center flex min-w-0 flex-col gap-2">
|
||||
<i className="i-mgc-close-cute-re text-3xl text-red-500" />
|
||||
|
|
@ -310,10 +314,12 @@ const TitleMetaHandler: Component<{
|
|||
const {
|
||||
entries: { title: entryTitle },
|
||||
feedId,
|
||||
inboxId,
|
||||
} = useEntry(entryId)!
|
||||
|
||||
const { title: feedTitle } = useFeedById(feedId)!
|
||||
|
||||
const feed = useFeedById(feedId)
|
||||
const inbox = useInboxById(inboxId)
|
||||
const feedTitle = feed?.title || inbox?.title
|
||||
const atTop = useIsSoFWrappedElement()
|
||||
useEffect(() => {
|
||||
setEntryContentScrollToTop(true)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry"
|
|||
import { FeedIcon } from "~/modules/feed/feed-icon"
|
||||
import { useEntry } from "~/store/entry"
|
||||
import { useFeedById } from "~/store/feed"
|
||||
import { useInboxById } from "~/store/inbox"
|
||||
import { useListById } from "~/store/list"
|
||||
|
||||
const handleClickPlay = () => {
|
||||
|
|
@ -119,6 +120,8 @@ const CornerPlayerImpl = () => {
|
|||
})
|
||||
}, [entry, feed])
|
||||
|
||||
const isInbox = useInboxById(entry?.feedId, (inbox) => inbox !== null)
|
||||
|
||||
const navigateToEntry = useNavigateEntry()
|
||||
usePlayerTracker()
|
||||
|
||||
|
|
@ -127,9 +130,9 @@ const CornerPlayerImpl = () => {
|
|||
const options: NavigateEntryOptions = {
|
||||
entryId: entry.entries.id,
|
||||
}
|
||||
if (feed?.type === "inbox") {
|
||||
if (isInbox) {
|
||||
Object.assign(options, {
|
||||
inboxId: feed.id,
|
||||
inboxId: entry?.feedId,
|
||||
view: FeedViewType.Articles,
|
||||
})
|
||||
} else if (list) {
|
||||
|
|
|
|||
|
|
@ -43,7 +43,18 @@ const FeedItemImpl = ({ view, feedId, className }: FeedItemProps) => {
|
|||
const { t } = useTranslation()
|
||||
const subscription = useSubscriptionByFeedId(feedId)
|
||||
const navigate = useNavigateEntry()
|
||||
const feed = useFeedById(feedId)
|
||||
const feed = useFeedById(feedId, (feed) => {
|
||||
return {
|
||||
type: feed.type,
|
||||
id: feed.id,
|
||||
title: feed.title,
|
||||
errorAt: feed.errorAt,
|
||||
errorMessage: feed.errorMessage,
|
||||
url: feed.url,
|
||||
image: feed.image,
|
||||
}
|
||||
})
|
||||
|
||||
const [selectedFeedIds, setSelectedFeedIds] = useSelectedFeedIds()
|
||||
|
||||
const handleClick: React.MouseEventHandler<HTMLDivElement> = useCallback(
|
||||
|
|
|
|||
|
|
@ -64,6 +64,14 @@ const FallbackableImage = forwardRef<
|
|||
/>
|
||||
)
|
||||
})
|
||||
|
||||
type FeedIconFeed =
|
||||
| (Pick<FeedModel, "ownerUserId" | "id" | "title" | "url" | "image"> & {
|
||||
type: FeedOrListRespModel["type"]
|
||||
})
|
||||
| FeedOrListRespModel
|
||||
|
||||
type FeedIconEntry = Pick<CombinedEntryModel["entries"], "media" | "authorAvatar">
|
||||
export function FeedIcon({
|
||||
feed,
|
||||
entry,
|
||||
|
|
@ -74,8 +82,8 @@ export function FeedIcon({
|
|||
siteUrl,
|
||||
useMedia,
|
||||
}: {
|
||||
feed?: FeedOrListRespModel | null
|
||||
entry?: CombinedEntryModel["entries"]
|
||||
feed?: FeedIconFeed | null
|
||||
entry?: FeedIconEntry | null
|
||||
fallbackUrl?: string
|
||||
className?: string
|
||||
size?: number
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export const entries = {
|
|||
rootKey: ["entries"],
|
||||
}),
|
||||
byInboxId: (id: string) =>
|
||||
defineQuery(["entry", id], async () => entryActions.fetchInboxEntryById(id), {
|
||||
defineQuery(["entry", "inbox", id], async () => entryActions.fetchInboxEntryById(id), {
|
||||
rootKey: ["entries"],
|
||||
}),
|
||||
preview: (id: string) =>
|
||||
|
|
|
|||
|
|
@ -21,19 +21,16 @@ class EntryServiceStatic extends BaseService<EntryModel> implements Hydable {
|
|||
// @ts-expect-error
|
||||
override async upsertMany(data: EntryModel[], entryFeedMap: Record<string, string>) {
|
||||
const renewList = [] as { type: "entry"; id: string }[]
|
||||
const nextData = [] as (EntryModel & { feedId: string })[]
|
||||
const nextData = [] as (EntryModel & { feedId?: string; inboxId?: string })[]
|
||||
|
||||
for (const item of data) {
|
||||
const feedId = entryFeedMap[item.id]
|
||||
for (const entry of data) {
|
||||
const feedId = entryFeedMap[entry.id]
|
||||
if (!feedId) {
|
||||
console.error("EntryService.upsertMany: feedId not found", item)
|
||||
console.error("EntryService.upsertMany: feedId not found", entry)
|
||||
continue
|
||||
}
|
||||
renewList.push({ type: "entry", id: item.id })
|
||||
nextData.push({
|
||||
...item,
|
||||
feedId,
|
||||
})
|
||||
renewList.push({ type: "entry", id: entry.id })
|
||||
nextData.push(Object.assign({}, entry, feedId ? { feedId } : { inboxId: feedId }))
|
||||
}
|
||||
|
||||
CleanerService.reset(renewList)
|
||||
|
|
@ -62,7 +59,7 @@ class EntryServiceStatic extends BaseService<EntryModel> implements Hydable {
|
|||
}
|
||||
|
||||
override async findAll() {
|
||||
return super.findAll() as Promise<(EntryModel & { feedId: string })[]>
|
||||
return super.findAll() as Promise<(EntryModel & { feedId: string; inboxId: string })[]>
|
||||
}
|
||||
|
||||
bulkStoreReadStatus(record: Record<string, boolean>) {
|
||||
|
|
@ -121,6 +118,7 @@ class EntryServiceStatic extends BaseService<EntryModel> implements Hydable {
|
|||
collections: collections[entry.id] as {
|
||||
createdAt: string
|
||||
},
|
||||
inboxId: entry.inboxId,
|
||||
})
|
||||
}
|
||||
entryActions.hydrate(storeValue)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type {
|
|||
EntryModel,
|
||||
FeedModel,
|
||||
FeedOrListRespModel,
|
||||
InboxModel,
|
||||
} from "@follow/models/types"
|
||||
import type { EntryReadHistoriesModel } from "@follow/shared/hono"
|
||||
import { omitObjectUndefinedValue } from "@follow/utils/utils"
|
||||
|
|
@ -82,11 +83,12 @@ class EntryActions {
|
|||
},
|
||||
})
|
||||
if (data) {
|
||||
this.upsertMany([
|
||||
// patch data, should omit `read` because the network race condition or server cache
|
||||
omit(data, "read") as any,
|
||||
])
|
||||
inboxActions.upsertMany([data.feeds])
|
||||
// patch data, should omit `read` because the network race condition or server cache
|
||||
const nextData = omit(data, "feeds", "read")
|
||||
// Data compatibility
|
||||
if (data.feeds && !(data as any).inboxes) (nextData as any).inboxes = data.feeds
|
||||
this.upsertMany([nextData as any])
|
||||
inboxActions.upsertMany([(nextData as any).inboxes])
|
||||
}
|
||||
|
||||
return data
|
||||
|
|
@ -112,14 +114,26 @@ class EntryActions {
|
|||
isArchived?: boolean
|
||||
}) {
|
||||
const data = inboxId
|
||||
? await apiClient.entries.inbox.$post({
|
||||
json: {
|
||||
publishedAfter: pageParam,
|
||||
limit,
|
||||
inboxId: `${inboxId}`,
|
||||
read,
|
||||
},
|
||||
})
|
||||
? await apiClient.entries.inbox
|
||||
.$post({
|
||||
json: {
|
||||
publishedAfter: pageParam,
|
||||
limit,
|
||||
inboxId: `${inboxId}`,
|
||||
read,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
return {
|
||||
...res,
|
||||
data: res.data?.map(({ feeds, ...d }) => {
|
||||
return {
|
||||
...d,
|
||||
inboxes: feeds,
|
||||
}
|
||||
}),
|
||||
}
|
||||
})
|
||||
: await apiClient.entries.$post({
|
||||
json: {
|
||||
publishedAfter: pageParam,
|
||||
|
|
@ -220,6 +234,7 @@ class EntryActions {
|
|||
upsertMany(data: CombinedEntryModel[]) {
|
||||
const feeds = [] as FeedOrListRespModel[]
|
||||
const entries = [] as EntryModel[]
|
||||
const inboxes = [] as InboxModel[]
|
||||
const entry2Read = {} as Record<string, boolean>
|
||||
const entryFeedMap = {} as Record<string, string>
|
||||
const entryCollection = {} as Record<string, any>
|
||||
|
|
@ -233,38 +248,74 @@ class EntryActions {
|
|||
item.entries,
|
||||
)
|
||||
|
||||
if (!draft.entries[item.feeds.id]) {
|
||||
draft.entries[item.feeds.id] = []
|
||||
// Is related to feed
|
||||
if (item.feeds) {
|
||||
if (!draft.entries[item.feeds.id]) {
|
||||
draft.entries[item.feeds.id] = []
|
||||
}
|
||||
|
||||
if (!draft.internal_feedId2entryIdSet[item.feeds.id]) {
|
||||
draft.internal_feedId2entryIdSet[item.feeds.id] = new Set()
|
||||
}
|
||||
|
||||
if (!draft.internal_feedId2entryIdSet[item.feeds.id].has(item.entries.id)) {
|
||||
draft.entries[item.feeds.id].push(item.entries.id)
|
||||
draft.internal_feedId2entryIdSet[item.feeds.id].add(item.entries.id)
|
||||
}
|
||||
|
||||
draft.flatMapEntries[item.entries.id] = merge(
|
||||
draft.flatMapEntries[item.entries.id] || {},
|
||||
{
|
||||
feedId: item.feeds.id,
|
||||
entries: mergedEntry,
|
||||
},
|
||||
omit(item, "feeds"),
|
||||
)
|
||||
|
||||
// Push feed
|
||||
feeds.push(item.feeds)
|
||||
// Push entryFeedMap
|
||||
entryFeedMap[item.entries.id] = item.feeds.id
|
||||
}
|
||||
|
||||
if (!draft.internal_feedId2entryIdSet[item.feeds.id]) {
|
||||
draft.internal_feedId2entryIdSet[item.feeds.id] = new Set()
|
||||
// Is related to inbox
|
||||
if (item.inboxes) {
|
||||
const inboxId = `inbox-${item.inboxes.id}`
|
||||
if (!draft.entries[inboxId]) {
|
||||
draft.entries[inboxId] = []
|
||||
}
|
||||
|
||||
if (!draft.internal_feedId2entryIdSet[inboxId]) {
|
||||
draft.internal_feedId2entryIdSet[inboxId] = new Set()
|
||||
}
|
||||
|
||||
if (!draft.internal_feedId2entryIdSet[inboxId].has(item.entries.id)) {
|
||||
draft.entries[inboxId].push(item.entries.id)
|
||||
draft.internal_feedId2entryIdSet[inboxId].add(item.entries.id)
|
||||
}
|
||||
|
||||
draft.flatMapEntries[item.entries.id] = merge(
|
||||
draft.flatMapEntries[item.entries.id] || {},
|
||||
{
|
||||
inboxId: item.inboxes.id,
|
||||
entries: mergedEntry,
|
||||
},
|
||||
omit(item, "inboxes"),
|
||||
)
|
||||
|
||||
// Push entryFeedMap
|
||||
entryFeedMap[item.entries.id] = inboxId
|
||||
|
||||
inboxes.push(item.inboxes)
|
||||
}
|
||||
|
||||
if (!draft.internal_feedId2entryIdSet[item.feeds.id].has(item.entries.id)) {
|
||||
draft.entries[item.feeds.id].push(item.entries.id)
|
||||
draft.internal_feedId2entryIdSet[item.feeds.id].add(item.entries.id)
|
||||
}
|
||||
|
||||
draft.flatMapEntries[item.entries.id] = merge(
|
||||
draft.flatMapEntries[item.entries.id] || {},
|
||||
{
|
||||
feedId: item.feeds.id,
|
||||
entries: mergedEntry,
|
||||
},
|
||||
omit(item, "feeds"),
|
||||
)
|
||||
|
||||
// Push feed
|
||||
feeds.push(item.feeds)
|
||||
// Push entry
|
||||
entries.push(mergedEntry)
|
||||
// Push entry2Read
|
||||
if (!isNil(item.read)) {
|
||||
entry2Read[item.entries.id] = item.read
|
||||
}
|
||||
// Push entryFeedMap
|
||||
entryFeedMap[item.entries.id] = item.feeds.id
|
||||
|
||||
// Push entryCollection
|
||||
if ("collections" in item) {
|
||||
entryCollection[item.entries.id] = item.collections
|
||||
|
|
@ -291,6 +342,7 @@ class EntryActions {
|
|||
)
|
||||
// Insert to feed store
|
||||
feedActions.upsertMany(feeds as FeedModel[])
|
||||
inboxActions.upsertMany(inboxes)
|
||||
const newStarIds = new Set(get().starIds)
|
||||
for (const entryId in entryCollection) {
|
||||
newStarIds.add(entryId)
|
||||
|
|
|
|||
|
|
@ -7,9 +7,8 @@ type EntryId = string
|
|||
type EntriesIdTable = Record<FeedId, EntryId[]>
|
||||
|
||||
export type FlatEntryModel = Omit<CombinedEntryModel, "feeds"> & {
|
||||
feedId: FeedId
|
||||
view?: number
|
||||
}
|
||||
} & { feedId: FeedId; inboxId: string }
|
||||
export interface EntryState {
|
||||
/**
|
||||
* A map of feedId to entryIds
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { views } from "@follow/constants"
|
||||
import type { FeedOrListRespModel, InboxModel, ListModel } from "@follow/models/types"
|
||||
import type { FeedModel, FeedOrListRespModel, InboxModel, ListModel } from "@follow/models/types"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
|
|
@ -14,8 +14,14 @@ import { listActions, useListStore } from "../list"
|
|||
import { getPreferredTitle, useFeedStore } from "./store"
|
||||
import type { FeedQueryParams } from "./types"
|
||||
|
||||
export const useFeedById = (feedId: Nullable<string>): FeedOrListRespModel | null =>
|
||||
useFeedStore((state) => (feedId ? state.feeds[feedId] : null))
|
||||
export function useFeedById(feedId: Nullable<string>): FeedModel | null
|
||||
export function useFeedById<T>(feedId: Nullable<string>, selector: (feed: FeedModel) => T): T | null
|
||||
export function useFeedById<T>(feedId: Nullable<string>, selector?: (feed: FeedModel) => T) {
|
||||
return useFeedStore((state) => {
|
||||
const feed = feedId ? state.feeds[feedId] : null
|
||||
return selector ? feed && selector(feed) : feed
|
||||
})
|
||||
}
|
||||
|
||||
export const useFeedByIdOrUrl = (feed: FeedQueryParams) =>
|
||||
useFeedStore((state) => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import type {
|
||||
CombinedEntryModel,
|
||||
FeedModel,
|
||||
FeedOrListModel,
|
||||
FeedOrListRespModel,
|
||||
UserModel,
|
||||
} from "@follow/models/types"
|
||||
|
|
@ -38,18 +37,14 @@ class FeedActions {
|
|||
set((state) =>
|
||||
produce(state, (state) => {
|
||||
for (const feed of feeds) {
|
||||
if (
|
||||
feed.type === "feed" &&
|
||||
feed.errorAt &&
|
||||
new Date(feed.errorAt).getTime() > Date.now() - distanceTime
|
||||
) {
|
||||
if (feed.errorAt && new Date(feed.errorAt).getTime() > Date.now() - distanceTime) {
|
||||
feed.errorAt = null
|
||||
}
|
||||
if (feed.id) {
|
||||
if (feed.owner) {
|
||||
userActions.upsert(feed.owner as UserModel)
|
||||
}
|
||||
if (feed.type === "feed" && feed.tipUsers) {
|
||||
if (feed.tipUsers) {
|
||||
userActions.upsert(feed.tipUsers)
|
||||
}
|
||||
|
||||
|
|
@ -62,7 +57,7 @@ class FeedActions {
|
|||
feed.tipUsers = [...targetFeed.tipUsers]
|
||||
}
|
||||
|
||||
state.feeds[feed.id] = omit(feed, "feeds") as FeedOrListModel
|
||||
state.feeds[feed.id] = omit(feed, "feeds") as FeedModel
|
||||
} else {
|
||||
// Store temp feed in memory
|
||||
const nonce = feed["nonce"] || nanoid(8)
|
||||
|
|
@ -137,8 +132,8 @@ export const getFeedById = (feedId: string): Nullable<FeedOrListRespModel> =>
|
|||
useFeedStore.getState().feeds[feedId]
|
||||
|
||||
export const getPreferredTitle = (
|
||||
feed?: FeedOrListRespModel | null,
|
||||
entry?: CombinedEntryModel["entries"],
|
||||
feed?: Pick<FeedOrListRespModel, "type" | "id" | "title"> | null,
|
||||
entry?: Pick<CombinedEntryModel["entries"], "authorUrl"> | null,
|
||||
) => {
|
||||
if (!feed?.id) {
|
||||
return feed?.title
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import type { FeedOrListModel } from "@follow/models/types"
|
||||
import type { FeedModel } from "@follow/models/types"
|
||||
|
||||
type FeedId = string
|
||||
|
||||
export interface FeedState {
|
||||
feeds: Record<FeedId, FeedOrListModel>
|
||||
feeds: Record<FeedId, FeedModel>
|
||||
}
|
||||
export type FeedQueryParams = { id?: string; url?: string }
|
||||
|
|
|
|||
|
|
@ -2,5 +2,14 @@ import type { InboxModel } from "@follow/models/types"
|
|||
|
||||
import { useInboxStore } from "./store"
|
||||
|
||||
export const useInboxById = (inboxId: Nullable<string>): InboxModel | null =>
|
||||
useInboxStore((state) => (inboxId ? state.inboxes[inboxId] : null))
|
||||
export function useInboxById(inboxId: Nullable<string>): InboxModel | null
|
||||
export function useInboxById<T>(
|
||||
inboxId: Nullable<string>,
|
||||
selector: (inbox: InboxModel | null) => T,
|
||||
): T
|
||||
|
||||
export function useInboxById<T>(inboxId: Nullable<string>, selector?: (inbox: InboxModel) => T) {
|
||||
return useInboxStore((state) =>
|
||||
inboxId ? (selector ? selector(state.inboxes[inboxId]) : state.inboxes[inboxId]) : null,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,16 +23,17 @@ class ListActionStatic {
|
|||
for (const list of lists) {
|
||||
state.lists[list.id] = list
|
||||
|
||||
if (list.feeds)
|
||||
for (const feed of list.feeds) {
|
||||
feeds.push(feed)
|
||||
}
|
||||
if (!list.feeds) continue
|
||||
for (const feed of list.feeds) {
|
||||
feeds.push(feed)
|
||||
}
|
||||
}
|
||||
|
||||
feedActions.upsertMany(feeds)
|
||||
return state
|
||||
})
|
||||
|
||||
feedActions.upsertMany(feeds)
|
||||
|
||||
runTransactionInScope(() => ListService.upsertMany(lists))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -184,6 +184,7 @@ class SubscriptionActions {
|
|||
updateFeedBoostStatus(subscription.feedId, subscription.boost.boosters.length > 0)
|
||||
}
|
||||
}
|
||||
|
||||
this.updateCategoryOpenState(transformedData.filter((s) => s.category || s.defaultCategory))
|
||||
feedActions.upsertMany(feeds)
|
||||
listActions.upsertMany(lists)
|
||||
|
|
|
|||
|
|
@ -46,10 +46,12 @@ export type EntriesResponse = Array<
|
|||
| Exclude<Awaited<ReturnType<typeof _apiClient.entries.inbox.$post>>["data"], undefined>
|
||||
>[number]
|
||||
|
||||
export type CombinedEntryModel = EntriesResponse[number] & {
|
||||
export type CombinedEntryModel = Omit<EntriesResponse[number], "feeds"> & {
|
||||
entries: {
|
||||
content?: string | null
|
||||
}
|
||||
inboxes?: InboxModel
|
||||
feeds?: EntriesResponse[number]["feeds"]
|
||||
}
|
||||
export type EntryModel = CombinedEntryModel["entries"]
|
||||
export type EntryModelSimple = Exclude<
|
||||
|
|
|
|||
Loading…
Reference in New Issue