refactor: setting list data getter
Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
11ff813c45
commit
bf71094110
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
|||
</Button>
|
||||
<Divider className="mb-6 mt-8" />
|
||||
<div className="flex flex-1 flex-col">
|
||||
<ScrollArea.ScrollArea viewportClassName="max-h-[380px]">
|
||||
{listList.data?.length ? (
|
||||
<div className="overflow-auto">
|
||||
<Table className="mt-4">
|
||||
<TableHeader className="border-b">
|
||||
<TableRow className="[&_*]:!font-semibold">
|
||||
<TableHead size="sm">{t.settings("lists.title")}</TableHead>
|
||||
<TableHead size="sm">{t.settings("lists.view")}</TableHead>
|
||||
<TableHead size="sm">{t.settings("lists.fee.label")}</TableHead>
|
||||
<TableHead size="sm">{t.settings("lists.subscriptions")}</TableHead>
|
||||
<TableHead size="sm">{t.settings("lists.earnings")}</TableHead>
|
||||
<TableHead size="sm" className="center">
|
||||
{t.common("words.actions")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody className="border-t-[12px] border-transparent [&_td]:!px-3">
|
||||
{listList.data?.map((row) => (
|
||||
<TableRow key={row.title} className="h-8">
|
||||
<TableCell size="sm">
|
||||
<a
|
||||
target="_blank"
|
||||
href={UrlBuilder.shareList(row.id)}
|
||||
className="inline-flex items-center gap-2 font-semibold"
|
||||
>
|
||||
{row.image && (
|
||||
<Avatar className="size-6">
|
||||
<AvatarImage src={row.image} />
|
||||
</Avatar>
|
||||
)}
|
||||
<span className="inline-block max-w-[200px] truncate">{row.title}</span>
|
||||
</a>
|
||||
</TableCell>
|
||||
<TableCell size="sm">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
className={cn("inline-flex items-center", views[row.view].className)}
|
||||
>
|
||||
{views[row.view].icon}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>{t(views[row.view].name)}</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell size="sm">
|
||||
<div className="flex items-center gap-1">
|
||||
{row.fee}
|
||||
<i className="i-mgc-power shrink-0 text-lg text-accent" />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell size="sm">{row.subscriptionCount}</TableCell>
|
||||
<TableCell size="sm">
|
||||
<Balance>{BigInt(row.purchaseAmount || 0n)}</Balance>
|
||||
</TableCell>
|
||||
<TableCell size="sm" className="center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
present({
|
||||
title: t.settings("lists.feeds.manage"),
|
||||
content: () => <ListFeedsModalContent id={row.id} />,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-inbox-cute-re" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>{t.common("words.manage")}</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
present({
|
||||
title: t.settings("lists.edit.label"),
|
||||
content: () => <ListCreationModalContent id={row.id} />,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-edit-cute-re" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>{t.common("words.edit")}</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<ActionButton
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
present({
|
||||
title: t.settings("lists.delete.confirm"),
|
||||
content: ({ dismiss }) => (
|
||||
<ConfirmDestroyModalContent
|
||||
listId={row.id}
|
||||
onSuccess={() => {
|
||||
listList.refetch()
|
||||
dismiss()
|
||||
}}
|
||||
/>
|
||||
),
|
||||
})
|
||||
}
|
||||
>
|
||||
<i className="i-mgc-delete-2-cute-re" />
|
||||
</ActionButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>{t.common("words.delete")}</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
{isLoading && ownedLists.length === 0 && (
|
||||
<LoadingCircle size="large" className="center absolute inset-0" />
|
||||
)}
|
||||
|
||||
{isLoading && ownedLists.length > 0 && (
|
||||
<LoadingCircle size="small" className="center absolute right-0" />
|
||||
)}
|
||||
{!!ownedLists && (
|
||||
<ScrollArea.ScrollArea viewportClassName="max-h-[380px]">
|
||||
{ownedLists.length > 0 ? (
|
||||
<div className="overflow-auto">
|
||||
<Table className="mt-4">
|
||||
<TableHeader className="border-b">
|
||||
<TableRow className="[&_*]:!font-semibold">
|
||||
<TableHead size="sm">{t.settings("lists.title")}</TableHead>
|
||||
<TableHead size="sm">{t.settings("lists.view")}</TableHead>
|
||||
<TableHead size="sm">{t.settings("lists.fee.label")}</TableHead>
|
||||
<TableHead size="sm">{t.settings("lists.subscriptions")}</TableHead>
|
||||
<TableHead size="sm">{t.settings("lists.earnings")}</TableHead>
|
||||
<TableHead size="sm" className="center">
|
||||
{t.common("words.actions")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
) : listList.isLoading ? (
|
||||
<LoadingCircle size="large" className="center absolute inset-0" />
|
||||
) : (
|
||||
<div className="mt-36 w-full text-center text-sm text-zinc-400">
|
||||
<p>{t.settings("lists.noLists")}</p>
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea.ScrollArea>
|
||||
</TableHeader>
|
||||
<TableBody className="border-t-[12px] border-transparent [&_td]:!px-3">
|
||||
{ownedLists.map((row) => (
|
||||
<TableRow key={row.title} className="h-8">
|
||||
<TableCell size="sm">
|
||||
<a
|
||||
target="_blank"
|
||||
href={UrlBuilder.shareList(row.id)}
|
||||
className="inline-flex items-center gap-2 font-semibold"
|
||||
>
|
||||
{row.image && (
|
||||
<Avatar className="size-6">
|
||||
<AvatarImage src={row.image} />
|
||||
</Avatar>
|
||||
)}
|
||||
<span className="inline-block max-w-[200px] truncate">{row.title}</span>
|
||||
</a>
|
||||
</TableCell>
|
||||
<TableCell size="sm">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center",
|
||||
views[row.view].className,
|
||||
)}
|
||||
>
|
||||
{views[row.view].icon}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>{t(views[row.view].name)}</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell size="sm">
|
||||
<div className="flex items-center gap-1 tabular-nums">
|
||||
{row.fee}
|
||||
<i className="i-mgc-power shrink-0 text-lg text-accent" />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell size="sm" className="tabular-nums">
|
||||
{listDataMap[row.id]?.subscriptionCount}
|
||||
</TableCell>
|
||||
<TableCell size="sm" className="tabular-nums">
|
||||
<Balance>{BigInt(listDataMap[row.id]?.purchaseAmount || 0n)}</Balance>
|
||||
</TableCell>
|
||||
<TableCell size="sm" className="center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
present({
|
||||
title: t.settings("lists.feeds.manage"),
|
||||
content: () => <ListFeedsModalContent id={row.id} />,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-inbox-cute-re" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>{t.common("words.manage")}</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
present({
|
||||
title: t.settings("lists.edit.label"),
|
||||
content: () => <ListCreationModalContent id={row.id} />,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-edit-cute-re" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>{t.common("words.edit")}</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<ActionButton
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
present({
|
||||
title: t.settings("lists.delete.confirm"),
|
||||
content: () => <ConfirmDestroyModalContent listId={row.id} />,
|
||||
})
|
||||
}
|
||||
>
|
||||
<i className="i-mgc-delete-2-cute-re" />
|
||||
</ActionButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>{t.common("words.delete")}</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-36 w-full text-center text-sm text-zinc-400">
|
||||
<p>{t.settings("lists.noLists")}</p>
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea.ScrollArea>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ListState>("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) {
|
||||
|
|
|
|||
|
|
@ -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> = T
|
|||
export const toRaw = <T>(draft: MayBeDraft<T>): T => {
|
||||
return isDraft(draft) ? original(draft)! : draft
|
||||
}
|
||||
|
||||
const noop = (err: any) => {
|
||||
console.error(err)
|
||||
}
|
||||
export const createTransaction = <S>(snapshot: S) => {
|
||||
let onRollback: ((snapshot: S) => Promise<void>) | undefined
|
||||
let executorFn: (snapshot: S) => Promise<void> | undefined
|
||||
let optimisticExecutor: (snapshot: S) => Promise<void> | undefined
|
||||
let onPersist: ((snapshot: S) => Promise<void>) | undefined
|
||||
|
||||
const ret = {
|
||||
rollback: (fn: (snapshot: S) => Promise<void>) => {
|
||||
onRollback = fn
|
||||
return ret
|
||||
},
|
||||
execute: (executor: (snapshot: S) => Promise<void>) => {
|
||||
executorFn = executor
|
||||
return ret
|
||||
},
|
||||
optimistic: (executor: (snapshot: S) => Promise<void>) => {
|
||||
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<void>) => {
|
||||
onPersist = fn
|
||||
return ret
|
||||
},
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue