From d39f95c5b322de8f98bfc4e4eddf48b084cf3876 Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 10 Jun 2025 21:45:56 +0800 Subject: [PATCH] fix(desktop): can update list after create list - Replaced grouped data hooks with direct ID retrieval for lists and inboxes to simplify data management. - Updated related components to utilize the new hooks for improved performance and clarity. - Enhanced list creation and update logic to utilize centralized actions for better maintainability. Signed-off-by: Innei --- .../modules/settings/tabs/lists/modals.tsx | 35 ++--- .../SubscriptionList.electron.tsx | 20 ++- .../SubscriptionList.mobile.tsx | 8 +- .../sort-by/SortByAlphabeticalList.tsx | 6 +- .../subscription-column/sort-by/types.tsx | 2 +- .../layer/renderer/src/services/list.ts | 6 + .../layer/renderer/src/store/list/store.ts | 136 ++++++++++++++++-- .../renderer/src/store/subscription/hooks.ts | 42 +++--- packages/internal/models/src/types.ts | 1 + 9 files changed, 178 insertions(+), 78 deletions(-) diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/lists/modals.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/lists/modals.tsx index fb03a8c0a..a4ab83b9c 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/tabs/lists/modals.tsx +++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/lists/modals.tsx @@ -35,16 +35,14 @@ import type { Suggestion } from "~/components/ui/auto-completion" import { Autocomplete } from "~/components/ui/auto-completion" import { useCurrentModal } from "~/components/ui/modal/stacked/hooks" import { useAddFeedToFeedList, useRemoveFeedFromFeedList } from "~/hooks/biz/useFeedActions" -import { apiClient } from "~/lib/api-fetch" import { createErrorToaster } from "~/lib/error-parser" import { UrlBuilder } from "~/lib/url-builder" import { FeedCertification } from "~/modules/feed/feed-certification" import { FeedIcon } from "~/modules/feed/feed-icon" import { ViewSelectorRadioGroup } from "~/modules/shared/ViewSelectorRadioGroup" -import { Queries } from "~/queries" import { useFeedById } from "~/store/feed" -import { useListById } from "~/store/list" -import { subscriptionActions, useAllFeeds } from "~/store/subscription" +import { listActions, useListById } from "~/store/list" +import { useAllFeeds } from "~/store/subscription" const formSchema = z.object({ view: z.string(), @@ -74,30 +72,23 @@ export const ListCreationModalContent = ({ id }: { id?: string }) => { const createMutation = useMutation({ mutationFn: async (values: z.infer) => { if (id) { - await apiClient.lists.$patch({ - json: { - listId: id, - ...values, - view: Number.parseInt(values.view), - }, + listActions.updateList({ + listId: id, + ...values, + view: Number.parseInt(values.view), }) } else { - await apiClient.lists.$post({ - json: { - ...values, - view: Number.parseInt(values.view), - }, + listActions.createList({ + ...values, + view: Number.parseInt(values.view), }) } }, - onSuccess: (_, values) => { - toast.success(t(id ? "lists.edit.success" : "lists.created.success")) - Queries.lists.list().invalidate() - dismiss() + onSuccess: (_) => { + const isCreate = !id + toast.success(t(isCreate ? "lists.created.success" : "lists.edit.success")) - if (!list) return - if (id) - subscriptionActions.changeListView(id, views[list.view]!.view, views[values.view].view) + dismiss() }, onError: createErrorToaster(id ? t("lists.edit.error") : t("lists.created.error")), }) diff --git a/apps/desktop/layer/renderer/src/modules/subscription-column/SubscriptionList.electron.tsx b/apps/desktop/layer/renderer/src/modules/subscription-column/SubscriptionList.electron.tsx index 210d3fe66..d97310a14 100644 --- a/apps/desktop/layer/renderer/src/modules/subscription-column/SubscriptionList.electron.tsx +++ b/apps/desktop/layer/renderer/src/modules/subscription-column/SubscriptionList.electron.tsx @@ -22,8 +22,8 @@ import { useList } from "~/queries/lists" import { useCategoryOpenStateByView, useFeedsGroupedData, - useInboxesGroupedData, - useListsGroupedData, + useSubscriptionInboxIds, + useSubscriptionListIds, } from "~/store/subscription" import { COMMAND_ID } from "../command/commands/id" @@ -45,23 +45,21 @@ import { EmptyFeedList, ListHeader, StarredItem } from "./SubscriptionList.share const SubscriptionImpl = ({ ref, className, view }: SubscriptionProps) => { const feedsData = useFeedsGroupedData(view) - const listsData = useListsGroupedData(view) - const inboxesData = useInboxesGroupedData(view) + const listSubIds = useSubscriptionListIds(view) + const inboxSubIds = useSubscriptionInboxIds(view) const categoryOpenStateData = useCategoryOpenStateByView(view) const hasData = - Object.keys(feedsData).length > 0 || - Object.keys(listsData).length > 0 || - Object.keys(inboxesData).length > 0 + Object.keys(feedsData).length > 0 || listSubIds.length > 0 || inboxSubIds.length > 0 const { t } = useTranslation() // Data prefetch useAuthQuery(Queries.lists.list()) - const hasListData = Object.keys(listsData).length > 0 - const hasInboxData = Object.keys(inboxesData).length > 0 + const hasListData = Object.keys(listSubIds).length > 0 + const hasInboxData = Object.keys(inboxSubIds).length > 0 const scrollerRef = useRef(null) const selectoRef = useRef(null) @@ -254,7 +252,7 @@ const SubscriptionImpl = ({ ref, className, view }: SubscriptionProps) => { isPreview /> )} - + )} {hasInboxData && ( @@ -262,7 +260,7 @@ const SubscriptionImpl = ({ ref, className, view }: SubscriptionProps) => {
{t("words.inbox")}
- + )} diff --git a/apps/desktop/layer/renderer/src/modules/subscription-column/SubscriptionList.mobile.tsx b/apps/desktop/layer/renderer/src/modules/subscription-column/SubscriptionList.mobile.tsx index a6085fc3c..3ce5d9a3c 100644 --- a/apps/desktop/layer/renderer/src/modules/subscription-column/SubscriptionList.mobile.tsx +++ b/apps/desktop/layer/renderer/src/modules/subscription-column/SubscriptionList.mobile.tsx @@ -10,8 +10,8 @@ import { Queries } from "~/queries" import { useCategoryOpenStateByView, useFeedsGroupedData, - useInboxesGroupedData, - useListsGroupedData, + useSubscriptionInboxIds, + useSubscriptionListIds, } from "~/store/subscription" import { SortableFeedList, SortByAlphabeticalInbox, SortByAlphabeticalList } from "./sort-by" @@ -21,8 +21,8 @@ import { EmptyFeedList, ListHeader, StarredItem } from "./SubscriptionList.share const FeedListImpl = ({ className, view }: SubscriptionProps) => { const feedsData = useFeedsGroupedData(view) - const listsData = useListsGroupedData(view) - const inboxesData = useInboxesGroupedData(view) + const listsData = useSubscriptionListIds(view) + const inboxesData = useSubscriptionInboxIds(view) const categoryOpenStateData = useCategoryOpenStateByView(view) const hasData = diff --git a/apps/desktop/layer/renderer/src/modules/subscription-column/sort-by/SortByAlphabeticalList.tsx b/apps/desktop/layer/renderer/src/modules/subscription-column/sort-by/SortByAlphabeticalList.tsx index a5745360d..b34e99c2a 100644 --- a/apps/desktop/layer/renderer/src/modules/subscription-column/sort-by/SortByAlphabeticalList.tsx +++ b/apps/desktop/layer/renderer/src/modules/subscription-column/sort-by/SortByAlphabeticalList.tsx @@ -96,7 +96,7 @@ export const SortByAlphabeticalFeedList = ({ export const SortByAlphabeticalListList = ({ view, data }: ListListProps) => { return (
- {Object.keys(data).map((listId) => ( + {data.map((listId) => ( ))}
@@ -106,8 +106,8 @@ export const SortByAlphabeticalListList = ({ view, data }: ListListProps) => { export const SortByAlphabeticalInboxList = ({ view, data }: ListListProps) => { return (
- {Object.keys(data).map((feedId) => ( - + {data.map((inboxId) => ( + ))}
) diff --git a/apps/desktop/layer/renderer/src/modules/subscription-column/sort-by/types.tsx b/apps/desktop/layer/renderer/src/modules/subscription-column/sort-by/types.tsx index b73404b24..18daa5529 100644 --- a/apps/desktop/layer/renderer/src/modules/subscription-column/sort-by/types.tsx +++ b/apps/desktop/layer/renderer/src/modules/subscription-column/sort-by/types.tsx @@ -9,5 +9,5 @@ export type SortBy = "count" | "alphabetical" export type ListListProps = { view: FeedViewType - data: Record + data: string[] } diff --git a/apps/desktop/layer/renderer/src/services/list.ts b/apps/desktop/layer/renderer/src/services/list.ts index 0f361bb5a..6d02fe94c 100644 --- a/apps/desktop/layer/renderer/src/services/list.ts +++ b/apps/desktop/layer/renderer/src/services/list.ts @@ -34,6 +34,12 @@ class ServiceStatic extends BaseService<{ id: string }> implements Hydratable { return this.table.bulkDelete(ids) } + async findAndUpdate(id: string, data: Partial) { + const list = await this.table.get(id) + if (!list) return + return this.table.put({ ...list, ...data }) + } + async hydrate() { const lists = await ListService.findAll() listActions.upsertMany(lists) diff --git a/apps/desktop/layer/renderer/src/store/list/store.ts b/apps/desktop/layer/renderer/src/store/list/store.ts index cfcf68d78..2f917da5a 100644 --- a/apps/desktop/layer/renderer/src/store/list/store.ts +++ b/apps/desktop/layer/renderer/src/store/list/store.ts @@ -1,4 +1,10 @@ -import type { FeedModel, ListModel, ListModelPoplutedFeeds } from "@follow/models/types" +import { views } from "@follow/constants" +import type { + ExtractHonoParams, + FeedModel, + ListModel, + ListModelPoplutedFeeds, +} from "@follow/models/types" import { sleep } from "@follow/utils/utils" import { runTransactionInScope } from "~/database" @@ -6,6 +12,7 @@ import { apiClient } from "~/lib/api-fetch" import { ListService } from "~/services/list" import { feedActions } from "../feed" +import { subscriptionActions } from "../subscription" import { createImmerSetter, createTransaction, createZustandStore } from "../utils/helper" import type { ListState } from "./types" @@ -15,34 +22,33 @@ export const useListStore = createZustandStore("list")(() => ({ const immerSet = createImmerSetter(useListStore) const get = useListStore.getState -const set = useListStore.setState + class ListActionStatic { upsertMany(lists: ListModelPoplutedFeeds[]) { if (lists.length === 0) return const feeds = [] as FeedModel[] - set((state) => { + + immerSet((state) => { for (const list of lists) { state.lists[list.id] = list - - if (!list.feeds) continue - for (const feed of list.feeds) { - feeds.push(feed) - } - } - - return { - ...state, - lists: { ...state.lists }, } + return state }) + for (const list of lists) { + if (!list.feeds) continue + for (const feed of list.feeds) { + feeds.push(feed) + } + } + feedActions.upsertMany(feeds) runTransactionInScope(() => ListService.upsertMany(lists)) } async fetchOwnedLists() { - const res = await apiClient.lists.list.$get() + const res = await apiClient.lists.list.$get({ query: {} }) this.upsertMany(res.data) return res.data @@ -119,6 +125,106 @@ class ListActionStatic { return res.data } + async createList(data: Omit) { + const tx = createTransaction() + tx.execute(async (_, ctx) => { + const res = await apiClient.lists.$post({ + json: data, + }) + + Object.assign(ctx, res.data) + }) + tx.persist(async (_, ctx) => { + if (!ctx.id) return + ListService.upsert({ + ...data, + id: ctx.id, + type: "list", + feedIds: [], + createdAt: new Date().toISOString(), + updatedAt: null, + }) + + const userId = ctx.ownerUserId + if (!userId) return + subscriptionActions.upsertMany([ + { + listId: ctx.id, + view: views[data.view]!.view, + createdAt: new Date().toISOString(), + title: data.title, + userId, + feedId: ctx.id, + isPrivate: false, + }, + ]) + this.upsertMany([ + { + ...data, + ...ctx, + id: ctx.id, + createdAt: new Date().toISOString(), + updatedAt: null, + type: "list", + feedIds: [], + }, + ]) + }) + tx.rollback(async (_, ctx) => { + if (!ctx.id) return + ListService.bulkDelete([ctx.id]) + }) + + await tx.run() + } + async updateList(data: InsertListModel) { + const tx = createTransaction() + const snapshot = get().lists[data.listId] + + tx.execute( + async () => + void (await apiClient.lists.$patch({ + json: { + ...data, + }, + })), + ) + tx.optimistic(async () => { + if (!snapshot) return + + this.upsertMany([ + { + ...snapshot, + ...data, + id: data.listId, + createdAt: snapshot?.createdAt, + updatedAt: new Date().toISOString(), + type: "list", + view: data.view, + feedIds: [], + fee: data.fee, + }, + ]) + }) + tx.persist(async () => { + ListService.findAndUpdate(data.listId, data) + + if (!snapshot) return + + subscriptionActions.changeListView( + data.listId, + views[snapshot.view]!.view, + views[data.view]!.view, + ) + }) + tx.rollback(async () => { + if (!snapshot) return + this.upsertMany([snapshot]) + }) + + await tx.run() + } + clear() { immerSet((state) => { state.lists = {} @@ -130,3 +236,5 @@ export const listActions = new ListActionStatic() export const getListById = (listId: string): Nullable => useListStore.getState().lists[listId] + +type InsertListModel = ExtractHonoParams diff --git a/apps/desktop/layer/renderer/src/store/subscription/hooks.ts b/apps/desktop/layer/renderer/src/store/subscription/hooks.ts index 634c12706..6a21a215f 100644 --- a/apps/desktop/layer/renderer/src/store/subscription/hooks.ts +++ b/apps/desktop/layer/renderer/src/store/subscription/hooks.ts @@ -149,40 +149,36 @@ export const useFeedsGroupedData = (view: FeedViewType) => { }, [autoGroup, data]) } -export const useListsGroupedData = (view: FeedViewType) => { +export const useSubscriptionListIds = (view: FeedViewType) => { const data = useSubscriptionByView(view) return useMemo(() => { - if (!data || data.length === 0) return {} - - const lists = data.filter((s) => s && "listId" in s) - - const groupFolder = {} as Record - - for (const subscription of lists.filter((s) => !!s)) { - groupFolder[subscription.feedId] = [subscription.feedId] + if (!data || data.length === 0) return [] + const ids: string[] = [] + for (const subscription of data) { + if (!subscription) continue + if ("listId" in subscription) { + ids.push(subscription.listId!) + } } - - return groupFolder + return ids }, [data]) } -export const useInboxesGroupedData = (view: FeedViewType) => { +export const useSubscriptionInboxIds = (view: FeedViewType) => { const data = useSubscriptionByView(view) return useMemo(() => { - if (!data || data.length === 0) return {} - - const inboxes = data.filter((s) => s && "inboxId" in s) - - const groupFolder = {} as Record - - for (const subscription of inboxes.filter((s) => !!s)) { - if (!subscription.inboxId) continue - groupFolder[subscription.inboxId] = [subscription.inboxId] + if (!data || data.length === 0) return [] + // return data.filter((s) => s && "inboxId" in s). as unknown[] as string[] + const ids: string[] = [] + for (const subscription of data) { + if (!subscription) continue + if ("inboxId" in subscription) { + ids.push(subscription.inboxId!) + } } - - return groupFolder + return ids }, [data]) } diff --git a/packages/internal/models/src/types.ts b/packages/internal/models/src/types.ts index d7c481b0d..d2516c482 100644 --- a/packages/internal/models/src/types.ts +++ b/packages/internal/models/src/types.ts @@ -21,6 +21,7 @@ export type ExtractBizResponse any> = Exclude< Awaited>, undefined > +export type ExtractHonoParams any> = Parameters[0]["json"] export type ActiveList = { id: string | number