+
-
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) => (
)}
/>
- (
-
-
- {t("feed_form.category")}
- {t("feed_form.category_description")}
-
-
+ {!isList && (
+ (
+
-
{
- if (suggestion) {
- field.onChange(suggestion.value)
- }
- }}
- />
+ {t("feed_form.category")}
+ {t("feed_form.category_description")}
-
-
-
- )}
- />
+
+
+
{
+ if (suggestion) {
+ field.onChange(suggestion.value)
+ }
+ }}
+ />
+
+
+
+
+ )}
+ />
+ )}
)}
/>
-
+ {isList && !!feed.fee && !isSubscribed && (
+
+
{t("feed_form.fee")}
+
{t("feed_form.fee_description")}
+
+ {feed.fee}
+
+
+
+ )}
{isSubscribed && (
diff --git a/apps/renderer/src/modules/discover/form.tsx b/apps/renderer/src/modules/discover/form.tsx
index cc2d17db55..0b09573397 100644
--- a/apps/renderer/src/modules/discover/form.tsx
+++ b/apps/renderer/src/modules/discover/form.tsx
@@ -23,6 +23,8 @@ import {
import { Input } from "~/components/ui/input"
import { Media } from "~/components/ui/media"
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
+import { Radio } from "~/components/ui/radio-group"
+import { RadioGroup } from "~/components/ui/radio-group/RadioGroup"
import { apiClient } from "~/lib/api-fetch"
import type { FeedViewType } from "~/lib/enum"
@@ -31,6 +33,7 @@ import { FeedForm } from "./feed-form"
const formSchema = z.object({
keyword: z.string().min(1),
+ target: z.enum(["feeds", "lists"]),
})
const info: Record<
@@ -66,16 +69,18 @@ export function DiscoverForm({ type }: { type: string }) {
resolver: zodResolver(formSchema),
defaultValues: {
keyword: defaultValue || "",
+ target: "feeds",
},
})
const { t } = useTranslation()
const jotaiStore = useStore()
const mutation = useMutation({
- mutationFn: async (keyword: string) => {
+ mutationFn: async ({ keyword, target }: { keyword: string; target: "feeds" | "lists" }) => {
const { data } = await apiClient.discover.$post({
json: {
keyword,
+ target,
},
})
@@ -107,7 +112,7 @@ export function DiscoverForm({ type }: { type: string }) {
),
})
} else {
- mutation.mutate(values.keyword)
+ mutation.mutate(values)
}
}
@@ -139,7 +144,9 @@ export function DiscoverForm({ type }: { type: string }) {
jotaiStore.set(
discoverSearchDataAtom,
produce(currentData, (draft) => {
- const sub = draft.find((i) => i.feed.id === item.feed.id)
+ const sub = draft.find(
+ (i) => i.feed?.id === item.feed?.id || i.list?.id === item.list?.id,
+ )
if (!sub) return
sub.isSubscribed = true
sub.subscriptionCount = -~(sub.subscriptionCount as number)
@@ -156,7 +163,9 @@ export function DiscoverForm({ type }: { type: string }) {
jotaiStore.set(
discoverSearchDataAtom,
produce(currentData, (draft) => {
- const sub = draft.find((i) => i.feed.id === item.feed.id)
+ const sub = draft.find(
+ (i) => i.feed?.id === item.feed?.id || i.list?.id === item.list?.id,
+ )
if (!sub) return
sub.isSubscribed = false
sub.subscriptionCount = Number.isNaN(sub.subscriptionCount)
@@ -167,6 +176,14 @@ export function DiscoverForm({ type }: { type: string }) {
},
[discoverSearchDataAtom, jotaiStore],
)
+
+ const handleTargetChange = useCallback(
+ (value: string) => {
+ form.setValue("target", value as "feeds" | "lists")
+ },
+ [form],
+ )
+
return (
<>
{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(() => {