fix: list data store and list edit form
Signed-off-by: Innei <i@innei.in>
This commit is contained in:
parent
9fe48c5713
commit
f420e78ca9
|
|
@ -4,14 +4,14 @@ import { useWhoami } from "~/atoms/user"
|
|||
import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar"
|
||||
import { Tooltip, TooltipContent, TooltipPortal, TooltipTrigger } from "~/components/ui/tooltip"
|
||||
import { cn } from "~/lib/utils"
|
||||
import type { TargetModel } from "~/models"
|
||||
import type { FeedOrListRespModel } from "~/models"
|
||||
import { usePresentUserProfileModal } from "~/modules/profile/hooks"
|
||||
|
||||
export const FeedCertification = ({
|
||||
feed,
|
||||
className,
|
||||
}: {
|
||||
feed: TargetModel
|
||||
feed: FeedOrListRespModel
|
||||
className?: string
|
||||
}) => {
|
||||
const me = useWhoami()
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { forwardRef, useMemo } from "react"
|
|||
import { getColorScheme, stringToHue } from "~/lib/color"
|
||||
import { getImageProxyUrl } from "~/lib/img-proxy"
|
||||
import { cn, getUrlIcon } from "~/lib/utils"
|
||||
import type { CombinedEntryModel, FeedModel, TargetModel } from "~/models"
|
||||
import type { CombinedEntryModel, FeedModel, FeedOrListRespModel } from "~/models"
|
||||
|
||||
import { PlatformIcon } from "./ui/platform-icon"
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ export function FeedIcon({
|
|||
siteUrl,
|
||||
useMedia,
|
||||
}: {
|
||||
feed?: TargetModel
|
||||
feed?: FeedOrListRespModel
|
||||
entry?: CombinedEntryModel["entries"]
|
||||
fallbackUrl?: string
|
||||
className?: string
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { WEB_URL } from "@follow/shared/constants"
|
|||
|
||||
import { FeedIcon } from "~/components/feed-icon"
|
||||
import { cn } from "~/lib/utils"
|
||||
import type { TargetModel } from "~/models"
|
||||
import type { FeedOrListRespModel } from "~/models"
|
||||
|
||||
import { FeedCertification } from "./feed-certification"
|
||||
import { EllipsisHorizontalTextWithTooltip } from "./ui/typography"
|
||||
|
|
@ -12,7 +12,7 @@ export function FollowSummary({
|
|||
docs,
|
||||
className,
|
||||
}: {
|
||||
feed: TargetModel
|
||||
feed: FeedOrListRespModel
|
||||
docs?: string
|
||||
className?: string
|
||||
}) {
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export const RelativeTime: FC<{
|
|||
{/* https://github.com/radix-ui/primitives/issues/2248#issuecomment-2147056904 */}
|
||||
<TooltipTrigger onFocusCapture={stopPropagation}>
|
||||
{relative}
|
||||
{t("words.space")}
|
||||
{t("space")}
|
||||
{t("words.ago")}
|
||||
</TooltipTrigger>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import type { TargetModel } from "~/models"
|
||||
import type { FeedOrListRespModel } from "~/models"
|
||||
|
||||
export type DB_FeedUnread = {
|
||||
id: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export type DB_Feed = TargetModel & { id: string }
|
||||
export type DB_Feed = FeedOrListRespModel & { id: string }
|
||||
|
|
|
|||
|
|
@ -25,9 +25,10 @@ export type TransactionModel = ExtractBizResponse<
|
|||
|
||||
export type FeedModel = ExtractBizResponse<typeof apiClient.feeds.$get>["data"]["feed"]
|
||||
|
||||
export type ListModel = ExtractBizResponse<typeof apiClient.lists.$get>["data"]["list"]
|
||||
|
||||
export type TargetModel = FeedModel | ListModel
|
||||
type ListModelPoplutedFeeds = ExtractBizResponse<typeof apiClient.lists.$get>["data"]["list"]
|
||||
export type ListModel = Omit<ListModelPoplutedFeeds, "feeds">
|
||||
export type FeedOrListRespModel = FeedModel | ListModelPoplutedFeeds
|
||||
export type FeedOrListModel = FeedModel | ListModel
|
||||
|
||||
export type EntryResponse = Exclude<
|
||||
Extract<ExtractBizResponse<typeof apiClient.entries.$get>, { code: 0 }>["data"],
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ export const EntryItemWrapper: FC<
|
|||
},
|
||||
{
|
||||
type: "text" as const,
|
||||
label: `${t("words.copy")}${t("words.space")}${t("words.entry")} ${t("words.id")}`,
|
||||
label: `${t("words.copy")}${t("space")}${t("words.entry")} ${t("words.id")}`,
|
||||
click: () => {
|
||||
navigator.clipboard.writeText(entry.entries.id)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export const EntryListHeader: FC<{
|
|||
<div className="text-xs font-medium text-zinc-400">
|
||||
{totalCount || 0} {t("quantifier.piece", { ns: "common" })}
|
||||
{unreadOnly && !isInCollectionList ? t("words.unread") : ""}
|
||||
{t("words.space", { ns: "common" })}
|
||||
{t("space", { ns: "common" })}
|
||||
{t("words.items", { ns: "common", count: totalCount })}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import type { FC } from "react"
|
||||
|
||||
import type { CombinedEntryModel, TargetModel } from "~/models"
|
||||
import type { CombinedEntryModel, FeedOrListRespModel } from "~/models"
|
||||
|
||||
export type UniversalItemProps = {
|
||||
entryId: string
|
||||
entryPreview?: CombinedEntryModel & {
|
||||
feeds: TargetModel
|
||||
feeds: FeedOrListRespModel
|
||||
feedId: string
|
||||
}
|
||||
translation?: {
|
||||
|
|
|
|||
|
|
@ -318,7 +318,7 @@ const SearchResultCount: FC<{
|
|||
<>
|
||||
{count} {t.common("quantifier.piece")}
|
||||
{t.common("words.local")}
|
||||
{t.common("words.space")}
|
||||
{t.common("space")}
|
||||
{t.common("words.record", { count })}
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -40,17 +40,20 @@ import { views } from "~/constants"
|
|||
import { useAuthQuery, useI18n } from "~/hooks/common"
|
||||
import { apiClient } from "~/lib/api-fetch"
|
||||
import { cn, isBizId } from "~/lib/utils"
|
||||
import type { ListModel } from "~/models"
|
||||
import type { FeedModel, ListModel } from "~/models"
|
||||
import { ViewSelectorRadioGroup } from "~/modules/shared/ViewSelectorRadioGroup"
|
||||
import { Balance } from "~/modules/wallet/balance"
|
||||
import { Queries } from "~/queries"
|
||||
import { feedActions, getFeedById, useFeedById } from "~/store/feed"
|
||||
import {
|
||||
getFeedById,
|
||||
useAddFeedToFeedList,
|
||||
useFeedById,
|
||||
useRemoveFeedFromFeedList,
|
||||
} from "~/store/feed"
|
||||
import { useSubscriptionStore } from "~/store/subscription"
|
||||
|
||||
export const SettingLists = () => {
|
||||
// const { t: appT } = useTranslation()
|
||||
const t = useI18n()
|
||||
// const { t } = useTranslation("settings")
|
||||
const listList = useAuthQuery(Queries.lists.list())
|
||||
|
||||
const { present } = useModalStack()
|
||||
|
|
@ -342,42 +345,7 @@ export const ListFeedsModalContent = ({ id }: { id: string }) => {
|
|||
const { t } = useTranslation("settings")
|
||||
|
||||
const [feedSearchFor, setFeedSearchFor] = useState("")
|
||||
const addMutation = useMutation({
|
||||
mutationFn: async (values: string) => {
|
||||
const feed = await apiClient.lists.feeds.$post({
|
||||
json: {
|
||||
listId: id,
|
||||
feedId: values,
|
||||
},
|
||||
})
|
||||
feedActions.upsertMany([feed.data])
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success(t("lists.feeds.add.success"))
|
||||
Queries.lists.list().invalidate()
|
||||
},
|
||||
async onError() {
|
||||
toast.error(t("lists.feeds.add.error"))
|
||||
},
|
||||
})
|
||||
|
||||
const removeMutation = useMutation({
|
||||
mutationFn: async (feedId: string) => {
|
||||
await apiClient.lists.feeds.$delete({
|
||||
json: {
|
||||
listId: id,
|
||||
feedId,
|
||||
},
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success(t("lists.feeds.delete.success"))
|
||||
Queries.lists.list().invalidate()
|
||||
},
|
||||
async onError() {
|
||||
toast.error(t("lists.feeds.delete.error"))
|
||||
},
|
||||
})
|
||||
const addMutation = useAddFeedToFeedList()
|
||||
|
||||
const allFeeds = useSubscriptionStore((store) => {
|
||||
const feedInfo = [] as { title: string; id: string }[]
|
||||
|
|
@ -395,11 +363,13 @@ export const ListFeedsModalContent = ({ id }: { id: string }) => {
|
|||
})
|
||||
|
||||
const autocompleteSuggestions: Suggestion[] = useMemo(() => {
|
||||
return allFeeds.map((feed) => ({
|
||||
name: feed.title,
|
||||
value: feed.id,
|
||||
}))
|
||||
}, [allFeeds])
|
||||
return allFeeds
|
||||
.filter((feed) => !list.feedIds?.includes(feed.id))
|
||||
.map((feed) => ({
|
||||
name: feed.title,
|
||||
value: feed.id,
|
||||
}))
|
||||
}, [allFeeds, list.feedIds])
|
||||
|
||||
const selectedFeedIdRef = useRef<string | null>()
|
||||
return (
|
||||
|
|
@ -422,11 +392,11 @@ export const ListFeedsModalContent = ({ id }: { id: string }) => {
|
|||
className="whitespace-nowrap"
|
||||
onClick={() => {
|
||||
if (isBizId(feedSearchFor)) {
|
||||
addMutation.mutate(feedSearchFor)
|
||||
addMutation.mutate({ feedId: feedSearchFor, listId: id })
|
||||
return
|
||||
}
|
||||
if (selectedFeedIdRef.current) {
|
||||
addMutation.mutate(selectedFeedIdRef.current)
|
||||
addMutation.mutate({ feedId: selectedFeedIdRef.current, listId: id })
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
@ -438,9 +408,6 @@ export const ListFeedsModalContent = ({ id }: { id: string }) => {
|
|||
<Table className="mt-4">
|
||||
<TableHeader className="border-b">
|
||||
<TableRow className="[&_*]:!font-semibold">
|
||||
{/* <TableHead className="w-20 text-center" size="sm">
|
||||
{t("lists.feeds.id")}
|
||||
</TableHead> */}
|
||||
<TableHead size="sm" className="pl-8">
|
||||
{t("lists.feeds.title")}
|
||||
</TableHead>
|
||||
|
|
@ -453,36 +420,41 @@ export const ListFeedsModalContent = ({ id }: { id: string }) => {
|
|||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody className="border-t-[12px] border-transparent">
|
||||
{list.feeds?.map((row) => (
|
||||
<TableRow key={row.title} className="h-8">
|
||||
{/* <TableCell align="center" size="sm">
|
||||
{row.id}
|
||||
</TableCell> */}
|
||||
<TableCell size="sm">
|
||||
<a
|
||||
target="_blank"
|
||||
href={`${WEB_URL}/list/${row.id}`}
|
||||
className="flex items-center gap-2 font-semibold"
|
||||
>
|
||||
{row.siteUrl && <FeedIcon className="mr-0" siteUrl={row.siteUrl} />}
|
||||
<span className="inline-block max-w-[200px] truncate">{row.title}</span>
|
||||
</a>
|
||||
</TableCell>
|
||||
<TableCell align="center" size="sm">
|
||||
<div className="center">
|
||||
<FeedCertification className="ml-0" feed={row} />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell align="center" size="sm">
|
||||
<Button variant="ghost" onClick={() => removeMutation.mutate(row.id)}>
|
||||
<i className="i-mgc-delete-2-cute-re" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{list.feedIds?.map((feedId) => <RowRender feedId={feedId} key={feedId} listId={id} />)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ScrollArea.ScrollArea>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const RowRender = ({ feedId, listId }: { feedId: string; listId: string }) => {
|
||||
const feed = useFeedById(feedId) as FeedModel
|
||||
|
||||
const removeMutation = useRemoveFeedFromFeedList()
|
||||
if (!feed) return null
|
||||
return (
|
||||
<TableRow key={feed.title} className="h-8">
|
||||
<TableCell size="sm">
|
||||
<a
|
||||
target="_blank"
|
||||
href={`${WEB_URL}/list/${feed.id}`}
|
||||
className="flex items-center gap-2 font-semibold"
|
||||
>
|
||||
{feed.siteUrl && <FeedIcon className="mr-0" siteUrl={feed.siteUrl} />}
|
||||
<span className="inline-block max-w-[200px] truncate">{feed.title}</span>
|
||||
</a>
|
||||
</TableCell>
|
||||
<TableCell align="center" size="sm">
|
||||
<div className="center">
|
||||
<FeedCertification className="ml-0" feed={feed} />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell align="center" size="sm">
|
||||
<Button variant="ghost" onClick={() => removeMutation.mutate({ feedId: feed.id, listId })}>
|
||||
<i className="i-mgc-delete-2-cute-re" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { usePresentFeedFormModal } from "~/hooks/biz/useFeedFormModal"
|
|||
import { useTitle } from "~/hooks/common"
|
||||
import type { ListModel } from "~/models"
|
||||
import { useFeed } from "~/queries/feed"
|
||||
import { useFeedById } from "~/store/feed/hooks"
|
||||
|
||||
export function Component() {
|
||||
const { id } = useParams()
|
||||
|
|
@ -51,7 +52,7 @@ export function Component() {
|
|||
subscriptionCount: feed.data.subscriptionCount,
|
||||
subscriptionNoun: t("feed.follower", { count: feed.data.subscriptionCount }),
|
||||
feedsCount: "feedCount" in feed.data ? feed.data.feedCount : 0,
|
||||
feedsNoun: t("feed.feeds", { count: listData?.feeds?.length }),
|
||||
feedsNoun: t("feed.feeds", { count: listData?.feedIds?.length }),
|
||||
appName: APP_NAME,
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -78,23 +79,9 @@ export function Component() {
|
|||
</Button>
|
||||
</span>
|
||||
<div className="flex w-full max-w-3xl flex-col gap-4 pb-12 pt-8">
|
||||
{listData.feeds?.slice(0, 5).map((feed) => (
|
||||
<a
|
||||
className="relative flex cursor-pointer items-center text-base"
|
||||
href={`/feed/${feed.id}`}
|
||||
target="_blank"
|
||||
key={feed.id}
|
||||
>
|
||||
<FeedIcon
|
||||
fallback
|
||||
feed={feed}
|
||||
className="mask-squircle mask mr-2 shrink-0"
|
||||
size={20}
|
||||
/>
|
||||
{feed.title}
|
||||
<FeedCertification feed={feed} />
|
||||
</a>
|
||||
))}
|
||||
{listData.feedIds
|
||||
?.slice(0, 5)
|
||||
.map((feedId) => <FeedRow feedId={feedId} key={feedId} />)}
|
||||
{"feedCount" in feed.data && (
|
||||
<div
|
||||
onClick={() => {
|
||||
|
|
@ -117,3 +104,20 @@ export function Component() {
|
|||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const FeedRow = ({ feedId }: { feedId: string }) => {
|
||||
const feed = useFeedById(feedId)
|
||||
if (!feed) return null
|
||||
return (
|
||||
<a
|
||||
className="relative flex cursor-pointer items-center text-base"
|
||||
href={`/feed/${feed.id}`}
|
||||
target="_blank"
|
||||
key={feed.id}
|
||||
>
|
||||
<FeedIcon fallback feed={feed} className="mask-squircle mask mr-2 shrink-0" size={20} />
|
||||
{feed.title}
|
||||
<FeedCertification feed={feed} />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,25 @@
|
|||
|
||||
import { browserDB } from "~/database"
|
||||
import type { TargetModel } from "~/models/types"
|
||||
import type { FeedOrListModel } from "~/models/types"
|
||||
|
||||
import { BaseService } from "./base"
|
||||
import { CleanerService } from "./cleaner"
|
||||
|
||||
type TargetModelWithId = TargetModel & { id: string }
|
||||
type TargetModelWithId = FeedOrListModel & { id: string }
|
||||
class ServiceStatic extends BaseService<TargetModelWithId> {
|
||||
constructor() {
|
||||
super(browserDB.feeds)
|
||||
}
|
||||
|
||||
override async upsertMany(data: TargetModel[]) {
|
||||
override async upsertMany(data: FeedOrListModel[]) {
|
||||
const filterData = data.filter((d) => d.id)
|
||||
|
||||
CleanerService.reset(filterData.map((d) => ({ type: "feed", id: d.id! })))
|
||||
|
||||
return this.table.bulkPut(filterData as TargetModelWithId[])
|
||||
}
|
||||
|
||||
override async upsert(data: TargetModel): Promise<string | null> {
|
||||
override async upsert(data: FeedOrListModel): Promise<string | null> {
|
||||
if (!data.id) return null
|
||||
CleanerService.reset([{ type: "feed", id: data.id }])
|
||||
return this.table.put(data as TargetModelWithId)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { isNil, merge, omit } from "lodash-es"
|
|||
import { runTransactionInScope } from "~/database"
|
||||
import { apiClient } from "~/lib/api-fetch"
|
||||
import { getEntriesParams, omitObjectUndefinedValue } from "~/lib/utils"
|
||||
import type { CombinedEntryModel, EntryModel, TargetModel } from "~/models"
|
||||
import type { CombinedEntryModel, EntryModel, FeedOrListRespModel } from "~/models"
|
||||
import { EntryService } from "~/services"
|
||||
|
||||
import { feedActions } from "../feed"
|
||||
|
|
@ -178,7 +178,7 @@ class EntryActions {
|
|||
}
|
||||
|
||||
upsertMany(data: CombinedEntryModel[]) {
|
||||
const feeds = [] as TargetModel[]
|
||||
const feeds = [] as FeedOrListRespModel[]
|
||||
const entries = [] as EntryModel[]
|
||||
const entry2Read = {} as Record<string, boolean>
|
||||
const entryFeedMap = {} as Record<string, string>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
import { useMutation } from "@tanstack/react-query"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
import { useShallow } from "zustand/react/shallow"
|
||||
|
||||
import { FEED_COLLECTION_LIST, ROUTE_FEED_IN_FOLDER, ROUTE_FEED_PENDING, views } from "~/constants"
|
||||
import { useRouteParams } from "~/hooks/biz/useRouteParams"
|
||||
import type { TargetModel } from "~/models"
|
||||
import { apiClient } from "~/lib/api-fetch"
|
||||
import type { FeedOrListRespModel } from "~/models"
|
||||
|
||||
import { getSubscriptionByFeedId } from "../subscription"
|
||||
import { useFeedStore } from "./store"
|
||||
import { feedActions, useFeedStore } from "./store"
|
||||
import type { FeedQueryParams } from "./types"
|
||||
|
||||
export const useFeedById = (feedId: Nullable<string>): TargetModel | null =>
|
||||
export const useFeedById = (feedId: Nullable<string>): FeedOrListRespModel | null =>
|
||||
useFeedStore((state) => (feedId ? state.feeds[feedId] : null))
|
||||
|
||||
export const useFeedByIdOrUrl = (feed: FeedQueryParams) =>
|
||||
|
|
@ -26,7 +29,7 @@ export const useFeedByIdOrUrl = (feed: FeedQueryParams) =>
|
|||
|
||||
export const useFeedByIdSelector = <T>(
|
||||
feedId: Nullable<string>,
|
||||
selector: (feed: TargetModel) => T,
|
||||
selector: (feed: FeedOrListRespModel) => T,
|
||||
) =>
|
||||
useFeedStore(
|
||||
useShallow((state) => (feedId && state.feeds[feedId] ? selector(state.feeds[feedId]) : null)),
|
||||
|
|
@ -45,7 +48,6 @@ export const useFeedHeaderTitle = () => {
|
|||
|
||||
switch (currentFeedId) {
|
||||
case ROUTE_FEED_PENDING: {
|
||||
// TODO: fix this type error
|
||||
return t(views[view].name)
|
||||
}
|
||||
case FEED_COLLECTION_LIST: {
|
||||
|
|
@ -59,3 +61,54 @@ export const useFeedHeaderTitle = () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const useAddFeedToFeedList = (options?: { onSuccess: () => void; onError: () => void }) => {
|
||||
const { t } = useTranslation("settings")
|
||||
return useMutation({
|
||||
mutationFn: async (payload: { feedId: string; listId: string }) => {
|
||||
const feed = await apiClient.lists.feeds.$post({
|
||||
json: {
|
||||
listId: payload.listId,
|
||||
feedId: payload.feedId,
|
||||
},
|
||||
})
|
||||
|
||||
feedActions.addFeedToFeedList(payload.listId, feed.data)
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success(t("lists.feeds.add.success"))
|
||||
|
||||
options?.onSuccess?.()
|
||||
},
|
||||
async onError() {
|
||||
toast.error(t("lists.feeds.add.error"))
|
||||
options?.onError?.()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useRemoveFeedFromFeedList = (options?: {
|
||||
onSuccess: () => void
|
||||
onError: () => void
|
||||
}) => {
|
||||
const { t } = useTranslation("settings")
|
||||
return useMutation({
|
||||
mutationFn: async (payload: { feedId: string; listId: string }) => {
|
||||
feedActions.removeFeedFromFeedList(payload.listId, payload.feedId)
|
||||
await apiClient.lists.feeds.$delete({
|
||||
json: {
|
||||
listId: payload.listId,
|
||||
feedId: payload.feedId,
|
||||
},
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success(t("lists.feeds.delete.success"))
|
||||
options?.onSuccess?.()
|
||||
},
|
||||
async onError() {
|
||||
toast.error(t("lists.feeds.delete.error"))
|
||||
options?.onError?.()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
import { produce } from "immer"
|
||||
import { omit } from "lodash-es"
|
||||
import { nanoid } from "nanoid"
|
||||
|
||||
import { whoami } from "~/atoms/user"
|
||||
import { runTransactionInScope } from "~/database"
|
||||
import { apiClient } from "~/lib/api-fetch"
|
||||
import type { TargetModel, UserModel } from "~/models"
|
||||
import type {
|
||||
FeedModel,
|
||||
FeedOrListModel,
|
||||
FeedOrListRespModel,
|
||||
ListModel,
|
||||
UserModel,
|
||||
} from "~/models"
|
||||
import { FeedService } from "~/services"
|
||||
|
||||
import { getSubscriptionByFeedId } from "../subscription"
|
||||
|
|
@ -23,9 +30,16 @@ class FeedActions {
|
|||
set({ feeds: {} })
|
||||
}
|
||||
|
||||
upsertMany(feeds: TargetModel[]) {
|
||||
upsertMany(feeds: FeedOrListRespModel[]) {
|
||||
runTransactionInScope(() => {
|
||||
FeedService.upsertMany(feeds)
|
||||
FeedService.upsertMany(
|
||||
feeds.map((feed) => {
|
||||
if (feed.type === "list") {
|
||||
return omit(feed, ["feeds"])
|
||||
}
|
||||
return feed
|
||||
}),
|
||||
)
|
||||
})
|
||||
set((state) =>
|
||||
produce(state, (state) => {
|
||||
|
|
@ -53,7 +67,7 @@ class FeedActions {
|
|||
}
|
||||
})
|
||||
|
||||
state.feeds[feed.id] = feed
|
||||
state.feeds[feed.id] = omit(feed, "feeds") as FeedOrListModel
|
||||
} else {
|
||||
// Store temp feed in memory
|
||||
const nonce = feed["nonce"] || nanoid(8)
|
||||
|
|
@ -67,7 +81,7 @@ class FeedActions {
|
|||
)
|
||||
}
|
||||
|
||||
private patch(feedId: string, patch: Partial<TargetModel>) {
|
||||
private patch(feedId: string, patch: Partial<FeedOrListRespModel>) {
|
||||
set((state) =>
|
||||
produce(state, (state) => {
|
||||
const feed = state.feeds[feedId]
|
||||
|
|
@ -100,6 +114,27 @@ class FeedActions {
|
|||
})
|
||||
}
|
||||
|
||||
async addFeedToFeedList(listId: string, feed: FeedModel) {
|
||||
const list = get().feeds[listId] as ListModel
|
||||
if (!list) return
|
||||
feedActions.upsertMany([feed])
|
||||
|
||||
this.patch(listId, {
|
||||
feedIds: [feed.id, ...list.feedIds],
|
||||
})
|
||||
feedActions.upsertMany([get().feeds[listId]])
|
||||
}
|
||||
|
||||
async removeFeedFromFeedList(listId: string, feedId: string) {
|
||||
const list = get().feeds[listId] as ListModel
|
||||
if (!list) return
|
||||
|
||||
this.patch(listId, {
|
||||
feedIds: list.feedIds.filter((id) => id !== feedId),
|
||||
})
|
||||
feedActions.upsertMany([get().feeds[listId]])
|
||||
}
|
||||
|
||||
// API Fetcher
|
||||
//
|
||||
|
||||
|
|
@ -136,17 +171,17 @@ class FeedActions {
|
|||
|
||||
async fetchOwnedLists() {
|
||||
const res = await apiClient.lists.list.$get()
|
||||
this.upsertMany(res.data)
|
||||
this.upsertMany(res.data.concat())
|
||||
|
||||
return res.data
|
||||
}
|
||||
}
|
||||
export const feedActions = new FeedActions()
|
||||
|
||||
export const getFeedById = (feedId: string): Nullable<TargetModel> =>
|
||||
export const getFeedById = (feedId: string): Nullable<FeedOrListRespModel> =>
|
||||
useFeedStore.getState().feeds[feedId]
|
||||
|
||||
export const getPreferredTitle = (feed?: TargetModel | null) => {
|
||||
export const getPreferredTitle = (feed?: FeedOrListRespModel | null) => {
|
||||
if (!feed?.id) {
|
||||
return feed?.title
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import type { TargetModel } from "~/models"
|
||||
import type { FeedOrListModel, FeedOrListRespModel } from "~/models"
|
||||
|
||||
type FeedId = string
|
||||
|
||||
export interface FeedState {
|
||||
feeds: Record<FeedId, TargetModel>
|
||||
feeds: Record<FeedId, FeedOrListModel>
|
||||
}
|
||||
|
||||
export interface FeedActions {
|
||||
upsertMany: (feeds: TargetModel[]) => void
|
||||
upsertMany: (feeds: FeedOrListRespModel[]) => void
|
||||
clear: () => void
|
||||
patch: (feedId: FeedId, patch: Partial<TargetModel>) => void
|
||||
patch: (feedId: FeedId, patch: Partial<FeedOrListModel>) => void
|
||||
}
|
||||
|
||||
export type FeedQueryParams = { id?: string; url?: string; isList?: boolean }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { EntryModel, TargetModel } from "~/models"
|
||||
import type { EntryModel, FeedOrListRespModel } from "~/models"
|
||||
|
||||
import type { SubscriptionFlatModel } from "../subscription"
|
||||
import type { SearchType } from "./constants"
|
||||
|
|
@ -9,7 +9,7 @@ export interface SearchResult<T extends object, A extends object = object> exten
|
|||
}
|
||||
|
||||
export interface SearchState {
|
||||
feeds: SearchResult<TargetModel>[]
|
||||
feeds: SearchResult<FeedOrListRespModel>[]
|
||||
entries: SearchResult<EntryModel, { feedId: string }>[]
|
||||
subscriptions: SearchResult<SubscriptionFlatModel, { feedId: string }>[]
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
"confirm": "تأكيد",
|
||||
"ok": "موافق",
|
||||
"quantifier.piece": "",
|
||||
"space": " ",
|
||||
"time.last_night": "الليلة الماضية",
|
||||
"time.the_night_before_last": "الليلة التي قبل الماضية",
|
||||
"time.today": "اليوم",
|
||||
|
|
@ -24,6 +25,5 @@
|
|||
"words.result": "نتيجة",
|
||||
"words.result_one": "نتيجة",
|
||||
"words.result_other": "نتائج",
|
||||
"words.space": " ",
|
||||
"words.which.all": "الكل"
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
"confirm": "تأكيد",
|
||||
"ok": "موافق",
|
||||
"quantifier.piece": "",
|
||||
"space": " ",
|
||||
"time.last_night": "الليلة الماضية",
|
||||
"time.the_night_before_last": "الليلة قبل الماضية",
|
||||
"time.today": "اليوم",
|
||||
|
|
@ -23,6 +24,5 @@
|
|||
"words.result": "نتيجة",
|
||||
"words.result_one": "نتيجة",
|
||||
"words.result_other": "نتائج",
|
||||
"words.space": " ",
|
||||
"words.which.all": "الكل"
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
"confirm": "تأكيد",
|
||||
"ok": "موافق",
|
||||
"quantifier.piece": "",
|
||||
"space": " ",
|
||||
"time.last_night": "الليلة الماضية",
|
||||
"time.the_night_before_last": "الليلة قبل الماضية",
|
||||
"time.today": "اليوم",
|
||||
|
|
@ -23,6 +24,5 @@
|
|||
"words.result": "نتيجة",
|
||||
"words.result_one": "نتيجة",
|
||||
"words.result_other": "نتائج",
|
||||
"words.space": " ",
|
||||
"words.which.all": "الكل"
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
"confirm": "تأكيد",
|
||||
"ok": "موافق",
|
||||
"quantifier.piece": "",
|
||||
"space": " ",
|
||||
"time.last_night": "الليلة الماضية",
|
||||
"time.the_night_before_last": "الليلة التي سبقتها",
|
||||
"time.today": "اليوم",
|
||||
|
|
@ -24,6 +25,5 @@
|
|||
"words.result": "نتيجة",
|
||||
"words.result_one": "نتيجة",
|
||||
"words.result_other": "نتائج",
|
||||
"words.space": " ",
|
||||
"words.which.all": "الكل"
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
"confirm": "تأكيد",
|
||||
"ok": "حسنًا",
|
||||
"quantifier.piece": "",
|
||||
"space": " ",
|
||||
"time.last_night": "الليلة الماضية",
|
||||
"time.the_night_before_last": "الليلة قبل الماضية",
|
||||
"time.today": "اليوم",
|
||||
|
|
@ -24,6 +25,5 @@
|
|||
"words.result": "نتيجة",
|
||||
"words.result_one": "نتيجة",
|
||||
"words.result_other": "نتائج",
|
||||
"words.space": " ",
|
||||
"words.which.all": "الكل"
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
"confirm": "تأكيد",
|
||||
"ok": "موافق",
|
||||
"quantifier.piece": "",
|
||||
"space": " ",
|
||||
"time.last_night": "الليلة الماضية",
|
||||
"time.the_night_before_last": "الليلة التي قبلها",
|
||||
"time.today": "اليوم",
|
||||
|
|
@ -23,6 +24,5 @@
|
|||
"words.result": "نتيجة",
|
||||
"words.result_one": "نتيجة",
|
||||
"words.result_other": "نتائج",
|
||||
"words.space": " ",
|
||||
"words.which.all": "الكل"
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
"confirm": "Confirm",
|
||||
"ok": "OK",
|
||||
"quantifier.piece": "",
|
||||
"space": " ",
|
||||
"time.last_night": "Last Night",
|
||||
"time.the_night_before_last": "The Night Before Last",
|
||||
"time.today": "Today",
|
||||
|
|
@ -28,8 +29,7 @@
|
|||
"words.result": "result",
|
||||
"words.result_one": "result",
|
||||
"words.result_other": "results",
|
||||
"words.space": " ",
|
||||
"words.submit": "Submit",
|
||||
"words.update": "Update",
|
||||
"words.which.all": "All"
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
"confirm": "Confirmar",
|
||||
"ok": "OK",
|
||||
"quantifier.piece": "",
|
||||
"space": " ",
|
||||
"time.last_night": "Anoche",
|
||||
"time.the_night_before_last": "Anteanoche",
|
||||
"time.today": "Hoy",
|
||||
|
|
@ -24,6 +25,5 @@
|
|||
"words.result": "resultado",
|
||||
"words.result_one": "resultado",
|
||||
"words.result_other": "resultados",
|
||||
"words.space": " ",
|
||||
"words.which.all": "Todo"
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
"confirm": "Vahvista",
|
||||
"ok": "OK",
|
||||
"quantifier.piece": "",
|
||||
"space": " ",
|
||||
"time.last_night": "Viime yö",
|
||||
"time.the_night_before_last": "Toissa yö",
|
||||
"time.today": "Tänään",
|
||||
|
|
@ -23,6 +24,5 @@
|
|||
"words.result": "tulos",
|
||||
"words.result_one": "tulos",
|
||||
"words.result_other": "tulokset",
|
||||
"words.space": " ",
|
||||
"words.which.all": "Kaikki"
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
"confirm": "Confirmer",
|
||||
"ok": "OK",
|
||||
"quantifier.piece": "",
|
||||
"space": " ",
|
||||
"time.last_night": "La nuit dernière",
|
||||
"time.the_night_before_last": "L'avant-dernière nuit",
|
||||
"time.today": "Aujourd'hui",
|
||||
|
|
@ -24,6 +25,5 @@
|
|||
"words.result": "résultat",
|
||||
"words.result_one": "résultat",
|
||||
"words.result_other": "résultats",
|
||||
"words.space": " ",
|
||||
"words.which.all": "Tous"
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
"confirm": "Conferma",
|
||||
"ok": "OK",
|
||||
"quantifier.piece": "",
|
||||
"space": " ",
|
||||
"time.last_night": "Ieri sera",
|
||||
"time.the_night_before_last": "L'altra sera",
|
||||
"time.today": "Oggi",
|
||||
|
|
@ -23,6 +24,5 @@
|
|||
"words.result": "risultato",
|
||||
"words.result_one": "risultato",
|
||||
"words.result_other": "risultati",
|
||||
"words.space": " ",
|
||||
"words.which.all": "Tutti"
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
"confirm": "確認",
|
||||
"ok": "OK",
|
||||
"quantifier.piece": "個",
|
||||
"space": "",
|
||||
"time.last_night": "昨夜",
|
||||
"time.the_night_before_last": "一昨夜",
|
||||
"time.today": "今日",
|
||||
|
|
@ -24,6 +25,5 @@
|
|||
"words.result": "結果",
|
||||
"words.result_one": "結果",
|
||||
"words.result_other": "結果",
|
||||
"words.space": "",
|
||||
"words.which.all": "すべて"
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
"confirm": "확인",
|
||||
"ok": "확인",
|
||||
"quantifier.piece": "",
|
||||
"space": " ",
|
||||
"time.last_night": "어젯밤",
|
||||
"time.the_night_before_last": "그저께 밤",
|
||||
"time.today": "오늘",
|
||||
|
|
@ -27,8 +28,7 @@
|
|||
"words.result": "결과",
|
||||
"words.result_one": "결과",
|
||||
"words.result_other": "결과들",
|
||||
"words.space": " ",
|
||||
"words.submit": "제출",
|
||||
"words.update": "업데이트",
|
||||
"words.which.all": "모두"
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
"confirm": "Confirmar",
|
||||
"ok": "OK",
|
||||
"quantifier.piece": "",
|
||||
"space": " ",
|
||||
"time.last_night": "Ontem à noite",
|
||||
"time.the_night_before_last": "Anteontem à noite",
|
||||
"time.today": "Hoje",
|
||||
|
|
@ -24,6 +25,5 @@
|
|||
"words.result": "resultado",
|
||||
"words.result_one": "resultado",
|
||||
"words.result_other": "resultados",
|
||||
"words.space": " ",
|
||||
"words.which.all": "Todos"
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
"confirm": "Подтвердить",
|
||||
"ok": "ОК",
|
||||
"quantifier.piece": "",
|
||||
"space": " ",
|
||||
"time.last_night": "Прошлой ночью",
|
||||
"time.the_night_before_last": "Позапрошлой ночью",
|
||||
"time.today": "Сегодня",
|
||||
|
|
@ -23,6 +24,5 @@
|
|||
"words.result": "результат",
|
||||
"words.result_one": "результат",
|
||||
"words.result_other": "результаты",
|
||||
"words.space": " ",
|
||||
"words.which.all": "Все"
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
"confirm": "Onayla",
|
||||
"ok": "Tamam",
|
||||
"quantifier.piece": "",
|
||||
"space": " ",
|
||||
"time.last_night": "Dün Gece",
|
||||
"time.the_night_before_last": "Evvelsi Gece",
|
||||
"time.today": "Bugün",
|
||||
|
|
@ -27,8 +28,7 @@
|
|||
"words.result": "sonuç",
|
||||
"words.result_one": "sonuç",
|
||||
"words.result_other": "sonuçlar",
|
||||
"words.space": " ",
|
||||
"words.submit": "Gönder",
|
||||
"words.update": "Güncelle",
|
||||
"words.which.all": "Tümü"
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
"confirm": "确认",
|
||||
"ok": "好",
|
||||
"quantifier.piece": "条",
|
||||
"space": "",
|
||||
"time.last_night": "昨晚",
|
||||
"time.the_night_before_last": "前天晚上",
|
||||
"time.today": "今天",
|
||||
|
|
@ -28,8 +29,7 @@
|
|||
"words.result": "结果",
|
||||
"words.result_one": "结果",
|
||||
"words.result_other": "结果",
|
||||
"words.space": "",
|
||||
"words.submit": "提交",
|
||||
"words.update": "更新",
|
||||
"words.which.all": "全部"
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
"confirm": "確認",
|
||||
"ok": "確定",
|
||||
"quantifier.piece": "",
|
||||
"space": " ",
|
||||
"time.last_night": "昨晚",
|
||||
"time.the_night_before_last": "前晚",
|
||||
"time.today": "今天",
|
||||
|
|
@ -27,8 +28,7 @@
|
|||
"words.result": "結果",
|
||||
"words.result_one": "結果",
|
||||
"words.result_other": "結果",
|
||||
"words.space": " ",
|
||||
"words.submit": "提交",
|
||||
"words.update": "更新",
|
||||
"words.which.all": "全部"
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
"confirm": "確認",
|
||||
"ok": "確定",
|
||||
"quantifier.piece": "",
|
||||
"space": " ",
|
||||
"time.last_night": "昨晚",
|
||||
"time.the_night_before_last": "前晚",
|
||||
"time.today": "今天",
|
||||
|
|
@ -24,6 +25,5 @@
|
|||
"words.result": "結果",
|
||||
"words.result_one": "結果",
|
||||
"words.result_other": "多筆結果",
|
||||
"words.space": " ",
|
||||
"words.which.all": "全部"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue