diff --git a/apps/renderer/src/components/feed-icon.tsx b/apps/renderer/src/components/feed-icon.tsx index 1eb7fa7f0f..30c3b84a4e 100644 --- a/apps/renderer/src/components/feed-icon.tsx +++ b/apps/renderer/src/components/feed-icon.tsx @@ -5,7 +5,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 } from "~/models" +import type { CombinedEntryModel, FeedModel, TargetModel } from "~/models" import { PlatformIcon } from "./ui/platform-icon" @@ -75,7 +75,7 @@ export function FeedIcon({ siteUrl, useMedia, }: { - feed?: FeedModel + feed?: TargetModel entry?: CombinedEntryModel["entries"] fallbackUrl?: string className?: string @@ -98,8 +98,8 @@ export function FeedIcon({ } const colors = useMemo( - () => getColorScheme(stringToHue(feed?.title || feed?.url || siteUrl!), true), - [feed?.title, feed?.url, siteUrl], + () => getColorScheme(stringToHue(feed?.title || (feed as FeedModel)?.url || siteUrl!), true), + [feed?.title, (feed as FeedModel)?.url, siteUrl], ) let ImageElement: ReactNode let finalSrc = "" @@ -109,6 +109,32 @@ export function FeedIcon({ height: size, } + const fallbackIcon = ( + + + {!!feed?.title && feed.title[0]} + + + ) + switch (true) { case !feed && !!siteUrl: { const [src] = getFeedIconSrc({ @@ -137,9 +163,9 @@ export function FeedIcon({ break } case !!fallbackUrl: - case !!feed?.siteUrl: { + case !!(feed as FeedModel)?.siteUrl: { const [src, fallbackSrc] = getFeedIconSrc({ - siteUrl: feed?.siteUrl || fallbackUrl, + siteUrl: (feed as FeedModel)?.siteUrl || fallbackUrl, fallback, proxy: { width: size * 2, @@ -150,7 +176,7 @@ export function FeedIcon({ ImageElement = ( @@ -163,6 +189,10 @@ export function FeedIcon({ ) break } + case !!feed?.title && !!feed.title[0]: { + ImageElement = fallbackIcon + break + } default: { ImageElement = break @@ -183,25 +213,7 @@ export function FeedIcon({ > {ImageElement} - - - {!!feed?.title && feed.title[0]} - - + {fallbackIcon} ) } diff --git a/apps/renderer/src/components/feed-summary.tsx b/apps/renderer/src/components/feed-summary.tsx index 623a65adb8..ef5550533b 100644 --- a/apps/renderer/src/components/feed-summary.tsx +++ b/apps/renderer/src/components/feed-summary.tsx @@ -1,6 +1,8 @@ +import { WEB_URL } from "@follow/shared/constants" + import { FeedIcon } from "~/components/feed-icon" import { cn } from "~/lib/utils" -import type { FeedModel } from "~/models" +import type { TargetModel } from "~/models" import { FeedCertification } from "./feed-certification" import { EllipsisHorizontalTextWithTooltip } from "./ui/typography" @@ -10,14 +12,14 @@ export function FollowSummary({ docs, className, }: { - feed: FeedModel + feed: TargetModel docs?: string className?: string }) { return (
{feed.title} - + {feed.type === "feed" && }
{feed.description} @@ -40,8 +42,12 @@ export function FollowSummary({
- - {feed.url || docs} + + {feed.type === "feed" ? feed.url || docs : `list:${feed.id}`}
diff --git a/apps/renderer/src/components/ui/markdown/renderers/MarkdownLink.tsx b/apps/renderer/src/components/ui/markdown/renderers/MarkdownLink.tsx index 1eeb788147..90060b253c 100644 --- a/apps/renderer/src/components/ui/markdown/renderers/MarkdownLink.tsx +++ b/apps/renderer/src/components/ui/markdown/renderers/MarkdownLink.tsx @@ -18,7 +18,9 @@ const safeUrl = (url: string, baseUrl: string) => { export const MarkdownLink = (props: LinkProps) => { const { view, feedId } = useEntryContentContext() - const feedSiteUrl = useFeedByIdSelector(feedId, (feed) => feed?.siteUrl) + const feedSiteUrl = useFeedByIdSelector(feedId, (feed) => + "siteUrl" in feed ? feed.siteUrl : undefined, + ) const populatedFullHref = useMemo(() => { const { href } = props diff --git a/apps/renderer/src/components/ui/radio-group/Radio.tsx b/apps/renderer/src/components/ui/radio-group/Radio.tsx index 75843b541e..c53835b886 100644 --- a/apps/renderer/src/components/ui/radio-group/Radio.tsx +++ b/apps/renderer/src/components/ui/radio-group/Radio.tsx @@ -23,13 +23,13 @@ export const Radio: FC< onChange?.(e) }) return ( -
+
-
diff --git a/apps/renderer/src/database/schemas/feed.ts b/apps/renderer/src/database/schemas/feed.ts index 563be27fc4..de391b9daf 100644 --- a/apps/renderer/src/database/schemas/feed.ts +++ b/apps/renderer/src/database/schemas/feed.ts @@ -1,8 +1,8 @@ -import type { FeedModel } from "~/models" +import type { TargetModel } from "~/models" export type DB_FeedUnread = { id: string count: number } -export type DB_Feed = FeedModel & { id: string } +export type DB_Feed = TargetModel & { id: string } diff --git a/apps/renderer/src/hooks/biz/useFeedActions.tsx b/apps/renderer/src/hooks/biz/useFeedActions.tsx index 05e6449f6c..6ed7d9af41 100644 --- a/apps/renderer/src/hooks/biz/useFeedActions.tsx +++ b/apps/renderer/src/hooks/biz/useFeedActions.tsx @@ -38,6 +38,7 @@ export const useFeedActions = ({ const items = useMemo(() => { if (!feed) return [] + const isList = feed?.type === "list" const items: NativeMenuItem[] = [ { type: "text" as const, @@ -46,7 +47,9 @@ export const useFeedActions = ({ click: () => { present({ title: t("sidebar.feed_actions.edit_feed"), - content: ({ dismiss }) => , + content: ({ dismiss }) => ( + + ), }) }, }, @@ -60,7 +63,11 @@ export const useFeedActions = ({ }, { type: "text" as const, - label: t("sidebar.feed_actions.navigate_to_feed"), + label: t( + isList + ? "sidebar.feed_actions.navigate_to_list" + : "sidebar.feed_actions.navigate_to_feed", + ), shortcut: "Meta+G", disabled: !isEntryList || getRouteParams().feedId === feedId, click: () => { @@ -71,14 +78,18 @@ export const useFeedActions = ({ type: "separator" as const, disabled: isEntryList, }, - { - type: "text", - label: t("sidebar.feed_actions.mark_all_as_read"), - shortcut: "Meta+Shift+A", - disabled: isEntryList, - click: () => subscriptionActions.markReadByFeedIds([feedId]), - }, - ...(!feed.ownerUserId && !!feed.id + ...(!isList + ? [ + { + type: "text" as const, + label: t("sidebar.feed_actions.mark_all_as_read"), + shortcut: "Meta+Shift+A", + disabled: isEntryList, + click: () => subscriptionActions.markReadByFeedIds({ feedIds: [feedId] }), + }, + ] + : []), + ...(!feed.ownerUserId && !!feed.id && !isList ? [ { type: "text" as const, @@ -96,7 +107,11 @@ export const useFeedActions = ({ ? [ { type: "text" as const, - label: t("sidebar.feed_actions.feed_owned_by_you"), + label: t( + isList + ? "sidebar.feed_actions.list_owned_by_you" + : "sidebar.feed_actions.feed_owned_by_you", + ), }, ] : []), @@ -106,37 +121,58 @@ export const useFeedActions = ({ }, { type: "text" as const, - label: t("sidebar.feed_actions.open_feed_in_browser"), + label: t( + isList + ? "sidebar.feed_actions.open_list_in_browser" + : "sidebar.feed_actions.open_feed_in_browser", + ), disabled: isEntryList, shortcut: "O", - click: () => window.open(`${WEB_URL}/feed/${feedId}?view=${view}`, "_blank"), - }, - { - type: "text" as const, - label: t("sidebar.feed_actions.open_site_in_browser"), - shortcut: "Meta+O", - disabled: isEntryList, - click: () => { - const feed = getFeedById(feedId) - if (feed) { - feed.siteUrl && window.open(feed.siteUrl, "_blank") - } - }, + click: () => + window.open( + isList + ? `${WEB_URL}/list/${feedId}?view=${view}` + : `${WEB_URL}/feed/${feedId}?view=${view}`, + "_blank", + ), }, + ...(!isList + ? [ + { + type: "text" as const, + label: t("sidebar.feed_actions.open_site_in_browser"), + shortcut: "Meta+O", + disabled: isEntryList, + click: () => { + const feed = getFeedById(feedId) + if (feed) { + "siteUrl" in feed && feed.siteUrl && window.open(feed.siteUrl, "_blank") + } + }, + }, + ] + : []), { type: "separator", disabled: isEntryList, }, { type: "text" as const, - label: t("sidebar.feed_actions.copy_feed_url"), + label: t( + isList ? "sidebar.feed_actions.copy_list_url" : "sidebar.feed_actions.copy_feed_url", + ), disabled: isEntryList, shortcut: "Meta+C", - click: () => navigator.clipboard.writeText(feed.url), + click: () => { + const url = isList ? `${WEB_URL}/list/${feedId}?view=${view}` : feed.url + navigator.clipboard.writeText(url) + }, }, { type: "text" as const, - label: t("sidebar.feed_actions.copy_feed_id"), + label: t( + isList ? "sidebar.feed_actions.copy_list_id" : "sidebar.feed_actions.copy_feed_id", + ), shortcut: "Meta+Shift+C", disabled: isEntryList, click: () => { diff --git a/apps/renderer/src/hooks/biz/useSubscriptionActions.tsx b/apps/renderer/src/hooks/biz/useSubscriptionActions.tsx index 9e93e80cb0..b79212f53e 100644 --- a/apps/renderer/src/hooks/biz/useSubscriptionActions.tsx +++ b/apps/renderer/src/hooks/biz/useSubscriptionActions.tsx @@ -26,7 +26,8 @@ export const useDeleteSubscription = ({ onSuccess }: { onSuccess?: () => void }) // TODO store action await apiClient.subscriptions.$post({ json: { - url: feed.url, + url: feed.type === "feed" ? feed.url : undefined, + listId: feed.type === "list" ? feed.id : undefined, view: subscription.view, category: subscription.category, isPrivate: subscription.isPrivate, diff --git a/apps/renderer/src/lib/utils.ts b/apps/renderer/src/lib/utils.ts index a93ccd7556..f109b5358c 100644 --- a/apps/renderer/src/lib/utils.ts +++ b/apps/renderer/src/lib/utils.ts @@ -27,7 +27,11 @@ export function getEntriesParams({ id, view }: { id?: number | string; view?: nu if (id === FEED_COLLECTION_LIST) { params.isCollection = true } else if (id && id !== ROUTE_FEED_PENDING) { - params.feedIdList = `${id}`.split(",") + if (id.toString().includes(",")) { + params.feedIdList = `${id}`.split(",") + } else { + params.feedId = `${id}` + } } if (view === FeedViewType.SocialMedia) { params.withContent = true diff --git a/apps/renderer/src/models/types.ts b/apps/renderer/src/models/types.ts index 43dc3923c6..099aacd628 100644 --- a/apps/renderer/src/models/types.ts +++ b/apps/renderer/src/models/types.ts @@ -19,22 +19,15 @@ export type ActiveList = { view: number } -export type FeedResponse = SubscriptionResponse[number]["feeds"] - export type TransactionModel = ExtractBizResponse< typeof apiClient.wallets.transactions.$get >["data"][number] -export type FeedModel = ExtractBizResponse["data"]["feed"] & { - owner?: UserModel | null - tipUsers?: UserModel[] | null -} +export type FeedModel = ExtractBizResponse["data"]["feed"] -export type SubscriptionResponse = Array< - ExtractBizResponse["data"][number] & { - unread?: number - } -> +export type ListModel = ExtractBizResponse["data"]["list"] + +export type TargetModel = FeedModel | ListModel export type EntryResponse = Exclude< Extract, { code: 0 }>["data"], @@ -60,13 +53,6 @@ export type ActionsResponse = Exclude< undefined >["rules"] -export type ListResponse = { - code: number - data?: T - total?: number - message?: string -} - export type DataResponse = { code: number data?: T @@ -74,15 +60,15 @@ export type DataResponse = { export type ActiveEntryId = Nullable -export type SubscriptionModel = SubscriptionResponse[number] - -export type FeedListModel = { - list: { - list: SubscriptionResponse - name: string - }[] +export type SubscriptionModel = ExtractBizResponse< + typeof apiClient.subscriptions.$get +>["data"][number] & { + unread?: number } +export type FeedSubscriptionModel = Extract +export type ListSubscriptionModel = Extract + export type SupportedLanguages = z.infer export type RecommendationItem = ExtractBizResponse< diff --git a/apps/renderer/src/modules/claim/feed-claim-modal.tsx b/apps/renderer/src/modules/claim/feed-claim-modal.tsx index db01bd3f37..ebe120f791 100644 --- a/apps/renderer/src/modules/claim/feed-claim-modal.tsx +++ b/apps/renderer/src/modules/claim/feed-claim-modal.tsx @@ -12,6 +12,7 @@ import { LoadingCircle } from "~/components/ui/loading" import { useCurrentModal } from "~/components/ui/modal" import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs" import { useAuthQuery } from "~/hooks/common" +import type { FeedModel } from "~/models" import { Queries } from "~/queries" import { useClaimFeedMutation } from "~/queries/feed" import { useFeedById } from "~/store/feed" @@ -20,7 +21,7 @@ export const FeedClaimModalContent: FC<{ feedId: string }> = ({ feedId }) => { const { t } = useTranslation() - const feed = useFeedById(feedId) + const feed = useFeedById(feedId) as FeedModel const { data: claimMessage, isLoading, diff --git a/apps/renderer/src/modules/discover/feed-form.tsx b/apps/renderer/src/modules/discover/feed-form.tsx index 58b0769c38..3827c0705c 100644 --- a/apps/renderer/src/modules/discover/feed-form.tsx +++ b/apps/renderer/src/modules/discover/feed-form.tsx @@ -49,14 +49,15 @@ const defaultValue = { view: FeedViewType.Articles.toString() } as z.infer asWidget?: boolean onSuccess?: () => void -}> = ({ id: _id, defaultValues = defaultValue, url, asWidget, onSuccess }) => { - const queryParams = { id: _id, url } +}> = ({ id: _id, defaultValues = defaultValue, url, asWidget, onSuccess, isList }) => { + const queryParams = { id: _id, url, isList } const feedQuery = useFeed(queryParams) @@ -165,10 +166,16 @@ const FeedInnerForm = ({ const buttonRef = useRef(null) const isSubscribed = !!subscription const feed = useFeedByIdOrUrl({ id, url })! + const isList = feed?.type === "list" const form = useForm>({ resolver: zodResolver(formSchema), - defaultValues, + defaultValues: isList + ? { + ...defaultValues, + view: feed.view.toString(), + } + : defaultValues, }) const { setClickOutSideToDismiss, dismiss } = useCurrentModal() @@ -189,17 +196,16 @@ const FeedInnerForm = ({ const followMutation = useMutation({ mutationFn: async (values: z.infer) => { const body = { - url: feed.url, + ...(isList ? { listId: feed.id } : { url: feed.url }), view: Number.parseInt(values.view), category: values.category, isPrivate: values.isPrivate, title: values.title, - ...(isSubscribed && { feedId: feed.id }), + ...(isSubscribed && !isList && { feedId: feed.id }), } const $method = isSubscribed ? apiClient.subscriptions.$patch : apiClient.subscriptions.$post return $method({ - // @ts-expect-error json: body, }) }, @@ -266,7 +272,9 @@ const FeedInnerForm = ({ {t("feed_form.view")} - + {views.map((view) => (
{titleAtBottom && titleInfo} diff --git a/apps/renderer/src/modules/entry-column/types.ts b/apps/renderer/src/modules/entry-column/types.ts index 6856f78f36..f6e7fb6ffd 100644 --- a/apps/renderer/src/modules/entry-column/types.ts +++ b/apps/renderer/src/modules/entry-column/types.ts @@ -1,11 +1,11 @@ import type { FC } from "react" -import type { CombinedEntryModel, FeedModel } from "~/models" +import type { CombinedEntryModel, TargetModel } from "~/models" export type UniversalItemProps = { entryId: string entryPreview?: CombinedEntryModel & { - feeds: FeedModel + feeds: TargetModel feedId: string } translation?: { diff --git a/apps/renderer/src/modules/entry-content/index.tsx b/apps/renderer/src/modules/entry-content/index.tsx index 0d34683a95..ed68a6a53e 100644 --- a/apps/renderer/src/modules/entry-content/index.tsx +++ b/apps/renderer/src/modules/entry-content/index.tsx @@ -35,7 +35,7 @@ import { stopPropagation } from "~/lib/dom" import { FeedViewType } from "~/lib/enum" import { getNewIssueUrl } from "~/lib/issues" import { cn } from "~/lib/utils" -import type { ActiveEntryId } from "~/models" +import type { ActiveEntryId, FeedModel } from "~/models" import { useIsSoFWrappedElement, useWrappedElement, @@ -97,7 +97,7 @@ export const EntryContentRender: Component<{ entryId: string }> = ({ entryId, cl const entry = useEntry(entryId) useTitle(entry?.entries.title) - const feed = useFeedById(entry?.feedId) + const feed = useFeedById(entry?.feedId) as FeedModel const entryHistory = useEntryReadHistory(entryId) diff --git a/apps/renderer/src/modules/feed-column/category.tsx b/apps/renderer/src/modules/feed-column/category.tsx index b640326763..596736dc22 100644 --- a/apps/renderer/src/modules/feed-column/category.tsx +++ b/apps/renderer/src/modules/feed-column/category.tsx @@ -155,7 +155,9 @@ function FeedCategoryImpl({ type: "text", label: t("sidebar.feed_column.context_menu.mark_as_read"), click: () => { - subscriptionActions.markReadByFeedIds(ids) + subscriptionActions.markReadByFeedIds({ + feedIds: ids, + }) }, }, { type: "separator" }, diff --git a/apps/renderer/src/modules/feed-column/header.tsx b/apps/renderer/src/modules/feed-column/header.tsx index f91331c1d8..4fabea7fc0 100644 --- a/apps/renderer/src/modules/feed-column/header.tsx +++ b/apps/renderer/src/modules/feed-column/header.tsx @@ -2,7 +2,6 @@ import { m } from "framer-motion" import type { FC, PropsWithChildren } from "react" import { memo, useCallback, useEffect, useRef, useState } from "react" import { useTranslation } from "react-i18next" -import { Link } from "react-router-dom" import { toast } from "sonner" import { setAppSearchOpen } from "~/atoms/app" @@ -34,7 +33,6 @@ const useBackHome = (active: number) => { export const FeedColumnHeader = memo(() => { const [active] = useSidebarActiveView() - const { t } = useTranslation() const navigateBackHome = useBackHome(active) const normalStyle = !window.electron || window.electron.process.platform !== "darwin" return ( @@ -62,11 +60,6 @@ export const FeedColumnHeader = memo(() => {
- - - - -
diff --git a/apps/renderer/src/modules/feed-column/item.tsx b/apps/renderer/src/modules/feed-column/item.tsx index 2fd1bec891..67fa32ec74 100644 --- a/apps/renderer/src/modules/feed-column/item.tsx +++ b/apps/renderer/src/modules/feed-column/item.tsx @@ -17,7 +17,7 @@ import { getNewIssueUrl } from "~/lib/issues" import { showNativeMenu } from "~/lib/native-menu" import { cn } from "~/lib/utils" import { getPreferredTitle, useFeedById } from "~/store/feed" -import { useSubscriptionByFeedId } from "~/store/subscription" +import { subscriptionActions, useSubscriptionByFeedId } from "~/store/subscription" import { useFeedUnreadStore } from "~/store/unread" import { UnreadNumber } from "./unread-number" @@ -32,6 +32,8 @@ const FeedItemImpl = ({ view, feedId, className, showUnreadCount = true }: FeedI const { t } = useTranslation() const subscription = useSubscriptionByFeedId(feedId) const navigate = useNavigateEntry() + const feed = useFeedById(feedId) + const handleNavigate: React.MouseEventHandler = useCallback( (e) => { e.stopPropagation() @@ -41,20 +43,23 @@ const FeedItemImpl = ({ view, feedId, className, showUnreadCount = true }: FeedI entryId: null, view, }) + if (feed?.type === "list") { + subscriptionActions.markReadByFeedIds({ + listId: feedId, + }) + } // focus to main container in order to let keyboard can navigate entry items by arrow keys nextFrame(() => { getMainContainerElement()?.focus() }) }, - [feedId, navigate, view], + [feedId, navigate, view, feed?.type], ) const feedUnread = useFeedUnreadStore((state) => state.data[feedId] || 0) const isActive = useRouteParamsSelector((routerParams) => routerParams.feedId === feedId) - const feed = useFeedById(feedId) - const { items } = useFeedActions({ feedId, view }) const [isContextMenuOpen, setIsContextMenuOpen] = useState(false) @@ -68,9 +73,9 @@ const FeedItemImpl = ({ view, feedId, className, showUnreadCount = true }: FeedI
{ setIsContextMenuOpen(true) const nextItems = items.concat() - if (feed.errorAt && feed.errorMessage) { + if (feed.type === "feed" && feed.errorAt && feed.errorMessage) { nextItems.push( { type: "separator", @@ -109,10 +114,10 @@ const FeedItemImpl = ({ view, feedId, className, showUnreadCount = true }: FeedI
- +
{getPreferredTitle(feed)}
- - {feed.errorAt && ( + {feed.type === "feed" && } + {feed.type === "feed" && feed.errorAt && ( @@ -157,7 +162,7 @@ const FeedItemImpl = ({ view, feedId, className, showUnreadCount = true }: FeedI )}
- +
) diff --git a/apps/renderer/src/modules/feed-column/list.tsx b/apps/renderer/src/modules/feed-column/list.tsx index 828269cf6a..79240391b9 100644 --- a/apps/renderer/src/modules/feed-column/list.tsx +++ b/apps/renderer/src/modules/feed-column/list.tsx @@ -2,7 +2,7 @@ import * as HoverCard from "@radix-ui/react-hover-card" import { AnimatePresence, m } from "framer-motion" import { Fragment, memo, useMemo, useState } from "react" import { useTranslation } from "react-i18next" -import { Link } from "react-router-dom" +import { Link, useLocation, useNavigate } from "react-router-dom" import { useUISettingKey } from "~/atoms/settings/ui" import { ScrollArea } from "~/components/ui/scroll-area" @@ -29,7 +29,7 @@ import { import { FeedCategory } from "./category" import { UnreadNumber } from "./unread-number" -const useGroupedData = (view: FeedViewType) => { +const useFeedsGroupedData = (view: FeedViewType) => { const { data: remoteData } = useAuthQuery(Queries.subscription.byView(view)) const data = useSubscriptionByView(view) || remoteData @@ -53,6 +53,26 @@ const useGroupedData = (view: FeedViewType) => { }, [data]) } +const useListsGroupedData = (view: FeedViewType) => { + const { data: remoteData } = useAuthQuery(Queries.subscription.byView(view)) + + const data = useSubscriptionByView(view) || remoteData + + return useMemo(() => { + if (!data || data.length === 0) return {} + + const groupFolder = {} as Record + + for (const subscription of data) { + if (!subscription.category && !subscription.defaultCategory) { + groupFolder[subscription.feedId] = [subscription.feedId] + } + } + + return groupFolder + }, [data]) +} + const useUpdateUnreadCount = () => { useAuthQuery(Queries.subscription.unreadAll(), { refetchInterval: false, @@ -61,28 +81,32 @@ const useUpdateUnreadCount = () => { function FeedListImpl({ className, view }: { className?: string; view: number }) { const [expansion, setExpansion] = useState(false) - const data = useGroupedData(view) + const feedsData = useFeedsGroupedData(view) + const listsData = useListsGroupedData(view) useUpdateUnreadCount() const totalUnread = useFeedUnreadStore((state) => { let unread = 0 - for (const category in data) { - for (const feedId of data[category]) { + for (const category in feedsData) { + for (const feedId of feedsData[category]) { unread += state.data[feedId] || 0 } } return unread }) - const hasData = Object.keys(data).length > 0 + const hasData = Object.keys(feedsData).length > 0 || Object.keys(listsData).length > 0 const feedId = useRouteFeedId() - const navigate = useNavigateEntry() + const navigateEntry = useNavigateEntry() const { t } = useTranslation() + const location = useLocation() + const navigate = useNavigate() + return (
{ e.stopPropagation() if (view !== undefined) { - navigate({ + navigateEntry({ entryId: null, feedId: null, view, @@ -117,13 +141,26 @@ function FeedListImpl({ className, view }: { className?: string; view: number })
{ + e.stopPropagation() + navigate(`/discover`) + }} + > + + {t("words.discover")} +
+
{ e.stopPropagation() if (view !== undefined) { - navigate({ + navigateEntry({ entryId: null, feedId: FEED_COLLECTION_LIST, view, @@ -131,11 +168,22 @@ function FeedListImpl({ className, view }: { className?: string; view: number }) } }} > - + {t("words.starred")}
+ {Object.keys(listsData).length > 0 && ( + <> +
+ {t("words.lists")} +
+ + + )} +
+ {t("words.feeds")} +
{hasData ? ( - + ) : (
{ +export const UnreadNumber = ({ + unread, + className, + isList, +}: { + unread: number + className?: string + isList?: boolean +}) => { const showUnreadCount = useUISettingKey("sidebarShowUnreadCount") if (!showUnreadCount) return null if (!unread) return null @@ -9,7 +17,11 @@ export const UnreadNumber = ({ unread, className }: { unread: number; className?
- {unread} + {isList ? ( + + ) : ( + unread + )}
) } diff --git a/apps/renderer/src/modules/panel/cmdk.tsx b/apps/renderer/src/modules/panel/cmdk.tsx index 48498c4db5..84e91f2a3b 100644 --- a/apps/renderer/src/modules/panel/cmdk.tsx +++ b/apps/renderer/src/modules/panel/cmdk.tsx @@ -178,7 +178,7 @@ export const SearchCmdK: React.FC = () => { feedId={entry.feedId} entryId={entry.item.id} id={entry.item.id} - icon={feed?.siteUrl} + icon={feed?.type === "feed" ? feed?.siteUrl : undefined} subtitle={feed?.title} /> ) @@ -203,7 +203,7 @@ export const SearchCmdK: React.FC = () => { feedId={feed.item.id!} entryId={ROUTE_ENTRY_PENDING} id={feed.item.id!} - icon={feed.item.siteUrl} + icon={feed.item.type === "feed" ? feed.item.siteUrl : undefined} subtitle={useFeedUnreadStore.getState().data[feed.item.id!]?.toString()} /> ))} diff --git a/apps/renderer/src/modules/profile/hooks.ts b/apps/renderer/src/modules/profile/hooks.ts index 7e697d50fe..579737d8c5 100644 --- a/apps/renderer/src/modules/profile/hooks.ts +++ b/apps/renderer/src/modules/profile/hooks.ts @@ -19,7 +19,7 @@ export const useUserSubscriptionsQuery = (userId: string | undefined) => { const groupFolder = {} as Record for (const subscription of res.data || []) { - if (!subscription.category && subscription.feeds) { + if (!subscription.category && "feeds" in subscription) { const { siteUrl } = subscription.feeds if (!siteUrl) continue const parsed = parse(siteUrl) diff --git a/apps/renderer/src/modules/profile/profile-setting-form.tsx b/apps/renderer/src/modules/profile/profile-setting-form.tsx index d334b7edd6..0a299a6690 100644 --- a/apps/renderer/src/modules/profile/profile-setting-form.tsx +++ b/apps/renderer/src/modules/profile/profile-setting-form.tsx @@ -102,14 +102,17 @@ export const ProfileSettingForm = () => { {t("profile.avatar.label")} - +
+ + {field.value && ( + + + + )} +
- - - -
)} /> diff --git a/apps/renderer/src/modules/profile/user-profile-modal.tsx b/apps/renderer/src/modules/profile/user-profile-modal.tsx index 7571cbdbba..6dfb1a9b0b 100644 --- a/apps/renderer/src/modules/profile/user-profile-modal.tsx +++ b/apps/renderer/src/modules/profile/user-profile-modal.tsx @@ -366,6 +366,7 @@ const SubscriptionItem: FC<{ const isFollowed = !!useSubscriptionStore((state) => state.data[subscription.feedId]) const { present } = useModalStack() const isLoose = variant === "loose" + if (!("feeds" in subscription)) return null return ( {

{t("feeds.claimTips")}

+
{claimedList.data?.length ? ( diff --git a/apps/renderer/src/modules/settings/tabs/lists.tsx b/apps/renderer/src/modules/settings/tabs/lists.tsx new file mode 100644 index 0000000000..5245cc9f95 --- /dev/null +++ b/apps/renderer/src/modules/settings/tabs/lists.tsx @@ -0,0 +1,455 @@ +import { WEB_URL } from "@follow/shared/constants" +import { zodResolver } from "@hookform/resolvers/zod" +import { useMutation } from "@tanstack/react-query" +import { useState } from "react" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import { toast } from "sonner" +import { z } from "zod" + +import { FeedCertification } from "~/components/feed-certification" +import { Avatar, AvatarImage } from "~/components/ui/avatar" +import { Button } from "~/components/ui/button" +import { Card, CardHeader } from "~/components/ui/card" +import { Divider } from "~/components/ui/divider" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "~/components/ui/form" +import { Input } from "~/components/ui/input" +import { LoadingCircle } from "~/components/ui/loading" +import { useModalStack } from "~/components/ui/modal" +import { ScrollArea } from "~/components/ui/scroll-area" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "~/components/ui/table" +import { views } from "~/constants" +import { useAuthQuery } from "~/hooks/common" +import { apiClient } from "~/lib/api-fetch" +import { cn } from "~/lib/utils" +import type { ListModel } from "~/models" +import { Queries } from "~/queries" +import { feedActions, useFeedById } from "~/store/feed" + +export const SettingLists = () => { + const { t: appT } = useTranslation() + const { t } = useTranslation("settings") + const listList = useAuthQuery(Queries.lists.list()) + + const { present } = useModalStack() + + return ( +
+
+

{t("lists.info")}

+
+ + +
+ + {listList.data?.length ? ( + + + + + {t("lists.title")} + + + {t("lists.view")} + + + {t("lists.fee")} + + + {t("lists.feeds")} + + + {t("lists.edit")} + + + + + {listList.data?.map((row) => ( + + + + {row.image && ( + + + + )} + {row.title} + + + +
+ + {views[row.view].icon} + + {appT(views[row.view].name)} +
+
+ +
+ {row.fee} + +
+
+ + + + + + +
+ ))} +
+
+ ) : listList.isLoading ? ( + + ) : ( +
+

{t("lists.noLists")}

+
+ )} +
+
+
+ ) +} + +const formSchema = z.object({ + view: z.string(), + title: z.string(), + description: z.string().optional(), + image: z.string().optional(), + fee: z.number().min(0), +}) + +const ListCreationModalContent = ({ dismiss, id }: { dismiss: () => void; id?: string }) => { + const { t: appT } = useTranslation() + const { t } = useTranslation("settings") + + const list = useFeedById(id) as ListModel + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + view: list?.view.toString() || views[0].view.toString(), + fee: list?.fee || 0, + title: list?.title || "", + description: list?.description || "", + image: list?.image || "", + }, + }) + + const createMutation = useMutation({ + mutationFn: async (values: z.infer) => { + if (id) { + await apiClient.lists.$patch({ + json: { + listId: id, + ...values, + view: Number.parseInt(values.view), + }, + }) + } else { + await apiClient.lists.$post({ + json: { + ...values, + view: Number.parseInt(values.view), + }, + }) + } + }, + onSuccess: () => { + toast.success(t(id ? "lists.edit.success" : "lists.created.success")) + Queries.lists.list().invalidate() + dismiss() + }, + async onError() { + toast.error(t(id ? "lists.edit.error" : "lists.created.error")) + }, + }) + + function onSubmit(values: z.infer) { + createMutation.mutate(values) + } + + return ( + + + ( + +
+ + {t("lists.title")} + * + +
+ + + + +
+ )} + /> + ( + +
+ {t("lists.description")} +
+ + + + +
+ )} + /> + ( +
+ + {t("lists.image")} + +
+ + {field.value && ( + + + + )} +
+
+ +
+
+ )} + /> + ( + + {t("lists.view")} + + + {views.map((view) => ( +
+ + +
+ ))} +
+
+ +
+ )} + /> + ( + +
+ {t("lists.fee")} + {t("lists.fee.description")} +
+ +
+ field.onChange(value.target.valueAsNumber)} + /> + +
+
+ +
+ )} + /> + + + + ) +} + +export const ListFeedsModalContent = ({ id }: { id: string }) => { + const list = useFeedById(id) as ListModel + const { t } = useTranslation("settings") + + const [addValue, setAddValue] = 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")) + }, + }) + + return ( +
+
+ setAddValue(e.target.value)} + /> + +
+ + + + + + + {t("lists.feeds.id")} + + + {t("lists.feeds.title")} + + + {t("lists.feeds.owner")} + + + {t("lists.feeds.remove")} + + + + + {list.feeds?.map((row) => ( + + + {row.id} + + + + {row.image && ( + + + + )} + {row.title} + + + + + + + + + + ))} + +
+
+
+ ) +} diff --git a/apps/renderer/src/modules/settings/tabs/wallet/my-wallet-section/withdraw.tsx b/apps/renderer/src/modules/settings/tabs/wallet/my-wallet-section/withdraw.tsx index 54f4576430..ba10bc1430 100644 --- a/apps/renderer/src/modules/settings/tabs/wallet/my-wallet-section/withdraw.tsx +++ b/apps/renderer/src/modules/settings/tabs/wallet/my-wallet-section/withdraw.tsx @@ -129,7 +129,6 @@ const WithdrawModalContent = ({ dismiss }: { dismiss: () => void }) => { {t("wallet.withdraw.amountLabel")} field.onChange(value.target.valueAsNumber)} diff --git a/apps/renderer/src/pages/(external)/(with-layout)/feed/[id]/index.tsx b/apps/renderer/src/pages/(external)/(with-layout)/feed/[id]/index.tsx index a3a6ab47f7..5dee62824f 100644 --- a/apps/renderer/src/pages/(external)/(with-layout)/feed/[id]/index.tsx +++ b/apps/renderer/src/pages/(external)/(with-layout)/feed/[id]/index.tsx @@ -14,6 +14,7 @@ import { usePresentFeedFormModal } from "~/hooks/biz/useFeedFormModal" import { useTitle } from "~/hooks/common" import { FeedViewType } from "~/lib/enum" import { cn } from "~/lib/utils" +import type { FeedModel } from "~/models" import { ArticleItem } from "~/modules/entry-column/Items/article-item" import { NotificationItem } from "~/modules/entry-column/Items/notification-item" import { PictureItem } from "~/modules/entry-column/Items/picture-item" @@ -31,6 +32,7 @@ export function Component() { const feed = useFeed({ id, }) + const feedData = feed.data?.feed as FeedModel const entries = useEntriesPreview({ id, }) @@ -93,11 +95,11 @@ export function Component() { })}
- {feed.data.feed.url.startsWith("https://") ? ( + {feedData.url.startsWith("https://") ? ( + + +
+ ) + )} +
+ ) +} diff --git a/apps/renderer/src/pages/(external)/(with-layout)/profile/[id]/index.tsx b/apps/renderer/src/pages/(external)/(with-layout)/profile/[id]/index.tsx index 8a841e8ef8..9a65cd6219 100644 --- a/apps/renderer/src/pages/(external)/(with-layout)/profile/[id]/index.tsx +++ b/apps/renderer/src/pages/(external)/(with-layout)/profile/[id]/index.tsx @@ -70,50 +70,58 @@ export function Component() {

{category}

- {subscriptions.data?.[category].map((subscription) => ( -
- - -
-
- {subscription.feeds?.title} -
- {subscription.feeds?.description && ( -
- {subscription.feeds.description} + {subscriptions.data?.[category].map( + (subscription) => + "feeds" in subscription && ( + - - - - -
- ))} + presentFeedFormModal(subscription.feedId) + }} + > + {isMe ? ( + t.common("words.edit") + ) : ( + <> + + {APP_NAME} + + )} + + +
+ ), + )}
)) diff --git a/apps/renderer/src/pages/settings/(settings)/feeds.tsx b/apps/renderer/src/pages/settings/(settings)/feeds.tsx index d988ea63ed..5d81f184e5 100644 --- a/apps/renderer/src/pages/settings/(settings)/feeds.tsx +++ b/apps/renderer/src/pages/settings/(settings)/feeds.tsx @@ -3,7 +3,7 @@ import { SettingsTitle } from "~/modules/settings/title" import { defineSettingPageData } from "~/modules/settings/utils" const iconName = "i-mgc-certificate-cute-re" -const priority = 1053 +const priority = 1060 export const loader = defineSettingPageData({ iconName, diff --git a/apps/renderer/src/pages/settings/(settings)/integration.tsx b/apps/renderer/src/pages/settings/(settings)/integration.tsx index 2fb020a6de..b90bc1fb2c 100644 --- a/apps/renderer/src/pages/settings/(settings)/integration.tsx +++ b/apps/renderer/src/pages/settings/(settings)/integration.tsx @@ -3,7 +3,7 @@ import { SettingsTitle } from "~/modules/settings/title" import { defineSettingPageData } from "~/modules/settings/utils" const iconName = "i-mgc-department-cute-re" -const priority = 1025 +const priority = 1030 export const loader = defineSettingPageData({ iconName, diff --git a/apps/renderer/src/pages/settings/(settings)/invitations.tsx b/apps/renderer/src/pages/settings/(settings)/invitations.tsx index 673ff6f6ad..02a3e81d0e 100644 --- a/apps/renderer/src/pages/settings/(settings)/invitations.tsx +++ b/apps/renderer/src/pages/settings/(settings)/invitations.tsx @@ -3,7 +3,7 @@ import { SettingsTitle } from "~/modules/settings/title" import { defineSettingPageData } from "~/modules/settings/utils" const iconName = "i-mgc-heart-hand-cute-re" -const priority = 1055 +const priority = 1070 export const loader = defineSettingPageData({ iconName, diff --git a/apps/renderer/src/pages/settings/(settings)/list.tsx b/apps/renderer/src/pages/settings/(settings)/list.tsx new file mode 100644 index 0000000000..b1a4b1d8bd --- /dev/null +++ b/apps/renderer/src/pages/settings/(settings)/list.tsx @@ -0,0 +1,21 @@ +import { SettingLists } from "~/modules/settings/tabs/lists" +import { SettingsTitle } from "~/modules/settings/title" +import { defineSettingPageData } from "~/modules/settings/utils" + +const iconName = "i-mgc-rada-cute-re" +const priority = 1050 + +export const loader = defineSettingPageData({ + iconName, + name: "titles.lists", + priority, +}) + +export function Component() { + return ( + <> + + + + ) +} diff --git a/apps/renderer/src/pages/settings/(settings)/profile.tsx b/apps/renderer/src/pages/settings/(settings)/profile.tsx index 3e1e583ef1..fa1603f6fd 100644 --- a/apps/renderer/src/pages/settings/(settings)/profile.tsx +++ b/apps/renderer/src/pages/settings/(settings)/profile.tsx @@ -3,7 +3,7 @@ import { SettingsTitle } from "~/modules/settings/title" import { defineSettingPageData } from "~/modules/settings/utils" const iconName = "i-mgc-user-setting-cute-re" -const priority = 1030 +const priority = 1090 export const loader = defineSettingPageData({ iconName, name: "titles.profile", diff --git a/apps/renderer/src/pages/settings/(settings)/shortcuts.tsx b/apps/renderer/src/pages/settings/(settings)/shortcuts.tsx index 33dc7a48d5..604981f651 100644 --- a/apps/renderer/src/pages/settings/(settings)/shortcuts.tsx +++ b/apps/renderer/src/pages/settings/(settings)/shortcuts.tsx @@ -7,7 +7,7 @@ import { SettingsTitle } from "~/modules/settings/title" import { defineSettingPageData } from "~/modules/settings/utils" const iconName = "i-mgc-hotkey-cute-re" -const priority = 1040 +const priority = 1080 export const loader = defineSettingPageData({ iconName, diff --git a/apps/renderer/src/pages/settings/(settings)/wallet.tsx b/apps/renderer/src/pages/settings/(settings)/wallet.tsx index 65fb014919..00d795c9a5 100644 --- a/apps/renderer/src/pages/settings/(settings)/wallet.tsx +++ b/apps/renderer/src/pages/settings/(settings)/wallet.tsx @@ -2,7 +2,7 @@ import { SettingWallet } from "~/modules/settings/tabs/wallet" import { defineSettingPageData } from "~/modules/settings/utils" const iconName = `i-mgc-power-outline` -const priority = 1050 +const priority = 1040 export const loader = defineSettingPageData({ iconName, diff --git a/apps/renderer/src/queries/feed.ts b/apps/renderer/src/queries/feed.ts index d647ca018f..3419c3ef7e 100644 --- a/apps/renderer/src/queries/feed.ts +++ b/apps/renderer/src/queries/feed.ts @@ -12,13 +12,14 @@ import { feedActions } from "~/store/feed" import { entries } from "./entries" export const feed = { - byId: ({ id, url }: FeedQueryParams) => + byId: ({ id, url, isList }: FeedQueryParams) => defineQuery( ["feed", id, url], async () => feedActions.fetchFeedById({ id, url, + isList, }), { rootKey: ["feed"], @@ -40,11 +41,12 @@ export const feed = { }), } -export const useFeed = ({ id, url }: FeedQueryParams) => +export const useFeed = ({ id, url, isList }: FeedQueryParams) => useAuthQuery( feed.byId({ id, url, + isList, }), { enabled: diff --git a/apps/renderer/src/queries/index.ts b/apps/renderer/src/queries/index.ts index 4560008eb1..abba5ccfa1 100644 --- a/apps/renderer/src/queries/index.ts +++ b/apps/renderer/src/queries/index.ts @@ -5,6 +5,7 @@ import { discover } from "./discover" import { entries } from "./entries" import { feed } from "./feed" import { invitations } from "./invitations" +import { lists } from "./lists" import { subscription } from "./subscriptions" import { wallet } from "./wallet" @@ -18,4 +19,5 @@ export const Queries = { discover, wallet, invitations, + lists, } diff --git a/apps/renderer/src/queries/lists.ts b/apps/renderer/src/queries/lists.ts new file mode 100644 index 0000000000..a995596ba3 --- /dev/null +++ b/apps/renderer/src/queries/lists.ts @@ -0,0 +1,9 @@ +import { defineQuery } from "~/lib/defineQuery" +import { feedActions } from "~/store/feed" + +export const lists = { + list: () => + defineQuery(["lists"], async () => feedActions.fetchOwnedLists(), { + rootKey: ["lists"], + }), +} diff --git a/apps/renderer/src/services/cleaner.spec.ts b/apps/renderer/src/services/cleaner.spec.ts index c2f7228f45..4c211073d7 100644 --- a/apps/renderer/src/services/cleaner.spec.ts +++ b/apps/renderer/src/services/cleaner.spec.ts @@ -1,5 +1,5 @@ // @ts-nocheck -import type { FeedModel } from "@follow/shared/hono" +import type { TargetModel } from "@follow/shared/hono" import { beforeAll, describe, expect, test } from "vitest" import { browserDB } from "~/database" @@ -42,7 +42,7 @@ const subscriptions: SubscriptionFlatModel[] = [ }, ] -const feeds: FeedModel[] = [ +const feeds: TargetModel[] = [ { id: "feed-id-1", }, diff --git a/apps/renderer/src/services/feed.ts b/apps/renderer/src/services/feed.ts index b056111f84..1f4324098e 100644 --- a/apps/renderer/src/services/feed.ts +++ b/apps/renderer/src/services/feed.ts @@ -1,26 +1,26 @@ import { browserDB } from "~/database" -import type { FeedModel } from "~/models/types" +import type { TargetModel } from "~/models/types" import { BaseService } from "./base" import { CleanerService } from "./cleaner" -type FeedModelWithId = FeedModel & { id: string } -class ServiceStatic extends BaseService { +type TargetModelWithId = TargetModel & { id: string } +class ServiceStatic extends BaseService { constructor() { super(browserDB.feeds) } - override async upsertMany(data: FeedModel[]) { + override async upsertMany(data: TargetModel[]) { const filterData = data.filter((d) => d.id) CleanerService.reset(filterData.map((d) => ({ type: "feed", id: d.id! }))) - return this.table.bulkPut(filterData as FeedModelWithId[]) + return this.table.bulkPut(filterData as TargetModelWithId[]) } - override async upsert(data: FeedModel): Promise { + override async upsert(data: TargetModel): Promise { if (!data.id) return null CleanerService.reset([{ type: "feed", id: data.id }]) - return this.table.put(data as FeedModelWithId) + return this.table.put(data as TargetModelWithId) } async bulkDelete(ids: string[]) { diff --git a/apps/renderer/src/store/entry/store.ts b/apps/renderer/src/store/entry/store.ts index de83dbeaf1..86cc967308 100644 --- a/apps/renderer/src/store/entry/store.ts +++ b/apps/renderer/src/store/entry/store.ts @@ -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, FeedModel } from "~/models" +import type { CombinedEntryModel, EntryModel, TargetModel } from "~/models" import { EntryService } from "~/services" import { feedActions } from "../feed" @@ -178,7 +178,7 @@ class EntryActions { } upsertMany(data: CombinedEntryModel[]) { - const feeds = [] as FeedModel[] + const feeds = [] as TargetModel[] const entries = [] as EntryModel[] const entry2Read = {} as Record const entryFeedMap = {} as Record diff --git a/apps/renderer/src/store/feed/hooks.ts b/apps/renderer/src/store/feed/hooks.ts index 22509663f3..ad28fea666 100644 --- a/apps/renderer/src/store/feed/hooks.ts +++ b/apps/renderer/src/store/feed/hooks.ts @@ -4,13 +4,13 @@ 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 { FeedModel } from "~/models" +import type { TargetModel } from "~/models" import { getSubscriptionByFeedId } from "../subscription" import { useFeedStore } from "./store" import type { FeedQueryParams } from "./types" -export const useFeedById = (feedId: Nullable): FeedModel | null => +export const useFeedById = (feedId: Nullable): TargetModel | null => useFeedStore((state) => (feedId ? state.feeds[feedId] : null)) export const useFeedByIdOrUrl = (feed: FeedQueryParams) => @@ -19,14 +19,14 @@ export const useFeedByIdOrUrl = (feed: FeedQueryParams) => return state.feeds[feed.id] } if (feed.url) { - return Object.values(state.feeds).find((f) => f.url === feed.url) || null + return Object.values(state.feeds).find((f) => f.type === "feed" && f.url === feed.url) || null } return null }) export const useFeedByIdSelector = ( feedId: Nullable, - selector: (feed: FeedModel) => T, + selector: (feed: TargetModel) => T, ) => useFeedStore( useShallow((state) => (feedId && state.feeds[feedId] ? selector(state.feeds[feedId]) : null)), diff --git a/apps/renderer/src/store/feed/store.ts b/apps/renderer/src/store/feed/store.ts index 51f3ab9bf1..37c8257b57 100644 --- a/apps/renderer/src/store/feed/store.ts +++ b/apps/renderer/src/store/feed/store.ts @@ -4,7 +4,7 @@ import { nanoid } from "nanoid" import { whoami } from "~/atoms/user" import { runTransactionInScope } from "~/database" import { apiClient } from "~/lib/api-fetch" -import type { FeedModel, UserModel } from "~/models" +import type { TargetModel, UserModel } from "~/models" import { FeedService } from "~/services" import { getSubscriptionByFeedId } from "../subscription" @@ -23,21 +23,25 @@ class FeedActions { set({ feeds: {} }) } - upsertMany(feeds: FeedModel[]) { + upsertMany(feeds: TargetModel[]) { runTransactionInScope(() => { FeedService.upsertMany(feeds) }) set((state) => produce(state, (state) => { for (const feed of feeds) { - if (feed.errorAt && new Date(feed.errorAt).getTime() > Date.now() - 1000 * 60 * 60 * 9) { + if ( + feed.type === "feed" && + feed.errorAt && + new Date(feed.errorAt).getTime() > Date.now() - 1000 * 60 * 60 * 9 + ) { feed.errorAt = null } if (feed.id) { if (feed.owner) { userActions.upsert(feed.owner as UserModel) } - if (feed.tipUsers) { + if (feed.type === "feed" && feed.tipUsers) { userActions.upsert(feed.tipUsers) } @@ -55,12 +59,15 @@ class FeedActions { const nonce = feed["nonce"] || nanoid(8) state.feeds[nonce] = { ...feed, id: nonce } } + if ("feeds" in feed && feed.feeds) { + this.upsertMany(feed.feeds) + } } }), ) } - private patch(feedId: string, patch: Partial) { + private patch(feedId: string, patch: Partial) { set((state) => produce(state, (state) => { const feed = state.feeds[feedId] @@ -96,18 +103,24 @@ class FeedActions { // API Fetcher // - async fetchFeedById({ id, url }: FeedQueryParams) { - const res = await apiClient.feeds.$get({ - query: { - id, - url, - }, - }) + async fetchFeedById({ id, url, isList }: FeedQueryParams) { + const res = isList + ? await apiClient.lists.$get({ + query: { + listId: id!, + }, + }) + : await apiClient.feeds.$get({ + query: { + id, + url, + }, + }) const nonce = nanoid(8) const finalData = { - ...res.data.feed, + ...("list" in res.data ? res.data.list : res.data.feed), } if (!finalData.id) { @@ -120,13 +133,20 @@ class FeedActions { feed: !finalData.id ? { ...finalData, id: nonce } : finalData, } } + + async fetchOwnedLists() { + const res = await apiClient.lists.list.$get() + this.upsertMany(res.data) + + return res.data + } } export const feedActions = new FeedActions() -export const getFeedById = (feedId: string): Nullable => +export const getFeedById = (feedId: string): Nullable => useFeedStore.getState().feeds[feedId] -export const getPreferredTitle = (feed?: FeedModel | null) => { +export const getPreferredTitle = (feed?: TargetModel | null) => { if (!feed?.id) { return feed?.title } diff --git a/apps/renderer/src/store/feed/types.ts b/apps/renderer/src/store/feed/types.ts index 097bdb3c56..d5bde4f2f8 100644 --- a/apps/renderer/src/store/feed/types.ts +++ b/apps/renderer/src/store/feed/types.ts @@ -1,15 +1,15 @@ -import type { FeedModel } from "~/models" +import type { TargetModel } from "~/models" type FeedId = string export interface FeedState { - feeds: Record + feeds: Record } export interface FeedActions { - upsertMany: (feeds: FeedModel[]) => void + upsertMany: (feeds: TargetModel[]) => void clear: () => void - patch: (feedId: FeedId, patch: Partial) => void + patch: (feedId: FeedId, patch: Partial) => void } -export type FeedQueryParams = { id?: string; url?: string } +export type FeedQueryParams = { id?: string; url?: string; isList?: boolean } diff --git a/apps/renderer/src/store/search/index.ts b/apps/renderer/src/store/search/index.ts index 702dc9adcd..ca6d344c5f 100644 --- a/apps/renderer/src/store/search/index.ts +++ b/apps/renderer/src/store/search/index.ts @@ -44,6 +44,7 @@ class SearchActions { const feedsMap = new Map(feeds.map((feed) => [feed.id, feed])) const entriesFuse = this.createFuse(entries, ["title", "content", "description", "id"]) + // @ts-expect-error const feedsFuse = this.createFuse(feeds, ["title", "description", "id", "siteUrl", "url"]) const subscriptionsFuse = this.createFuse(subscriptions, ["title", "category"]) diff --git a/apps/renderer/src/store/search/types.ts b/apps/renderer/src/store/search/types.ts index 3c1200671a..c53a583a63 100644 --- a/apps/renderer/src/store/search/types.ts +++ b/apps/renderer/src/store/search/types.ts @@ -1,4 +1,4 @@ -import type { EntryModel, FeedModel } from "~/models" +import type { EntryModel, TargetModel } from "~/models" import type { SubscriptionFlatModel } from "../subscription" import type { SearchType } from "./constants" @@ -9,7 +9,7 @@ export interface SearchResult exten } export interface SearchState { - feeds: SearchResult[] + feeds: SearchResult[] entries: SearchResult[] subscriptions: SearchResult[] diff --git a/apps/renderer/src/store/subscription/store.ts b/apps/renderer/src/store/subscription/store.ts index 146d0b5209..b2304f2075 100644 --- a/apps/renderer/src/store/subscription/store.ts +++ b/apps/renderer/src/store/subscription/store.ts @@ -35,7 +35,7 @@ function morphResponseData(data: SubscriptionModel[]): SubscriptionFlatModel[] { const result: SubscriptionFlatModel[] = [] for (const subscription of data) { const cloned: SubscriptionFlatModel = { ...subscription } - if (!subscription.category && subscription.feeds) { + if (!subscription.category && "feeds" in subscription && subscription.feeds) { const { siteUrl } = subscription.feeds if (!siteUrl) { cloned.defaultCategory = subscription.feedId @@ -99,7 +99,7 @@ class SubscriptionActions { const transformedData = morphResponseData(res.data) this.upsertMany(transformedData) - feedActions.upsertMany(res.data.map((s) => s.feeds)) + feedActions.upsertMany(res.data.map((s) => ("feeds" in s ? s.feeds : s.lists))) return res.data } @@ -145,30 +145,42 @@ class SubscriptionActions { ) } - async markReadByFeedIds(feedIds: string[]): Promise - async markReadByFeedIds( - feedIds: string[], - view: FeedViewType, - filter?: MarkReadFilter, - ): Promise - async markReadByFeedIds(...args: [string[], FeedViewType?, MarkReadFilter?]) { - const [feedIds, view, filter] = args - - const stableFeedIds = feedIds.concat() + async markReadByFeedIds({ + feedIds, + view, + filter, + listId, + }: { + feedIds?: string[] + view?: FeedViewType + filter?: MarkReadFilter + listId?: string + }) { + const stableFeedIds = feedIds?.concat() || [] doMutationAndTransaction( () => apiClient.reads.all.$post({ json: { - feedIdList: stableFeedIds, + ...(listId + ? { + listId, + } + : { + feedIdList: stableFeedIds, + }), ...filter, }, }), async () => { - for (const feedId of stableFeedIds) { - // We can not process this logic in local, so skip it. and then we will fetch the unread count from server. - !filter && feedUnreadActions.updateByFeedId(feedId, 0) - entryActions.patchManyByFeedId(feedId, { read: true }, filter) + if (listId) { + feedUnreadActions.updateByFeedId(listId, 0) + } else { + for (const feedId of stableFeedIds) { + // We can not process this logic in local, so skip it. and then we will fetch the unread count from server. + !filter && feedUnreadActions.updateByFeedId(feedId, 0) + entryActions.patchManyByFeedId(feedId, { read: true }, filter) + } } if (filter) { feedUnreadActions.fetchUnreadByView(view) @@ -202,7 +214,7 @@ class SubscriptionActions { if (idSet.has(id)) { const subscription = state.data[id] const feed = getFeedById(subscription.feedId) - if (!feed) return + if (!feed || feed.type !== "feed") return const { siteUrl } = feed if (!siteUrl) return const parsed = parse(siteUrl) diff --git a/icons/mgc/compass_cute_fi.svg b/icons/mgc/compass_cute_fi.svg new file mode 100644 index 0000000000..2da00c4910 --- /dev/null +++ b/icons/mgc/compass_cute_fi.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/mgc/edit_cute_re.svg b/icons/mgc/edit_cute_re.svg new file mode 100644 index 0000000000..b14c7754ee --- /dev/null +++ b/icons/mgc/edit_cute_re.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/mgc/inbox_cute_re.svg b/icons/mgc/inbox_cute_re.svg new file mode 100644 index 0000000000..9a80024ac5 --- /dev/null +++ b/icons/mgc/inbox_cute_re.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/mgc/rada_cute_fi.svg b/icons/mgc/rada_cute_fi.svg new file mode 100644 index 0000000000..117e4a5ac5 --- /dev/null +++ b/icons/mgc/rada_cute_fi.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/mgc/rada_cute_re.svg b/icons/mgc/rada_cute_re.svg new file mode 100644 index 0000000000..67ac9fce93 --- /dev/null +++ b/icons/mgc/rada_cute_re.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/locales/app/en.json b/locales/app/en.json index 7a428a9934..ca2ec2a2c2 100644 --- a/locales/app/en.json +++ b/locales/app/en.json @@ -14,6 +14,9 @@ "discover.rss_hub_route": "RSSHub Route", "discover.rss_url": "RSS URL", "discover.select_placeholder": "Select", + "discover.target": "Type", + "discover.target.feeds": "Feeds", + "discover.target.lists": "Lists", "early_access": "Early Access", "entry_actions.copy_link": "Copy link", "entry_actions.failed_to_save_to_eagle": "Failed to save to Eagle.", @@ -81,9 +84,12 @@ "feed_form.category": "Category", "feed_form.category_description": "By default, your follows will be grouped by website.", "feed_form.error_fetching_feed": "Error in fetching feed.", + "feed_form.fee": "Follow fee", + "feed_form.fee_description": "To follow this list, you must pay a fee to the list creator.", "feed_form.feed_not_found": "Feed not found.", "feed_form.feedback": "Feedback", "feed_form.follow": "Follow", + "feed_form.follow_with_fee": "Follow with {{fee}} Power", "feed_form.followed": "🎉 Followed.", "feed_form.private_follow": "Private Follow", "feed_form.private_follow_description": "Whether this follow is publicly visible on your profile page.", @@ -148,12 +154,17 @@ "sidebar.feed_actions.claim_feed": "Claim Feed", "sidebar.feed_actions.copy_feed_id": "Copy feed ID", "sidebar.feed_actions.copy_feed_url": "Copy feed URL", + "sidebar.feed_actions.copy_list_id": "Copy list ID", + "sidebar.feed_actions.copy_list_url": "Copy list URL", "sidebar.feed_actions.edit": "Edit", "sidebar.feed_actions.edit_feed": "Edit feed", "sidebar.feed_actions.feed_owned_by_you": "This feed is owned by you", + "sidebar.feed_actions.list_owned_by_you": "This list is owned by you", "sidebar.feed_actions.mark_all_as_read": "Mark all as read", "sidebar.feed_actions.navigate_to_feed": "Navigate to feed", + "sidebar.feed_actions.navigate_to_list": "Navigate to list", "sidebar.feed_actions.open_feed_in_browser": "Open feed in browser", + "sidebar.feed_actions.open_list_in_browser": "Open list in browser", "sidebar.feed_actions.open_site_in_browser": "Open site in browser", "sidebar.feed_actions.unfollow": "Unfollow", "sidebar.feed_actions.unfollow_feed": "Unfollow feed", @@ -195,9 +206,11 @@ "words.confirm": "Confirm", "words.discover": "Discover", "words.email": "Email", + "words.feeds": "Feeds", "words.import": "Import", "words.items": "Items", "words.language": "Language", + "words.lists": "Lists", "words.load_archived_entries": "Load archived entries", "words.login": "Login", "words.rss": "RSS", diff --git a/locales/external/en.json b/locales/external/en.json index 9fc84c75ce..6292cc41e7 100644 --- a/locales/external/en.json +++ b/locales/external/en.json @@ -1,7 +1,10 @@ { "copied_link": "Copied link to clipboard", + "feed.feeds_one": "feed", + "feed.feeds_other": "feeds", "feed.follower_one": "follower", "feed.follower_other": "followers", + "feed.followsAndFeeds": "{{subscriptionCount}} {{subscriptionNoun}} with {{feedsCount}} {{feedsNoun}} on {{appName}}", "feed.followsAndReads": "{{subscriptionCount}} {{subscriptionNoun}} with {{readCount}} {{readNoun}} on {{appName}}", "feed.read_one": "read", "feed.read_other": "reads", diff --git a/locales/settings/en.json b/locales/settings/en.json index 3f9fa74aa7..f7660b11da 100644 --- a/locales/settings/en.json +++ b/locales/settings/en.json @@ -152,6 +152,32 @@ "invitation.tableHeaders.creationTime": "Creation Time", "invitation.tableHeaders.usedBy": "Used by", "invitation.title": "Invitation Code", + "lists.create": "Create New List", + "lists.created.error": "Failed to create list.", + "lists.created.success": "List created successfully!", + "lists.description": "Description", + "lists.edit": "Edit", + "lists.edit.error": "Failed to edit list.", + "lists.edit.success": "List edited successfully!", + "lists.fee": "Fee", + "lists.fee.description": "The fee others need to pay you to subscribe to this list.", + "lists.feeds": "Feeds", + "lists.feeds.add": "Add", + "lists.feeds.add.error": "Failed to add feed to list.", + "lists.feeds.add.success": "Feed added to list.", + "lists.feeds.delete.error": "Failed to remove feed from list.", + "lists.feeds.delete.success": "Feed removed from list.", + "lists.feeds.id": "Feed ID", + "lists.feeds.manage": "Manage Feeds", + "lists.feeds.owner": "Owner", + "lists.feeds.remove": "Remove", + "lists.feeds.title": "Title", + "lists.image": "Image", + "lists.info": "Lists are collections of feeds that you can share or sell for others to subscribe to. Subscribers will synchronize and access all feeds in the list.", + "lists.noLists": "No lists", + "lists.submit": "Submit", + "lists.title": "Title", + "lists.view": "View", "profile.avatar.label": "Avatar", "profile.handle.description": "Your unique identifier.", "profile.handle.label": "Handle", @@ -168,6 +194,7 @@ "titles.general": "General", "titles.integration": "Integration", "titles.invitations": "Invitations", + "titles.lists": "Lists", "titles.power": "Power", "titles.profile": "Profile", "titles.shortcuts": "Shortcuts", diff --git a/packages/shared/src/hono.ts b/packages/shared/src/hono.ts index ae3a5ec8fe..d94dad9f33 100644 --- a/packages/shared/src/hono.ts +++ b/packages/shared/src/hono.ts @@ -1226,49 +1226,6 @@ declare const feedsOpenAPISchema: zod.ZodObject<{ errorAt: string | null; ownerUserId: string | null; }>; -declare const feedsInputSchema: zod.ZodObject<{ - description: zod.ZodOptional>; - title: zod.ZodOptional>; - id: zod.ZodOptional; - image: zod.ZodOptional>; - url: zod.ZodString; - siteUrl: zod.ZodOptional>; - checkedAt: zod.ZodString; - lastModifiedHeader: zod.ZodOptional>; - etagHeader: zod.ZodOptional>; - ttl: zod.ZodOptional>; - errorMessage: zod.ZodOptional>; - errorAt: zod.ZodOptional>; - ownerUserId: zod.ZodOptional>; -}, zod.UnknownKeysParam, zod.ZodTypeAny, { - url: string; - checkedAt: string; - description?: string | null | undefined; - title?: string | null | undefined; - id?: string | undefined; - image?: string | null | undefined; - siteUrl?: string | null | undefined; - lastModifiedHeader?: string | null | undefined; - etagHeader?: string | null | undefined; - ttl?: number | null | undefined; - errorMessage?: string | null | undefined; - errorAt?: string | null | undefined; - ownerUserId?: string | null | undefined; -}, { - url: string; - checkedAt: string; - description?: string | null | undefined; - title?: string | null | undefined; - id?: string | undefined; - image?: string | null | undefined; - siteUrl?: string | null | undefined; - lastModifiedHeader?: string | null | undefined; - etagHeader?: string | null | undefined; - ttl?: number | null | undefined; - errorMessage?: string | null | undefined; - errorAt?: string | null | undefined; - ownerUserId?: string | null | undefined; -}>; declare const feedsRelations: drizzle_orm.Relations<"feeds", { subscriptions: drizzle_orm.Many<"subscriptions">; entries: drizzle_orm.Many<"entries">; @@ -1276,6 +1233,286 @@ declare const feedsRelations: drizzle_orm.Relations<"feeds", { }>; type FeedModel = InferInsertModel; +declare const subscriptions: drizzle_orm_pg_core.PgTableWithColumns<{ + name: "subscriptions"; + schema: undefined; + columns: { + userId: drizzle_orm_pg_core.PgColumn<{ + name: "user_id"; + tableName: "subscriptions"; + dataType: "string"; + columnType: "PgText"; + data: string; + driverParam: string; + notNull: true; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: [string, ...string[]]; + baseColumn: never; + generated: undefined; + }, {}, {}>; + feedId: drizzle_orm_pg_core.PgColumn<{ + name: "feed_id"; + tableName: "subscriptions"; + dataType: "string"; + columnType: "PgText"; + data: string; + driverParam: string; + notNull: true; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: [string, ...string[]]; + baseColumn: never; + generated: undefined; + }, {}, {}>; + view: drizzle_orm_pg_core.PgColumn<{ + name: "view"; + tableName: "subscriptions"; + dataType: "number"; + columnType: "PgSmallInt"; + data: number; + driverParam: string | number; + notNull: true; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: undefined; + baseColumn: never; + generated: undefined; + }, {}, {}>; + category: drizzle_orm_pg_core.PgColumn<{ + name: "category"; + tableName: "subscriptions"; + dataType: "string"; + columnType: "PgText"; + data: string; + driverParam: string; + notNull: false; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: [string, ...string[]]; + baseColumn: never; + generated: undefined; + }, {}, {}>; + title: drizzle_orm_pg_core.PgColumn<{ + name: "title"; + tableName: "subscriptions"; + dataType: "string"; + columnType: "PgText"; + data: string; + driverParam: string; + notNull: false; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: [string, ...string[]]; + baseColumn: never; + generated: undefined; + }, {}, {}>; + isPrivate: drizzle_orm_pg_core.PgColumn<{ + name: "is_private"; + tableName: "subscriptions"; + dataType: "boolean"; + columnType: "PgBoolean"; + data: boolean; + driverParam: boolean; + notNull: true; + hasDefault: true; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: undefined; + baseColumn: never; + generated: undefined; + }, {}, {}>; + }; + dialect: "pg"; +}>; +declare const subscriptionsOpenAPISchema: zod.ZodObject<{ + userId: zod.ZodString; + feedId: zod.ZodString; + view: zod.ZodNumber; + category: zod.ZodNullable; + title: zod.ZodNullable; + isPrivate: zod.ZodBoolean; +}, zod.UnknownKeysParam, zod.ZodTypeAny, { + title: string | null; + userId: string; + view: number; + category: string | null; + feedId: string; + isPrivate: boolean; +}, { + title: string | null; + userId: string; + view: number; + category: string | null; + feedId: string; + isPrivate: boolean; +}>; +declare const subscriptionsRelations: drizzle_orm.Relations<"subscriptions", { + users: drizzle_orm.One<"user", true>; + feeds: drizzle_orm.One<"feeds", true>; +}>; + +declare const timeline: drizzle_orm_pg_core.PgTableWithColumns<{ + name: "timeline"; + schema: undefined; + columns: { + userId: drizzle_orm_pg_core.PgColumn<{ + name: "user_id"; + tableName: "timeline"; + dataType: "string"; + columnType: "PgText"; + data: string; + driverParam: string; + notNull: true; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: [string, ...string[]]; + baseColumn: never; + generated: undefined; + }, {}, {}>; + feedId: drizzle_orm_pg_core.PgColumn<{ + name: "feedId"; + tableName: "timeline"; + dataType: "string"; + columnType: "PgText"; + data: string; + driverParam: string; + notNull: true; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: [string, ...string[]]; + baseColumn: never; + generated: undefined; + }, {}, {}>; + entryId: drizzle_orm_pg_core.PgColumn<{ + name: "entry_id"; + tableName: "timeline"; + dataType: "string"; + columnType: "PgText"; + data: string; + driverParam: string; + notNull: true; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: [string, ...string[]]; + baseColumn: never; + generated: undefined; + }, {}, {}>; + publishedAt: drizzle_orm_pg_core.PgColumn<{ + name: "published_at"; + tableName: "timeline"; + dataType: "date"; + columnType: "PgTimestamp"; + data: Date; + driverParam: string; + notNull: true; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: undefined; + baseColumn: never; + generated: undefined; + }, {}, {}>; + insertedAt: drizzle_orm_pg_core.PgColumn<{ + name: "inserted_at"; + tableName: "timeline"; + dataType: "date"; + columnType: "PgTimestamp"; + data: Date; + driverParam: string; + notNull: true; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: undefined; + baseColumn: never; + generated: undefined; + }, {}, {}>; + view: drizzle_orm_pg_core.PgColumn<{ + name: "view"; + tableName: "timeline"; + dataType: "number"; + columnType: "PgSmallInt"; + data: number; + driverParam: string | number; + notNull: true; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: undefined; + baseColumn: never; + generated: undefined; + }, {}, {}>; + read: drizzle_orm_pg_core.PgColumn<{ + name: "read"; + tableName: "timeline"; + dataType: "boolean"; + columnType: "PgBoolean"; + data: boolean; + driverParam: boolean; + notNull: false; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: undefined; + baseColumn: never; + generated: undefined; + }, {}, {}>; + }; + dialect: "pg"; +}>; +declare const timelineOpenAPISchema: zod.ZodObject<{ + userId: zod.ZodString; + feedId: zod.ZodString; + entryId: zod.ZodString; + publishedAt: zod.ZodString; + insertedAt: zod.ZodString; + view: zod.ZodNumber; + read: zod.ZodNullable; +}, zod.UnknownKeysParam, zod.ZodTypeAny, { + userId: string; + view: number; + feedId: string; + insertedAt: string; + publishedAt: string; + entryId: string; + read: boolean | null; +}, { + userId: string; + view: number; + feedId: string; + insertedAt: string; + publishedAt: string; + entryId: string; + read: boolean | null; +}>; +declare const timelineRelations: drizzle_orm.Relations<"timeline", { + entries: drizzle_orm.One<"entries", true>; + feeds: drizzle_orm.One<"feeds", true>; + collections: drizzle_orm.One<"collections", true>; +}>; + declare const invitations: drizzle_orm_pg_core.PgTableWithColumns<{ name: "invitations"; schema: undefined; @@ -1328,9 +1565,115 @@ declare const invitations: drizzle_orm_pg_core.PgTableWithColumns<{ baseColumn: never; generated: undefined; }, {}, {}>; - toUserId: drizzle_orm_pg_core.PgColumn<{ - name: "to_user_id"; - tableName: "invitations"; + toUserId: drizzle_orm_pg_core.PgColumn<{ + name: "to_user_id"; + tableName: "invitations"; + dataType: "string"; + columnType: "PgText"; + data: string; + driverParam: string; + notNull: false; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: [string, ...string[]]; + baseColumn: never; + generated: undefined; + }, {}, {}>; + }; + dialect: "pg"; +}>; +declare const invitationsOpenAPISchema: zod.ZodObject<{ + code: zod.ZodString; + createdAt: zod.ZodNullable; + fromUserId: zod.ZodString; + toUserId: zod.ZodNullable; +}, zod.UnknownKeysParam, zod.ZodTypeAny, { + code: string; + createdAt: string | null; + fromUserId: string; + toUserId: string | null; +}, { + code: string; + createdAt: string | null; + fromUserId: string; + toUserId: string | null; +}>; +declare const invitationsRelations: drizzle_orm.Relations<"invitations", { + users: drizzle_orm.One<"user", false>; +}>; + +declare const lists: drizzle_orm_pg_core.PgTableWithColumns<{ + name: "lists"; + schema: undefined; + columns: { + id: drizzle_orm_pg_core.PgColumn<{ + name: "id"; + tableName: "lists"; + dataType: "string"; + columnType: "PgText"; + data: string; + driverParam: string; + notNull: true; + hasDefault: true; + isPrimaryKey: true; + isAutoincrement: false; + hasRuntimeDefault: true; + enumValues: [string, ...string[]]; + baseColumn: never; + generated: undefined; + }, {}, {}>; + feedIds: drizzle_orm_pg_core.PgColumn<{ + name: "feed_ids"; + tableName: "lists"; + dataType: "array"; + columnType: "PgArray"; + data: string[]; + driverParam: string | string[]; + notNull: true; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: [string, ...string[]]; + baseColumn: drizzle_orm.Column<{ + name: "feed_ids"; + tableName: "lists"; + dataType: "string"; + columnType: "PgText"; + data: string; + driverParam: string; + notNull: false; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: [string, ...string[]]; + baseColumn: never; + generated: undefined; + }, object, object>; + generated: undefined; + }, {}, {}>; + title: drizzle_orm_pg_core.PgColumn<{ + name: "title"; + tableName: "lists"; + dataType: "string"; + columnType: "PgText"; + data: string; + driverParam: string; + notNull: true; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: [string, ...string[]]; + baseColumn: never; + generated: undefined; + }, {}, {}>; + description: drizzle_orm_pg_core.PgColumn<{ + name: "description"; + tableName: "lists"; dataType: "string"; columnType: "PgText"; data: string; @@ -1344,57 +1687,14 @@ declare const invitations: drizzle_orm_pg_core.PgTableWithColumns<{ baseColumn: never; generated: undefined; }, {}, {}>; - }; - dialect: "pg"; -}>; -declare const invitationsOpenAPISchema: zod.ZodObject<{ - code: zod.ZodString; - createdAt: zod.ZodNullable; - fromUserId: zod.ZodString; - toUserId: zod.ZodNullable; -}, zod.UnknownKeysParam, zod.ZodTypeAny, { - code: string; - createdAt: string | null; - fromUserId: string; - toUserId: string | null; -}, { - code: string; - createdAt: string | null; - fromUserId: string; - toUserId: string | null; -}>; -declare const invitationsRelations: drizzle_orm.Relations<"invitations", { - users: drizzle_orm.One<"user", false>; -}>; - -declare const settings: drizzle_orm_pg_core.PgTableWithColumns<{ - name: "settings"; - schema: undefined; - columns: { - id: drizzle_orm_pg_core.PgColumn<{ - name: "id"; - tableName: "settings"; - dataType: "string"; - columnType: "PgText"; - data: string; - driverParam: string; - notNull: true; - hasDefault: true; - isPrimaryKey: true; - isAutoincrement: false; - hasRuntimeDefault: true; - enumValues: [string, ...string[]]; - baseColumn: never; - generated: undefined; - }, {}, {}>; - userId: drizzle_orm_pg_core.PgColumn<{ - name: "user_id"; - tableName: "settings"; + image: drizzle_orm_pg_core.PgColumn<{ + name: "image"; + tableName: "lists"; dataType: "string"; columnType: "PgText"; data: string; driverParam: string; - notNull: true; + notNull: false; hasDefault: false; isPrimaryKey: false; isAutoincrement: false; @@ -1403,31 +1703,31 @@ declare const settings: drizzle_orm_pg_core.PgTableWithColumns<{ baseColumn: never; generated: undefined; }, {}, {}>; - tab: drizzle_orm_pg_core.PgColumn<{ - name: "tab"; - tableName: "settings"; - dataType: "string"; - columnType: "PgText"; - data: "general" | "appearance" | "integration"; - driverParam: string; + view: drizzle_orm_pg_core.PgColumn<{ + name: "view"; + tableName: "lists"; + dataType: "number"; + columnType: "PgSmallInt"; + data: number; + driverParam: string | number; notNull: true; hasDefault: false; isPrimaryKey: false; isAutoincrement: false; hasRuntimeDefault: false; - enumValues: ["general", "appearance", "integration"]; + enumValues: undefined; baseColumn: never; generated: undefined; }, {}, {}>; - payload: drizzle_orm_pg_core.PgColumn<{ - name: "payload"; - tableName: "settings"; - dataType: "json"; - columnType: "PgJsonb"; - data: Record; - driverParam: unknown; - notNull: false; - hasDefault: true; + fee: drizzle_orm_pg_core.PgColumn<{ + name: "fee"; + tableName: "lists"; + dataType: "number"; + columnType: "PgInteger"; + data: number; + driverParam: string | number; + notNull: true; + hasDefault: false; isPrimaryKey: false; isAutoincrement: false; hasRuntimeDefault: false; @@ -1435,14 +1735,14 @@ declare const settings: drizzle_orm_pg_core.PgTableWithColumns<{ baseColumn: never; generated: undefined; }, {}, {}>; - updateAt: drizzle_orm_pg_core.PgColumn<{ - name: "update_at"; - tableName: "settings"; + timelineUpdatedAt: drizzle_orm_pg_core.PgColumn<{ + name: "timeline_updated_at"; + tableName: "lists"; dataType: "date"; columnType: "PgTimestamp"; data: Date; driverParam: string; - notNull: false; + notNull: true; hasDefault: false; isPrimaryKey: false; isAutoincrement: false; @@ -1451,33 +1751,68 @@ declare const settings: drizzle_orm_pg_core.PgTableWithColumns<{ baseColumn: never; generated: undefined; }, {}, {}>; - version: drizzle_orm_pg_core.PgColumn<{ - name: "version"; - tableName: "settings"; - dataType: "number"; - columnType: "PgInteger"; - data: number; - driverParam: string | number; + ownerUserId: drizzle_orm_pg_core.PgColumn<{ + name: "owner_user_id"; + tableName: "lists"; + dataType: "string"; + columnType: "PgText"; + data: string; + driverParam: string; notNull: true; - hasDefault: true; + hasDefault: false; isPrimaryKey: false; isAutoincrement: false; hasRuntimeDefault: false; - enumValues: undefined; + enumValues: [string, ...string[]]; baseColumn: never; generated: undefined; }, {}, {}>; }; dialect: "pg"; }>; +declare const listsOpenAPISchema: zod.ZodObject<{ + id: zod.ZodString; + feedIds: zod.ZodArray; + title: zod.ZodString; + description: zod.ZodNullable; + image: zod.ZodNullable; + view: zod.ZodNumber; + fee: zod.ZodNumber; + timelineUpdatedAt: zod.ZodString; + ownerUserId: zod.ZodString; +}, zod.UnknownKeysParam, zod.ZodTypeAny, { + description: string | null; + title: string; + id: string; + image: string | null; + view: number; + ownerUserId: string; + feedIds: string[]; + fee: number; + timelineUpdatedAt: string; +}, { + description: string | null; + title: string; + id: string; + image: string | null; + view: number; + ownerUserId: string; + feedIds: string[]; + fee: number; + timelineUpdatedAt: string; +}>; +declare const listsRelations: drizzle_orm.Relations<"lists", { + owner: drizzle_orm.One<"user", true>; + listsSubscriptions: drizzle_orm.Many<"lists_subscriptions">; +}>; -declare const subscriptions: drizzle_orm_pg_core.PgTableWithColumns<{ - name: "subscriptions"; +declare const listsSubscriptions: drizzle_orm_pg_core.PgTableWithColumns<{ + name: "lists_subscriptions"; schema: undefined; columns: { userId: drizzle_orm_pg_core.PgColumn<{ name: "user_id"; - tableName: "subscriptions"; + tableName: "lists_subscriptions"; dataType: "string"; columnType: "PgText"; data: string; @@ -1491,9 +1826,9 @@ declare const subscriptions: drizzle_orm_pg_core.PgTableWithColumns<{ baseColumn: never; generated: undefined; }, {}, {}>; - feedId: drizzle_orm_pg_core.PgColumn<{ - name: "feed_id"; - tableName: "subscriptions"; + listId: drizzle_orm_pg_core.PgColumn<{ + name: "list_id"; + tableName: "lists_subscriptions"; dataType: "string"; columnType: "PgText"; data: string; @@ -1509,7 +1844,7 @@ declare const subscriptions: drizzle_orm_pg_core.PgTableWithColumns<{ }, {}, {}>; view: drizzle_orm_pg_core.PgColumn<{ name: "view"; - tableName: "subscriptions"; + tableName: "lists_subscriptions"; dataType: "number"; columnType: "PgSmallInt"; data: number; @@ -1523,9 +1858,9 @@ declare const subscriptions: drizzle_orm_pg_core.PgTableWithColumns<{ baseColumn: never; generated: undefined; }, {}, {}>; - category: drizzle_orm_pg_core.PgColumn<{ - name: "category"; - tableName: "subscriptions"; + title: drizzle_orm_pg_core.PgColumn<{ + name: "title"; + tableName: "lists_subscriptions"; dataType: "string"; columnType: "PgText"; data: string; @@ -1539,25 +1874,25 @@ declare const subscriptions: drizzle_orm_pg_core.PgTableWithColumns<{ baseColumn: never; generated: undefined; }, {}, {}>; - title: drizzle_orm_pg_core.PgColumn<{ - name: "title"; - tableName: "subscriptions"; - dataType: "string"; - columnType: "PgText"; - data: string; + lastViewedAt: drizzle_orm_pg_core.PgColumn<{ + name: "last_viewed_at"; + tableName: "lists_subscriptions"; + dataType: "date"; + columnType: "PgTimestamp"; + data: Date; driverParam: string; notNull: false; hasDefault: false; isPrimaryKey: false; isAutoincrement: false; hasRuntimeDefault: false; - enumValues: [string, ...string[]]; + enumValues: undefined; baseColumn: never; generated: undefined; }, {}, {}>; isPrivate: drizzle_orm_pg_core.PgColumn<{ name: "is_private"; - tableName: "subscriptions"; + tableName: "lists_subscriptions"; dataType: "boolean"; columnType: "PgBoolean"; data: boolean; @@ -1574,40 +1909,40 @@ declare const subscriptions: drizzle_orm_pg_core.PgTableWithColumns<{ }; dialect: "pg"; }>; -declare const subscriptionsOpenAPISchema: zod.ZodObject<{ +declare const listsSubscriptionsOpenAPISchema: zod.ZodObject<{ userId: zod.ZodString; - feedId: zod.ZodString; + listId: zod.ZodString; view: zod.ZodNumber; - category: zod.ZodNullable; title: zod.ZodNullable; + lastViewedAt: zod.ZodNullable; isPrivate: zod.ZodBoolean; }, zod.UnknownKeysParam, zod.ZodTypeAny, { title: string | null; userId: string; view: number; - category: string | null; - feedId: string; isPrivate: boolean; + listId: string; + lastViewedAt: string | null; }, { title: string | null; userId: string; view: number; - category: string | null; - feedId: string; isPrivate: boolean; + listId: string; + lastViewedAt: string | null; }>; -declare const subscriptionsRelations: drizzle_orm.Relations<"subscriptions", { +declare const listsSubscriptionsRelations: drizzle_orm.Relations<"lists_subscriptions", { users: drizzle_orm.One<"user", true>; - feeds: drizzle_orm.One<"feeds", true>; + lists: drizzle_orm.One<"lists", true>; }>; -declare const timeline: drizzle_orm_pg_core.PgTableWithColumns<{ - name: "timeline"; +declare const listsTimeline: drizzle_orm_pg_core.PgTableWithColumns<{ + name: "lists_timeline"; schema: undefined; columns: { - userId: drizzle_orm_pg_core.PgColumn<{ - name: "user_id"; - tableName: "timeline"; + listId: drizzle_orm_pg_core.PgColumn<{ + name: "list_id"; + tableName: "lists_timeline"; dataType: "string"; columnType: "PgText"; data: string; @@ -1623,7 +1958,7 @@ declare const timeline: drizzle_orm_pg_core.PgTableWithColumns<{ }, {}, {}>; feedId: drizzle_orm_pg_core.PgColumn<{ name: "feedId"; - tableName: "timeline"; + tableName: "lists_timeline"; dataType: "string"; columnType: "PgText"; data: string; @@ -1639,7 +1974,7 @@ declare const timeline: drizzle_orm_pg_core.PgTableWithColumns<{ }, {}, {}>; entryId: drizzle_orm_pg_core.PgColumn<{ name: "entry_id"; - tableName: "timeline"; + tableName: "lists_timeline"; dataType: "string"; columnType: "PgText"; data: string; @@ -1653,9 +1988,9 @@ declare const timeline: drizzle_orm_pg_core.PgTableWithColumns<{ baseColumn: never; generated: undefined; }, {}, {}>; - publishedAt: drizzle_orm_pg_core.PgColumn<{ - name: "published_at"; - tableName: "timeline"; + insertedAt: drizzle_orm_pg_core.PgColumn<{ + name: "inserted_at"; + tableName: "lists_timeline"; dataType: "date"; columnType: "PgTimestamp"; data: Date; @@ -1669,14 +2004,106 @@ declare const timeline: drizzle_orm_pg_core.PgTableWithColumns<{ baseColumn: never; generated: undefined; }, {}, {}>; - insertedAt: drizzle_orm_pg_core.PgColumn<{ - name: "inserted_at"; - tableName: "timeline"; + }; + dialect: "pg"; +}>; +declare const listsTimelineOpenAPISchema: zod.ZodObject<{ + listId: zod.ZodString; + feedId: zod.ZodString; + entryId: zod.ZodString; + insertedAt: zod.ZodString; +}, zod.UnknownKeysParam, zod.ZodTypeAny, { + feedId: string; + insertedAt: string; + entryId: string; + listId: string; +}, { + feedId: string; + insertedAt: string; + entryId: string; + listId: string; +}>; +declare const listsTimelineRelations: drizzle_orm.Relations<"lists_timeline", { + entries: drizzle_orm.One<"entries", true>; + feeds: drizzle_orm.One<"feeds", true>; +}>; + +declare const settings: drizzle_orm_pg_core.PgTableWithColumns<{ + name: "settings"; + schema: undefined; + columns: { + id: drizzle_orm_pg_core.PgColumn<{ + name: "id"; + tableName: "settings"; + dataType: "string"; + columnType: "PgText"; + data: string; + driverParam: string; + notNull: true; + hasDefault: true; + isPrimaryKey: true; + isAutoincrement: false; + hasRuntimeDefault: true; + enumValues: [string, ...string[]]; + baseColumn: never; + generated: undefined; + }, {}, {}>; + userId: drizzle_orm_pg_core.PgColumn<{ + name: "user_id"; + tableName: "settings"; + dataType: "string"; + columnType: "PgText"; + data: string; + driverParam: string; + notNull: true; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: [string, ...string[]]; + baseColumn: never; + generated: undefined; + }, {}, {}>; + tab: drizzle_orm_pg_core.PgColumn<{ + name: "tab"; + tableName: "settings"; + dataType: "string"; + columnType: "PgText"; + data: "general" | "appearance" | "integration"; + driverParam: string; + notNull: true; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: ["general", "appearance", "integration"]; + baseColumn: never; + generated: undefined; + }, {}, {}>; + payload: drizzle_orm_pg_core.PgColumn<{ + name: "payload"; + tableName: "settings"; + dataType: "json"; + columnType: "PgJsonb"; + data: Record; + driverParam: unknown; + notNull: false; + hasDefault: true; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: undefined; + baseColumn: never; + generated: undefined; + }, {}, {}>; + updateAt: drizzle_orm_pg_core.PgColumn<{ + name: "update_at"; + tableName: "settings"; dataType: "date"; columnType: "PgTimestamp"; data: Date; driverParam: string; - notNull: true; + notNull: false; hasDefault: false; isPrimaryKey: false; isAutoincrement: false; @@ -1685,31 +2112,15 @@ declare const timeline: drizzle_orm_pg_core.PgTableWithColumns<{ baseColumn: never; generated: undefined; }, {}, {}>; - view: drizzle_orm_pg_core.PgColumn<{ - name: "view"; - tableName: "timeline"; + version: drizzle_orm_pg_core.PgColumn<{ + name: "version"; + tableName: "settings"; dataType: "number"; - columnType: "PgSmallInt"; + columnType: "PgInteger"; data: number; driverParam: string | number; notNull: true; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: undefined; - baseColumn: never; - generated: undefined; - }, {}, {}>; - read: drizzle_orm_pg_core.PgColumn<{ - name: "read"; - tableName: "timeline"; - dataType: "boolean"; - columnType: "PgBoolean"; - data: boolean; - driverParam: boolean; - notNull: false; - hasDefault: false; + hasDefault: true; isPrimaryKey: false; isAutoincrement: false; hasRuntimeDefault: false; @@ -1720,36 +2131,6 @@ declare const timeline: drizzle_orm_pg_core.PgTableWithColumns<{ }; dialect: "pg"; }>; -declare const timelineOpenAPISchema: zod.ZodObject<{ - userId: zod.ZodString; - feedId: zod.ZodString; - entryId: zod.ZodString; - publishedAt: zod.ZodString; - insertedAt: zod.ZodString; - view: zod.ZodNumber; - read: zod.ZodNullable; -}, zod.UnknownKeysParam, zod.ZodTypeAny, { - userId: string; - view: number; - feedId: string; - insertedAt: string; - publishedAt: string; - entryId: string; - read: boolean | null; -}, { - userId: string; - view: number; - feedId: string; - insertedAt: string; - publishedAt: string; - entryId: string; - read: boolean | null; -}>; -declare const timelineRelations: drizzle_orm.Relations<"timeline", { - entries: drizzle_orm.One<"entries", true>; - feeds: drizzle_orm.One<"feeds", true>; - collections: drizzle_orm.One<"collections", true>; -}>; declare const users: drizzle_orm_pg_core.PgTableWithColumns<{ name: "user"; @@ -2188,6 +2569,7 @@ declare const verificationTokens: drizzle_orm_pg_core.PgTableWithColumns<{ }>; declare const usersRelations: drizzle_orm.Relations<"user", { subscriptions: drizzle_orm.Many<"subscriptions">; + listsSubscriptions: drizzle_orm.Many<"lists_subscriptions">; collections: drizzle_orm.Many<"collections">; actions: drizzle_orm.One<"actions", true>; wallets: drizzle_orm.One<"wallets", true>; @@ -2412,6 +2794,22 @@ declare const transactions: drizzle_orm_pg_core.PgTableWithColumns<{ baseColumn: never; generated: undefined; }, {}, {}>; + toListId: drizzle_orm_pg_core.PgColumn<{ + name: "to_list_id"; + tableName: "transactions"; + dataType: "string"; + columnType: "PgText"; + data: string; + driverParam: string; + notNull: false; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: [string, ...string[]]; + baseColumn: never; + generated: undefined; + }, {}, {}>; toEntryId: drizzle_orm_pg_core.PgColumn<{ name: "to_entry_id"; tableName: "transactions"; @@ -2485,6 +2883,7 @@ declare const transactionsOpenAPISchema: zod.ZodObject<{ fromUserId: zod.ZodNullable; toUserId: zod.ZodNullable; toFeedId: zod.ZodNullable; + toListId: zod.ZodNullable; toEntryId: zod.ZodNullable; powerToken: zod.ZodString; createdAt: zod.ZodString; @@ -2496,6 +2895,7 @@ declare const transactionsOpenAPISchema: zod.ZodObject<{ toUserId: string | null; hash: string; toFeedId: string | null; + toListId: string | null; toEntryId: string | null; powerToken: string; comment: string | null; @@ -2506,6 +2906,7 @@ declare const transactionsOpenAPISchema: zod.ZodObject<{ toUserId: string | null; hash: string; toFeedId: string | null; + toListId: string | null; toEntryId: string | null; powerToken: string; comment: string | null; @@ -2554,23 +2955,305 @@ declare const feedPowerTokens: drizzle_orm_pg_core.PgTableWithColumns<{ generated: undefined; }, {}, {}>; }; - dialect: "pg"; -}>; -declare const feedPowerTokensOpenAPISchema: zod.ZodObject<{ - feedId: zod.ZodString; - powerToken: zod.ZodString; -}, zod.UnknownKeysParam, zod.ZodTypeAny, { - feedId: string; - powerToken: string; -}, { - feedId: string; - powerToken: string; -}>; -declare const feedPowerTokensRelations: drizzle_orm.Relations<"feedPowerTokens", { - feed: drizzle_orm.One<"feeds", true>; -}>; - -declare const _routes: hono_hono_base.HonoBase; +declare const feedPowerTokensOpenAPISchema: zod.ZodObject<{ + feedId: zod.ZodString; + powerToken: zod.ZodString; +}, zod.UnknownKeysParam, zod.ZodTypeAny, { + feedId: string; + powerToken: string; +}, { + feedId: string; + powerToken: string; +}>; +declare const feedPowerTokensRelations: drizzle_orm.Relations<"feedPowerTokens", { + feed: drizzle_orm.One<"feeds", true>; +}>; + +declare const _routes: hono_hono_base.HonoBase; type AppType = typeof _routes; -export { type ActionsModel, type AppType, type AttachmentsModel, type EntriesModel, type EntryReadHistoriesModel, type FeedModel, type MediaModel, type SettingsModel, accounts, actions, actionsItemOpenAPISchema, actionsOpenAPISchema, actionsRelations, collections, collectionsOpenAPISchema, collectionsRelations, entries, entriesOpenAPISchema, entriesRelations, entryReadHistories, entryReadHistoriesOpenAPISchema, entryReadHistoriesRelations, feedPowerTokens, feedPowerTokensOpenAPISchema, feedPowerTokensRelations, feeds, feedsInputSchema, feedsOpenAPISchema, feedsRelations, invitations, invitationsOpenAPISchema, invitationsRelations, languageSchema, sessions, settings, subscriptions, subscriptionsOpenAPISchema, subscriptionsRelations, timeline, timelineOpenAPISchema, timelineRelations, transactionType, transactions, transactionsOpenAPISchema, transactionsRelations, users, usersOpenApiSchema, usersRelations, verificationTokens, wallets, walletsOpenAPISchema, walletsRelations }; +export { type ActionsModel, type AppType, type AttachmentsModel, type EntriesModel, type EntryReadHistoriesModel, type FeedModel, type MediaModel, type SettingsModel, accounts, actions, actionsItemOpenAPISchema, actionsOpenAPISchema, actionsRelations, collections, collectionsOpenAPISchema, collectionsRelations, entries, entriesOpenAPISchema, entriesRelations, entryReadHistories, entryReadHistoriesOpenAPISchema, entryReadHistoriesRelations, feedPowerTokens, feedPowerTokensOpenAPISchema, feedPowerTokensRelations, feeds, feedsOpenAPISchema, feedsRelations, invitations, invitationsOpenAPISchema, invitationsRelations, languageSchema, lists, listsOpenAPISchema, listsRelations, listsSubscriptions, listsSubscriptionsOpenAPISchema, listsSubscriptionsRelations, listsTimeline, listsTimelineOpenAPISchema, listsTimelineRelations, sessions, settings, subscriptions, subscriptionsOpenAPISchema, subscriptionsRelations, timeline, timelineOpenAPISchema, timelineRelations, transactionType, transactions, transactionsOpenAPISchema, transactionsRelations, users, usersOpenApiSchema, usersRelations, verificationTokens, wallets, walletsOpenAPISchema, walletsRelations };