From 26f747e246c1aaee5611048a6a50c2be8320415a Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 11 Jun 2024 21:48:05 +0800 Subject: [PATCH] fix: unread mutation (#53) * fix: unread mutation Signed-off-by: Innei * fix: batch unread api Signed-off-by: Innei --------- Signed-off-by: Innei --- .../components/entry-column/item-wrapper.tsx | 29 +--- .../src/components/feed-column/list.tsx | 57 +++---- src/renderer/src/hono.ts | 2 +- src/renderer/src/hooks/useEntryActions.tsx | 155 ++++++++++-------- src/renderer/src/queries/subscriptions.ts | 14 +- src/renderer/src/store/entry.ts | 1 + src/renderer/src/store/unread.ts | 27 ++- 7 files changed, 149 insertions(+), 136 deletions(-) diff --git a/src/renderer/src/components/entry-column/item-wrapper.tsx b/src/renderer/src/components/entry-column/item-wrapper.tsx index a5da7e107..3c04b0a92 100644 --- a/src/renderer/src/components/entry-column/item-wrapper.tsx +++ b/src/renderer/src/components/entry-column/item-wrapper.tsx @@ -1,12 +1,10 @@ import { useAsRead } from "@renderer/hooks/useAsRead" -import { useEntryActions } from "@renderer/hooks/useEntryActions" -import { apiClient } from "@renderer/lib/api-fetch" +import { useEntryActions, useRead } from "@renderer/hooks/useEntryActions" import { views } from "@renderer/lib/constants" import { showNativeMenu } from "@renderer/lib/native-menu" import { cn } from "@renderer/lib/utils" import { feedActions, useFeedStore } from "@renderer/store" -import { entryActions, useEntry } from "@renderer/store/entry" -import { useMutation } from "@tanstack/react-query" +import { useEntry } from "@renderer/store/entry" import { ReactVirtuosoItemPlaceholder } from "../ui/placeholder" @@ -29,20 +27,7 @@ export function EntryItemWrapper({ const asRead = useAsRead(entry) - const read = useMutation({ - mutationFn: async () => - apiClient.reads.$post({ - json: { - entryIds: [entry.entries.id], - }, - }), - onMutate: () => { - entryActions.optimisticUpdate(entry.entries.id, { - read: true, - }) - }, - // TODO fallback - }) + const markReadMutation = useRead(entry) // NOTE: prevent 0 height element, react virtuoso will not stop render any more if (!entry) return @@ -52,15 +37,17 @@ export function EntryItemWrapper({
{ e.stopPropagation() feedActions.setActiveEntry(entry.entries.id) - if (!entry.read) { - read.mutate() + if (!asRead) { + markReadMutation.mutate() } }} onDoubleClick={() => diff --git a/src/renderer/src/components/feed-column/list.tsx b/src/renderer/src/components/feed-column/list.tsx index f7fefd25b..19028b17d 100644 --- a/src/renderer/src/components/feed-column/list.tsx +++ b/src/renderer/src/components/feed-column/list.tsx @@ -5,7 +5,11 @@ import type { FeedViewType } from "@renderer/lib/enum" import { cn } from "@renderer/lib/utils" import type { FeedListModel, SubscriptionResponse } from "@renderer/models" import { Queries } from "@renderer/queries" -import { feedActions, useFeedActiveList, useUnreadStore } from "@renderer/store" +import { + feedActions, + useFeedActiveList, + useUnreadStore, +} from "@renderer/store" import { useMemo, useState } from "react" import { Link } from "react-router-dom" import { parse } from "tldts" @@ -13,11 +17,11 @@ import { parse } from "tldts" import { FeedCategory } from "./category" const useData = (view?: FeedViewType) => { - const subscriptions = useBizQuery(Queries.subscription.byView(view)) + const query = useBizQuery(Queries.subscription.byView(view)) // TODO Refactor this into category store return useMemo(() => { - if (!subscriptions.data) return null + if (!query.data) return null const categories = { list: {}, } as { @@ -29,10 +33,10 @@ const useData = (view?: FeedViewType) => { > } const domains: Record = {} - const data = structuredClone(subscriptions.data) + const subscriptions = structuredClone(query.data) - if (subscriptions) { - for (const subscription of data.subscriptions) { + if (query) { + for (const subscription of subscriptions) { if (!subscription.category && subscription.feeds.siteUrl) { const { domain } = parse(subscription.feeds.siteUrl) if (domain) { @@ -44,8 +48,8 @@ const useData = (view?: FeedViewType) => { } } } - if (subscriptions) { - for (const subscription of data.subscriptions) { + if (query) { + for (const subscription of subscriptions) { if (!subscription.category) { if (subscription.feeds.siteUrl) { const { domain } = parse(subscription.feeds.siteUrl) @@ -64,8 +68,6 @@ const useData = (view?: FeedViewType) => { } } - const unread = data.unreads[subscription.feedId] || 0 - subscription.unread = unread categories.list[subscription.category].list.push(subscription) } } @@ -77,7 +79,7 @@ const useData = (view?: FeedViewType) => { return { list, } as FeedListModel - }, [subscriptions]) + }, [query]) } export function FeedList({ className, @@ -92,6 +94,8 @@ export function FeedList({ const data = useData(view) const activeList = useFeedActiveList() + useBizQuery(Queries.subscription.unreadAll()) + const totalUnread = useUnreadStore((state) => { let unread = 0 data?.list.forEach((a) => { @@ -154,8 +158,7 @@ export function FeedList({
{ e.stopPropagation() @@ -172,7 +175,7 @@ export function FeedList({ Starred
- {data?.list?.length ? ( + {data?.list?.length ? sortedByUnread?.map((category) => ( - )) - ) : ( - !hideTitle && ( -
- - - Add some feeds - -
- ) - )} + )) : + !hideTitle && ( +
+ + + Add some feeds + +
+ )}
) } diff --git a/src/renderer/src/hono.ts b/src/renderer/src/hono.ts index 74e69102c..ec35c5850 100644 --- a/src/renderer/src/hono.ts +++ b/src/renderer/src/hono.ts @@ -31,7 +31,7 @@ declare const routes: hono_hono_base.HonoBase + useMutation({ + mutationFn: async () => + entry && + apiClient.collections.$post({ + json: { + entryId: entry?.entries.id, + }, + }), + + onMutate() { + if (!entry) return + entryActions.optimisticUpdate(entry.entries.id, { + collections: { + createdAt: new Date().toISOString(), + }, + }) + }, + onSuccess: () => { + toast("Collected.", { + duration: 1000, + }) + }, + }) + +export const useUnCollect = (entry: EntryModel | undefined) => + useMutation({ + mutationFn: async () => + entry && + apiClient.collections.$delete({ + json: { + entryId: entry?.entries.id, + }, + }), + + onMutate() { + if (!entry) return + entryActions.optimisticUpdate(entry.entries.id, { + collections: undefined, + }) + }, + onSuccess: () => { + toast("Uncollected.", { + duration: 1000, + }) + }, + }) + +export const useRead = (entry: EntryModel | undefined) => + useMutation({ + mutationFn: async () => + entry && + apiClient.reads.$post({ + json: { + entryIds: [entry.entries.id], + }, + }), + + onMutate: () => { + if (!entry) return + + entryActions.markRead(entry.feeds.id, entry.entries.id, true) + }, + }) +export const useUnread = (entry: EntryModel | undefined) => + useMutation({ + mutationFn: async () => + entry && + apiClient.reads.$delete({ + json: { + entryId: entry.entries.id, + }, + }), + + onMutate: () => { + if (!entry) return + + entryActions.markRead(entry.feeds.id, entry.entries.id, false) + }, + }) + export const useEntryActions = ({ view, entry, @@ -27,76 +108,10 @@ export const useEntryActions = ({ }, }) - const collect = useMutation({ - mutationFn: async () => - entry && apiClient.collections.$post({ - json: { - entryId: entry?.entries.id, - }, - }), - - onMutate() { - if (!entry) return - entryActions.optimisticUpdate(entry.entries.id, { - collections: { - createdAt: new Date().toISOString(), - }, - }) - }, - onSuccess: () => { - toast("Collected.", { - duration: 1000, - }) - }, - }) - const uncollect = useMutation({ - mutationFn: async () => - entry && apiClient.collections.$delete({ - json: { - entryId: entry?.entries.id, - }, - }), - - onMutate() { - if (!entry) return - entryActions.optimisticUpdate(entry.entries.id, { - collections: undefined, - }) - }, - onSuccess: () => { - toast("Uncollected.", { - duration: 1000, - }) - }, - }) - const read = useMutation({ - mutationFn: async () => - entry && - apiClient.reads.$post({ - json: { - entryIds: [entry.entries.id], - }, - }), - - onMutate: () => { - if (!entry) return - - entryActions.markRead(entry.feeds.id, entry.entries.id, true) - }, - }) - const unread = useMutation({ - mutationFn: async () => entry && apiClient.reads.$delete({ - json: { - entryId: entry.entries.id, - }, - }), - - onMutate: () => { - if (!entry) return - - entryActions.markRead(entry.feeds.id, entry.entries.id, false) - }, - }) + const collect = useCollect(entry) + const uncollect = useUnCollect(entry) + const read = useRead(entry) + const unread = useUnread(entry) if (!entry?.entries.url || view === undefined) return { items: [] } diff --git a/src/renderer/src/queries/subscriptions.ts b/src/renderer/src/queries/subscriptions.ts index 4181162dc..3079ef953 100644 --- a/src/renderer/src/queries/subscriptions.ts +++ b/src/renderer/src/queries/subscriptions.ts @@ -7,16 +7,7 @@ export const subscription = { byView: (view?: FeedViewType) => defineQuery( ["subscriptions", view], - async () => { - const [subscriptions, unreads] = await Promise.all([ - subscriptionActions.fetchByView(view), - unreadActions.fetchUnreadByView(view), - ]) - return { - subscriptions, - unreads, - } - }, + async () => subscriptionActions.fetchByView(view), { rootKey: ["subscriptions"], }, @@ -29,4 +20,7 @@ export const subscription = { return res.data }), + + unreadAll: () => + defineQuery(["unread-all"], async () => unreadActions.fetchUnreadAll()), } diff --git a/src/renderer/src/store/entry.ts b/src/renderer/src/store/entry.ts index 01abacfbb..588c83391 100644 --- a/src/renderer/src/store/entry.ts +++ b/src/renderer/src/store/entry.ts @@ -127,6 +127,7 @@ export const useEntryStore = createZustandStore( }, markRead: (feedId: string, entryId: string, read: boolean) => { + console.log(feedId, "feedId") unreadActions.incrementByFeedId(feedId, read ? -1 : 1) entryActions.optimisticUpdate(entryId, { read, diff --git a/src/renderer/src/store/unread.ts b/src/renderer/src/store/unread.ts index 0c16d147d..e3f227ebb 100644 --- a/src/renderer/src/store/unread.ts +++ b/src/renderer/src/store/unread.ts @@ -10,6 +10,7 @@ interface UnreadState { interface UnreadActions { updateByFeedId: (feedId: string, unread: number) => void fetchUnreadByView: (view?: FeedViewType) => Promise> + fetchUnreadAll: () => Promise> incrementByFeedId: (feedId: string, inc: number) => void internal_reset: () => void @@ -19,7 +20,7 @@ export const useUnreadStore = createZustandStore( { version: 0, }, -)((set, get) => ({ +)((set) => ({ data: {}, internal_reset() { @@ -32,14 +33,28 @@ export const useUnreadStore = createZustandStore( }) const { data } = unread + set((state) => produce(state, (state) => { - get().internal_reset() - for (const feedId in data) { - state.data[feedId] = data[feedId] - return state + for (const [key, value] of Object.entries(data)) { + state.data[key] = value + } + }), + ) + return data + }, + async fetchUnreadAll() { + const unread = await apiClient.reads.$get({ + query: {}, + }) + + const { data } = unread + this.internal_reset() + set((state) => + produce(state, (state) => { + for (const [key, value] of Object.entries(data)) { + state.data[key] = value } - return state }), ) return data