diff --git a/apps/renderer/src/components/errors/previews/FeedPreview.tsx b/apps/renderer/src/components/errors/previews/FeedPreview.tsx index 17dcb00f7..0b5ed348c 100644 --- a/apps/renderer/src/components/errors/previews/FeedPreview.tsx +++ b/apps/renderer/src/components/errors/previews/FeedPreview.tsx @@ -95,6 +95,7 @@ export function FeedPreview(props: { feeds: feedData, read: true, feedId: feedData.id!, + inboxId: "", }} entryId={entry.id} /> diff --git a/apps/renderer/src/hooks/biz/useEntryActions.tsx b/apps/renderer/src/hooks/biz/useEntryActions.tsx index 4d13516a2..48d8d4f90 100644 --- a/apps/renderer/src/hooks/biz/useEntryActions.tsx +++ b/apps/renderer/src/hooks/biz/useEntryActions.tsx @@ -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) => { export const useRead = () => useMutation({ - mutationFn: async (entry: Nullable) => - entry && - entryActions.markRead({ - feedId: entry.feeds.id, + mutationFn: async (entry: Nullable) => { + 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) => - entry && - entryActions.markRead({ - feedId: entry.feeds.id, + mutationFn: async (entry: Nullable) => { + 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, }) diff --git a/apps/renderer/src/hooks/biz/useFeedActions.tsx b/apps/renderer/src/hooks/biz/useFeedActions.tsx index bc586bbe3..668154019 100644 --- a/apps/renderer/src/hooks/biz/useFeedActions.tsx +++ b/apps/renderer/src/hooks/biz/useFeedActions.tsx @@ -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, diff --git a/apps/renderer/src/modules/entry-column/Items/notification-item.tsx b/apps/renderer/src/modules/entry-column/Items/notification-item.tsx index e3706c37c..b8f23b3ed 100644 --- a/apps/renderer/src/modules/entry-column/Items/notification-item.tsx +++ b/apps/renderer/src/modules/entry-column/Items/notification-item.tsx @@ -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 ( - - ) + return } export function NotificationItemStateLess({ entry, feed }: EntryItemStatelessProps) { diff --git a/apps/renderer/src/modules/entry-column/item.tsx b/apps/renderer/src/modules/entry-column/item.tsx index c23b6820f..4c622fa92 100644 --- a/apps/renderer/src/modules/entry-column/item.tsx +++ b/apps/renderer/src/modules/entry-column/item.tsx @@ -52,6 +52,7 @@ function EntryItemImpl({ entry, view }: { entry: FlatEntryModel; view?: number } export const EntryItem: FC = memo(({ entryId, view }) => { const entry = useEntry(entryId) + if (!entry) return return }) diff --git a/apps/renderer/src/modules/entry-column/layouts/EntryItemWrapper.tsx b/apps/renderer/src/modules/entry-column/layouts/EntryItemWrapper.tsx index d80803667..8ce054b21 100644 --- a/apps/renderer/src/modules/entry-column/layouts/EntryItemWrapper.tsx +++ b/apps/renderer/src/modules/entry-column/layouts/EntryItemWrapper.tsx @@ -32,7 +32,7 @@ export const EntryItemWrapper: FC< }) const { items: feedItems } = useFeedActions({ - feedId: entry.feedId, + feedId: entry.feedId || entry.inboxId, view, type: "entryList", }) diff --git a/apps/renderer/src/modules/entry-column/templates/list-item-template.tsx b/apps/renderer/src/modules/entry-column/templates/list-item-template.tsx index 465dd7f80..0e6fa282b 100644 --- a/apps/renderer/src/modules/entry-column/templates/list-item-template.tsx +++ b/apps/renderer/src/modules/entry-column/templates/list-item-template.tsx @@ -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 + if (!entry || !(feed || inbox)) return const displayTime = inInCollection ? entry.collections?.createdAt : entry.entries.publishedAt const envIsSafari = isSafari() + const related = feed || inbox + return (
- {!withAudio && } + {!withAudio && }
- {getPreferredTitle(feed, entry.entries)} + {getPreferredTitle(related, entry.entries)} ยท {!!displayTime && } @@ -149,28 +148,6 @@ export function ListItem({ )}
- {/* TODO remove This only share page needed */} - {feedId && !isFollowed && ( - - )} - {withAudio && entry.entries?.attachments?.[0].url && ( { 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 ? (
- +
- {entry.entries.author || feed?.title} + {entry.entries.author || feed?.title || inbox?.title}
@@ -86,7 +88,7 @@ export const EntryTitle = ({ entryId, compact }: EntryLinkProps) => { />
- {getPreferredTitle(feed, entry.entries)} + {getPreferredTitle(feed || inbox, entry.entries)}
{entry.entries.publishedAt && new Date(entry.entries.publishedAt).toLocaleString()} diff --git a/apps/renderer/src/modules/entry-content/index.tsx b/apps/renderer/src/modules/entry-content/index.tsx index 7b2dd11b4..0d40dc26a 100644 --- a/apps/renderer/src/modules/entry-content/index.tsx +++ b/apps/renderer/src/modules/entry-content/index.tsx @@ -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 && (
{isPending ? ( - + ) : error ? (
@@ -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) diff --git a/apps/renderer/src/modules/feed-column/corner-player.tsx b/apps/renderer/src/modules/feed-column/corner-player.tsx index ae8f263cc..f89b450c6 100644 --- a/apps/renderer/src/modules/feed-column/corner-player.tsx +++ b/apps/renderer/src/modules/feed-column/corner-player.tsx @@ -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) { diff --git a/apps/renderer/src/modules/feed-column/item.tsx b/apps/renderer/src/modules/feed-column/item.tsx index 65b4f84e6..8eff896dd 100644 --- a/apps/renderer/src/modules/feed-column/item.tsx +++ b/apps/renderer/src/modules/feed-column/item.tsx @@ -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 = useCallback( diff --git a/apps/renderer/src/modules/feed/feed-icon.tsx b/apps/renderer/src/modules/feed/feed-icon.tsx index 5009ccf25..07af2acfa 100644 --- a/apps/renderer/src/modules/feed/feed-icon.tsx +++ b/apps/renderer/src/modules/feed/feed-icon.tsx @@ -64,6 +64,14 @@ const FallbackableImage = forwardRef< /> ) }) + +type FeedIconFeed = + | (Pick & { + type: FeedOrListRespModel["type"] + }) + | FeedOrListRespModel + +type FeedIconEntry = Pick 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 diff --git a/apps/renderer/src/queries/entries.ts b/apps/renderer/src/queries/entries.ts index 77e24db0b..6c84dc27d 100644 --- a/apps/renderer/src/queries/entries.ts +++ b/apps/renderer/src/queries/entries.ts @@ -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) => diff --git a/apps/renderer/src/services/entry.ts b/apps/renderer/src/services/entry.ts index 3788f4062..30dba80e6 100644 --- a/apps/renderer/src/services/entry.ts +++ b/apps/renderer/src/services/entry.ts @@ -21,19 +21,16 @@ class EntryServiceStatic extends BaseService implements Hydable { // @ts-expect-error override async upsertMany(data: EntryModel[], entryFeedMap: Record) { 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 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) { @@ -121,6 +118,7 @@ class EntryServiceStatic extends BaseService implements Hydable { collections: collections[entry.id] as { createdAt: string }, + inboxId: entry.inboxId, }) } entryActions.hydrate(storeValue) diff --git a/apps/renderer/src/store/entry/store.ts b/apps/renderer/src/store/entry/store.ts index 4cb8c4b4b..e24040686 100644 --- a/apps/renderer/src/store/entry/store.ts +++ b/apps/renderer/src/store/entry/store.ts @@ -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 const entryFeedMap = {} as Record const entryCollection = {} as Record @@ -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) diff --git a/apps/renderer/src/store/entry/types.ts b/apps/renderer/src/store/entry/types.ts index ee69f73f0..48341e8f4 100644 --- a/apps/renderer/src/store/entry/types.ts +++ b/apps/renderer/src/store/entry/types.ts @@ -7,9 +7,8 @@ type EntryId = string type EntriesIdTable = Record export type FlatEntryModel = Omit & { - feedId: FeedId view?: number -} +} & { feedId: FeedId; inboxId: string } export interface EntryState { /** * A map of feedId to entryIds diff --git a/apps/renderer/src/store/feed/hooks.ts b/apps/renderer/src/store/feed/hooks.ts index f002e82c4..d7cc850bd 100644 --- a/apps/renderer/src/store/feed/hooks.ts +++ b/apps/renderer/src/store/feed/hooks.ts @@ -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): FeedOrListRespModel | null => - useFeedStore((state) => (feedId ? state.feeds[feedId] : null)) +export function useFeedById(feedId: Nullable): FeedModel | null +export function useFeedById(feedId: Nullable, selector: (feed: FeedModel) => T): T | null +export function useFeedById(feedId: Nullable, 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) => { diff --git a/apps/renderer/src/store/feed/store.ts b/apps/renderer/src/store/feed/store.ts index c0749b328..da94b8327 100644 --- a/apps/renderer/src/store/feed/store.ts +++ b/apps/renderer/src/store/feed/store.ts @@ -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 => useFeedStore.getState().feeds[feedId] export const getPreferredTitle = ( - feed?: FeedOrListRespModel | null, - entry?: CombinedEntryModel["entries"], + feed?: Pick | null, + entry?: Pick | null, ) => { if (!feed?.id) { return feed?.title diff --git a/apps/renderer/src/store/feed/types.ts b/apps/renderer/src/store/feed/types.ts index d236e0582..e5aed997a 100644 --- a/apps/renderer/src/store/feed/types.ts +++ b/apps/renderer/src/store/feed/types.ts @@ -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 + feeds: Record } export type FeedQueryParams = { id?: string; url?: string } diff --git a/apps/renderer/src/store/inbox/hooks.ts b/apps/renderer/src/store/inbox/hooks.ts index a12c4b7be..6ff7d651a 100644 --- a/apps/renderer/src/store/inbox/hooks.ts +++ b/apps/renderer/src/store/inbox/hooks.ts @@ -2,5 +2,14 @@ import type { InboxModel } from "@follow/models/types" import { useInboxStore } from "./store" -export const useInboxById = (inboxId: Nullable): InboxModel | null => - useInboxStore((state) => (inboxId ? state.inboxes[inboxId] : null)) +export function useInboxById(inboxId: Nullable): InboxModel | null +export function useInboxById( + inboxId: Nullable, + selector: (inbox: InboxModel | null) => T, +): T + +export function useInboxById(inboxId: Nullable, selector?: (inbox: InboxModel) => T) { + return useInboxStore((state) => + inboxId ? (selector ? selector(state.inboxes[inboxId]) : state.inboxes[inboxId]) : null, + ) +} diff --git a/apps/renderer/src/store/list/store.ts b/apps/renderer/src/store/list/store.ts index 777fdd3ec..e43568fc6 100644 --- a/apps/renderer/src/store/list/store.ts +++ b/apps/renderer/src/store/list/store.ts @@ -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)) } diff --git a/apps/renderer/src/store/subscription/store.ts b/apps/renderer/src/store/subscription/store.ts index 261caa5a0..b36a507cd 100644 --- a/apps/renderer/src/store/subscription/store.ts +++ b/apps/renderer/src/store/subscription/store.ts @@ -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) diff --git a/packages/models/src/types.ts b/packages/models/src/types.ts index 82405ef20..548d8fb91 100644 --- a/packages/models/src/types.ts +++ b/packages/models/src/types.ts @@ -46,10 +46,12 @@ export type EntriesResponse = Array< | Exclude>["data"], undefined> >[number] -export type CombinedEntryModel = EntriesResponse[number] & { +export type CombinedEntryModel = Omit & { entries: { content?: string | null } + inboxes?: InboxModel + feeds?: EntriesResponse[number]["feeds"] } export type EntryModel = CombinedEntryModel["entries"] export type EntryModelSimple = Exclude<