diff --git a/apps/renderer/src/hooks/biz/useFeedActions.tsx b/apps/renderer/src/hooks/biz/useFeedActions.tsx
index eb7740507..2d9ed11e7 100644
--- a/apps/renderer/src/hooks/biz/useFeedActions.tsx
+++ b/apps/renderer/src/hooks/biz/useFeedActions.tsx
@@ -23,7 +23,7 @@ import {
useRemoveFeedFromFeedList,
} from "~/store/feed"
import { useInboxById } from "~/store/inbox"
-import { useListById, useOwnedList } from "~/store/list"
+import { useListById, useOwnedListByView } from "~/store/list"
import { subscriptionActions, useSubscriptionByFeedId } from "~/store/subscription"
import { useNavigateEntry } from "./useNavigateEntry"
@@ -85,7 +85,7 @@ export const useFeedActions = ({
const { mutateAsync: removeFeedFromListMutation } = useRemoveFeedFromFeedList()
const openBoostModal = useBoostModal()
- const listByView = useOwnedList(view!)
+ const listByView = useOwnedListByView(view!)
const isMultipleSelection = feedIds && feedIds.length > 0
diff --git a/apps/renderer/src/modules/feed-column/category.tsx b/apps/renderer/src/modules/feed-column/category.tsx
index 7ab9fe82a..6414f999b 100644
--- a/apps/renderer/src/modules/feed-column/category.tsx
+++ b/apps/renderer/src/modules/feed-column/category.tsx
@@ -19,7 +19,7 @@ import { ROUTE_FEED_IN_FOLDER } from "~/constants"
import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry"
import { getRouteParams, useRouteParamsSelector } from "~/hooks/biz/useRouteParams"
import { getPreferredTitle, useAddFeedToFeedList, useFeedStore } from "~/store/feed"
-import { useOwnedList } from "~/store/list"
+import { useOwnedListByView } from "~/store/list"
import {
subscriptionActions,
subscriptionCategoryExist,
@@ -148,7 +148,7 @@ function FeedCategoryImpl({ data: ids, view, categoryOpenStateData }: FeedCatego
const addMutation = useAddFeedToFeedList()
- const listList = useOwnedList(view!)
+ const listList = useOwnedListByView(view!)
const showContextMenu = useShowContextMenu()
return (
diff --git a/apps/renderer/src/modules/settings/tabs/lists/index.tsx b/apps/renderer/src/modules/settings/tabs/lists/index.tsx
index 289f0cd57..f3bc7f373 100644
--- a/apps/renderer/src/modules/settings/tabs/lists/index.tsx
+++ b/apps/renderer/src/modules/settings/tabs/lists/index.tsx
@@ -21,43 +21,33 @@ import { views } from "@follow/constants"
import { UrlBuilder } from "@follow/utils/url-builder"
import { cn } from "@follow/utils/utils"
import { useMutation } from "@tanstack/react-query"
+import { useMemo } from "react"
import { toast } from "sonner"
-import { useModalStack } from "~/components/ui/modal/stacked/hooks"
+import { useCurrentModal, useModalStack } from "~/components/ui/modal/stacked/hooks"
import { useAuthQuery, useI18n } from "~/hooks/common"
-import { apiClient } from "~/lib/api-fetch"
import { Balance } from "~/modules/wallet/balance"
import { Queries } from "~/queries"
-import { listActions } from "~/store/list"
+import { listActions, useOwnedLists } from "~/store/list"
import { ListCreationModalContent, ListFeedsModalContent } from "./modals"
-const ConfirmDestroyModalContent = ({
- listId,
- onSuccess,
-}: {
- listId: string
- onSuccess: () => void
-}) => {
+const ConfirmDestroyModalContent = ({ listId }: { listId: string }) => {
const t = useI18n()
+ const currentModal = useCurrentModal()
const deleteFeedList = useMutation({
- mutationFn: async (payload: { listId: string }) => {
- listActions.deleteList(payload.listId)
- await apiClient.lists.$delete({
- json: {
- listId: payload.listId,
- },
- })
- },
+ mutationFn: (payload: { listId: string }) => listActions.deleteList(payload.listId),
onSuccess: () => {
toast.success(t.settings("lists.delete.success"))
Queries.lists.list().invalidate()
- onSuccess()
},
- async onError() {
+ onError() {
toast.error(t.settings("lists.delete.error"))
},
+ onMutate() {
+ currentModal?.dismiss()
+ },
})
return (
@@ -77,7 +67,26 @@ const ConfirmDestroyModalContent = ({
export const SettingLists = () => {
const t = useI18n()
- const listList = useAuthQuery(Queries.lists.list())
+ const { isLoading, data } = useAuthQuery(Queries.lists.list())
+ const listDataMap = useMemo(() => {
+ if (!data) return {}
+ return data?.reduce(
+ (acc, curr) => {
+ acc[curr.id] = {
+ id: curr.id,
+ subscriptionCount: curr.subscriptionCount,
+ purchaseAmount: curr.purchaseAmount,
+ }
+ return acc
+ },
+ {} as Record<
+ string,
+ { id: string; subscriptionCount: number | undefined; purchaseAmount: number | undefined }
+ >,
+ )
+ }, [data])
+
+ const ownedLists = useOwnedLists()
const { present } = useModalStack()
@@ -99,140 +108,144 @@ export const SettingLists = () => {
-
- {listList.data?.length ? (
-
-
-
-
- {t.settings("lists.title")}
- {t.settings("lists.view")}
- {t.settings("lists.fee.label")}
- {t.settings("lists.subscriptions")}
- {t.settings("lists.earnings")}
-
- {t.common("words.actions")}
-
-
-
-
- {listList.data?.map((row) => (
-
-
-
- {row.image && (
-
-
-
- )}
- {row.title}
-
-
-
-
-
-
- {views[row.view].icon}
-
-
-
- {t(views[row.view].name)}
-
-
-
-
-
- {row.fee}
-
-
-
- {row.subscriptionCount}
-
- {BigInt(row.purchaseAmount || 0n)}
-
-
-
-
-
-
-
- {t.common("words.manage")}
-
-
-
-
-
-
-
- {t.common("words.edit")}
-
-
-
-
-
- present({
- title: t.settings("lists.delete.confirm"),
- content: ({ dismiss }) => (
- {
- listList.refetch()
- dismiss()
- }}
- />
- ),
- })
- }
- >
-
-
-
-
- {t.common("words.delete")}
-
-
-
+ {isLoading && ownedLists.length === 0 && (
+
+ )}
+
+ {isLoading && ownedLists.length > 0 && (
+
+ )}
+ {!!ownedLists && (
+
+ {ownedLists.length > 0 ? (
+
+
+
+
+ {t.settings("lists.title")}
+ {t.settings("lists.view")}
+ {t.settings("lists.fee.label")}
+ {t.settings("lists.subscriptions")}
+ {t.settings("lists.earnings")}
+
+ {t.common("words.actions")}
+
- ))}
-
-
-
- ) : listList.isLoading ? (
-
- ) : (
-
-
{t.settings("lists.noLists")}
-
- )}
-
+
+
+ {ownedLists.map((row) => (
+
+
+
+ {row.image && (
+
+
+
+ )}
+ {row.title}
+
+
+
+
+
+
+ {views[row.view].icon}
+
+
+
+ {t(views[row.view].name)}
+
+
+
+
+
+ {row.fee}
+
+
+
+
+ {listDataMap[row.id]?.subscriptionCount}
+
+
+ {BigInt(listDataMap[row.id]?.purchaseAmount || 0n)}
+
+
+
+
+
+
+
+ {t.common("words.manage")}
+
+
+
+
+
+
+
+ {t.common("words.edit")}
+
+
+
+
+
+ present({
+ title: t.settings("lists.delete.confirm"),
+ content: () => ,
+ })
+ }
+ >
+
+
+
+
+ {t.common("words.delete")}
+
+
+
+
+ ))}
+
+
+
+ ) : (
+
+
{t.settings("lists.noLists")}
+
+ )}
+
+ )}
)
diff --git a/apps/renderer/src/store/list/hooks.ts b/apps/renderer/src/store/list/hooks.ts
index 2bd892189..e95c9739c 100644
--- a/apps/renderer/src/store/list/hooks.ts
+++ b/apps/renderer/src/store/list/hooks.ts
@@ -13,7 +13,7 @@ export const useListByView = (view: FeedViewType) => {
return useListStore((state) => Object.values(state.lists).filter((list) => list.view === view))
}
-export const useOwnedList = (view: FeedViewType) => {
+export const useOwnedListByView = (view: FeedViewType) => {
const whoami = useWhoami()
const viewLists = useListByView(view)
return useMemo(
@@ -21,3 +21,10 @@ export const useOwnedList = (view: FeedViewType) => {
[viewLists, whoami],
)
}
+
+export const useOwnedLists = () => {
+ const whoami = useWhoami()
+ return useListStore((state) =>
+ Object.values(state.lists).filter((list) => list.ownerUserId === whoami?.id),
+ )
+}
diff --git a/apps/renderer/src/store/list/store.ts b/apps/renderer/src/store/list/store.ts
index 2cd485dfb..9164d1a46 100644
--- a/apps/renderer/src/store/list/store.ts
+++ b/apps/renderer/src/store/list/store.ts
@@ -1,11 +1,12 @@
import type { FeedModel, ListModel, ListModelPoplutedFeeds } from "@follow/models/types"
+import { sleep } from "@follow/utils/utils"
import { runTransactionInScope } from "~/database"
import { apiClient } from "~/lib/api-fetch"
import { ListService } from "~/services/list"
import { feedActions } from "../feed"
-import { createImmerSetter, createZustandStore } from "../utils/helper"
+import { createImmerSetter, createTransaction, createZustandStore } from "../utils/helper"
import type { ListState } from "./types"
export const useListStore = createZustandStore("list")(() => ({
@@ -78,13 +79,37 @@ class ListActionStatic {
})
}
- deleteList(listId: string) {
- immerSet((state) => {
- delete state.lists[listId]
- return state
+ async deleteList(listId: string) {
+ const deletedList = get().lists[listId]
+ if (!deletedList) return
+ const tx = createTransaction(deletedList)
+
+ tx.optimistic(async () => {
+ immerSet((state) => {
+ delete state.lists[listId]
+ return state
+ })
+ })
+ tx.execute(async () => {
+ await sleep(1000)
+
+ await apiClient.lists.$delete({
+ json: {
+ listId,
+ },
+ })
+ })
+ tx.rollback(async (s) => {
+ immerSet((state) => {
+ state.lists[listId] = s
+ return state
+ })
})
- runTransactionInScope(() => ListService.bulkDelete([listId]))
+ tx.onPersist(async () => {
+ ListService.bulkDelete([listId])
+ })
+ await tx.run()
}
async fetchListById(id: string) {
diff --git a/apps/renderer/src/store/utils/helper.ts b/apps/renderer/src/store/utils/helper.ts
index a3b4b49aa..9e263d4fb 100644
--- a/apps/renderer/src/store/utils/helper.ts
+++ b/apps/renderer/src/store/utils/helper.ts
@@ -1,4 +1,5 @@
/* eslint-disable no-unsafe-finally */
+
import { isDraft, original, produce } from "immer"
import { unstable_batchedUpdates } from "react-dom"
import type { StateCreator, StoreApi, UseBoundStore } from "zustand"
@@ -149,3 +150,43 @@ type MayBeDraft = T
export const toRaw = (draft: MayBeDraft): T => {
return isDraft(draft) ? original(draft)! : draft
}
+
+const noop = (err: any) => {
+ console.error(err)
+}
+export const createTransaction = (snapshot: S) => {
+ let onRollback: ((snapshot: S) => Promise) | undefined
+ let executorFn: (snapshot: S) => Promise | undefined
+ let optimisticExecutor: (snapshot: S) => Promise | undefined
+ let onPersist: ((snapshot: S) => Promise) | undefined
+
+ const ret = {
+ rollback: (fn: (snapshot: S) => Promise) => {
+ onRollback = fn
+ return ret
+ },
+ execute: (executor: (snapshot: S) => Promise) => {
+ executorFn = executor
+ return ret
+ },
+ optimistic: (executor: (snapshot: S) => Promise) => {
+ optimisticExecutor = executor
+ return ret
+ },
+ run: async () => {
+ await optimisticExecutor?.(snapshot)?.catch(noop)
+ await executorFn?.(snapshot)?.catch((err) => {
+ if (onRollback) {
+ onRollback(snapshot)
+ }
+ throw err
+ })
+ await runTransactionInScope(() => onPersist?.(snapshot))
+ },
+ onPersist: (fn: (snapshot: S) => Promise) => {
+ onPersist = fn
+ return ret
+ },
+ }
+ return ret
+}