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 <tukon479@gmail.com>
This commit is contained in:
Innei 2025-06-10 21:45:56 +08:00
parent e03d1ec872
commit d39f95c5b3
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
9 changed files with 178 additions and 78 deletions

View File

@ -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<typeof formSchema>) => {
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")),
})

View File

@ -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<HTMLDivElement | null>(null)
const selectoRef = useRef<Selecto>(null)
@ -254,7 +252,7 @@ const SubscriptionImpl = ({ ref, className, view }: SubscriptionProps) => {
isPreview
/>
)}
<SortByAlphabeticalList view={view} data={listsData} />
<SortByAlphabeticalList view={view} data={listSubIds} />
</>
)}
{hasInboxData && (
@ -262,7 +260,7 @@ const SubscriptionImpl = ({ ref, className, view }: SubscriptionProps) => {
<div className="text-text-secondary mt-1 flex h-6 w-full shrink-0 items-center rounded-md px-2.5 text-xs font-semibold transition-colors">
{t("words.inbox")}
</div>
<SortByAlphabeticalInbox view={view} data={inboxesData} />
<SortByAlphabeticalInbox view={view} data={inboxSubIds} />
</>
)}

View File

@ -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 =

View File

@ -96,7 +96,7 @@ export const SortByAlphabeticalFeedList = ({
export const SortByAlphabeticalListList = ({ view, data }: ListListProps) => {
return (
<div>
{Object.keys(data).map((listId) => (
{data.map((listId) => (
<ListItemAutoHideUnread key={listId} listId={listId} view={view} />
))}
</div>
@ -106,8 +106,8 @@ export const SortByAlphabeticalListList = ({ view, data }: ListListProps) => {
export const SortByAlphabeticalInboxList = ({ view, data }: ListListProps) => {
return (
<div>
{Object.keys(data).map((feedId) => (
<InboxItem key={feedId} inboxId={getInboxOrFeedIdFromFeedId(feedId)} view={view} />
{data.map((inboxId) => (
<InboxItem key={inboxId} inboxId={getInboxOrFeedIdFromFeedId(inboxId)} view={view} />
))}
</div>
)

View File

@ -9,5 +9,5 @@ export type SortBy = "count" | "alphabetical"
export type ListListProps = {
view: FeedViewType
data: Record<string, string[]>
data: string[]
}

View File

@ -34,6 +34,12 @@ class ServiceStatic extends BaseService<{ id: string }> implements Hydratable {
return this.table.bulkDelete(ids)
}
async findAndUpdate(id: string, data: Partial<ListModel>) {
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)

View File

@ -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<ListState>("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<InsertListModel, "listId">) {
const tx = createTransaction<never, FeedModel>()
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<ListModel> =>
useListStore.getState().lists[listId]
type InsertListModel = ExtractHonoParams<typeof apiClient.lists.$patch>

View File

@ -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<string, string[]>
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<string, string[]>
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])
}

View File

@ -21,6 +21,7 @@ export type ExtractBizResponse<T extends (...args: any[]) => any> = Exclude<
Awaited<ReturnType<T>>,
undefined
>
export type ExtractHonoParams<T extends (...args: any[]) => any> = Parameters<T>[0]["json"]
export type ActiveList = {
id: string | number