diff --git a/locales/app/en.json b/locales/app/en.json index 6498ff6d6..e788f0950 100644 --- a/locales/app/en.json +++ b/locales/app/en.json @@ -46,6 +46,8 @@ "entry_content.readability_notice": "This content is provided by Readability. If you find typographical anomalies, please go to the source site to view the original content.", "entry_content.render_error": "Render error:", "entry_content.report_issue": "Report issue", + "entry_content.support_amount": "{{amount}} people supported this feed's creator.", + "entry_content.support_creator": "Support Creator", "entry_content.web_app_notice": "Maybe web app doesn't support this content type. But you can download the desktop app.", "entry_list.zero_unread": "Zero Unread", "entry_list_header.daily_report": "Daily Report", diff --git a/src/hono.ts b/src/hono.ts index 0a64aa422..c4cfc0c01 100644 --- a/src/hono.ts +++ b/src/hono.ts @@ -2614,23 +2614,23 @@ declare const _routes: hono_hono_base.HonoBase { @@ -175,8 +180,14 @@ export function UserAvatar({ "flex h-20 items-center justify-center gap-2 px-5 py-2 font-medium text-zinc-600 dark:text-zinc-300", className, )} + onClick={enableModal ? () => presentUserProfile(userId) : undefined} > - + , "email"> +export type UserModel = Omit< + Omit, "email">, + "createdAt" +> export type ExtractBizResponse any> = Exclude< Awaited>, @@ -17,8 +20,13 @@ export type ActiveList = { export type FeedResponse = SubscriptionResponse[number]["feeds"] +export type TransactionModel = ExtractBizResponse< + typeof apiClient.wallets.transactions.$get +>["data"][number] + export type FeedModel = ExtractBizResponse["data"]["feed"] & { - owner?: Omit | null + owner?: UserModel | null + tipUsers?: UserModel[] | null } export type SubscriptionResponse = Array< diff --git a/src/renderer/src/modules/entry-content/index.tsx b/src/renderer/src/modules/entry-content/index.tsx index 2a99b2e1b..3287e5c74 100644 --- a/src/renderer/src/modules/entry-content/index.tsx +++ b/src/renderer/src/modules/entry-content/index.tsx @@ -11,11 +11,14 @@ import { useWhoami } from "@renderer/atoms/user" import { m } from "@renderer/components/common/Motion" import { ShadowDOM } from "@renderer/components/common/ShadowDOM" import { AutoResizeHeight } from "@renderer/components/ui/auto-resize-height" +import { Button } from "@renderer/components/ui/button" +import { Divider } from "@renderer/components/ui/divider" import { HTML } from "@renderer/components/ui/markdown" import { Toc } from "@renderer/components/ui/markdown/components/Toc" import { useInPeekModal } from "@renderer/components/ui/modal/inspire/PeekModal" import { RootPortal } from "@renderer/components/ui/portal" import { ScrollArea } from "@renderer/components/ui/scroll-area" +import { UserAvatar } from "@renderer/components/user-button" import { isWebBuild, ROUTE_FEED_PENDING } from "@renderer/constants" import { shortcuts } from "@renderer/constants/shortcuts" import { useEntryReadabilityToggle } from "@renderer/hooks/biz/useEntryActions" @@ -44,6 +47,7 @@ import { useTranslation } from "react-i18next" import { LoadingWithIcon } from "../../components/ui/loading" import { EntryPlaceholderDaily } from "../ai/ai-daily/EntryPlaceholderDaily" import { EntryTranslation } from "../entry-column/translation" +import { useTipModal } from "../wallet/hooks" import { setEntryContentScrollToTop, setEntryTitleMeta } from "./atoms" import { EntryPlaceholderLogo } from "./components/EntryPlaceholderLogo" import { EntryHeader } from "./header" @@ -172,6 +176,12 @@ export const EntryContentRender: Component<{ entryId: string }> = ({ entryId, cl return href }, [entry?.entries.url, feed?.siteUrl]) + const openTipModal = useTipModal({ + userId: feed?.ownerUserId ?? undefined, + feedId: entry?.feedId, + entryId, + }) + if (!entry) return null const content = entry?.entries.content ?? data?.entries.content @@ -289,6 +299,45 @@ export const EntryContentRender: Component<{ entryId: string }> = ({ entryId, cl )} )} + + {feed?.ownerUserId && ( + <> + + +
+ + + {feed.tipUsers?.length && ( + <> +
+ {t("entry_content.support_amount", { amount: feed.tipUsers.length })} +
+
+ {feed.tipUsers?.map((user) => ( +
+ +
+ ))} +
+ + )} +
+ + )} diff --git a/src/renderer/src/modules/wallet/tip-modal.tsx b/src/renderer/src/modules/wallet/tip-modal.tsx index 938173ebe..e23d0a99c 100644 --- a/src/renderer/src/modules/wallet/tip-modal.tsx +++ b/src/renderer/src/modules/wallet/tip-modal.tsx @@ -114,7 +114,11 @@ const TipModalContent_: FC<{ {userId ? ( <>

{t("tip_modal.feed_owner")}

- + ) : ( <> diff --git a/src/renderer/src/store/feed/store.ts b/src/renderer/src/store/feed/store.ts index 22bcf13d5..05be579a0 100644 --- a/src/renderer/src/store/feed/store.ts +++ b/src/renderer/src/store/feed/store.ts @@ -1,12 +1,13 @@ import { whoami } from "@renderer/atoms/user" import { runTransactionInScope } from "@renderer/database" import { apiClient } from "@renderer/lib/api-fetch" -import type { FeedModel } from "@renderer/models" +import type { FeedModel, UserModel } from "@renderer/models" import { FeedService } from "@renderer/services" import { produce } from "immer" import { nanoid } from "nanoid" import { getSubscriptionByFeedId } from "../subscription" +import { userActions } from "../user" import { createZustandStore } from "../utils/helper" import type { FeedQueryParams, FeedState } from "./types" @@ -32,9 +33,19 @@ class FeedActions { feed.errorAt = null } if (feed.id) { - // Not all API return the owner, so merging is needed here. - if (state.feeds[feed.id]?.owner && !feed.owner) { - feed.owner = state.feeds[feed.id]?.owner + // Not all API return these fields, so merging is needed here. + const optionalFields = ["owner", "tipUsers"] as const + optionalFields.forEach((field) => { + if (state.feeds[feed.id!]?.[field] && !(field in feed)) { + ;(feed as any)[field] = state.feeds[feed.id!]?.[field] + } + }) + + if (feed.owner) { + userActions.upsert(feed.owner as UserModel) + } + if (feed.tipUsers) { + userActions.upsert(feed.tipUsers) } state.feeds[feed.id] = feed diff --git a/src/renderer/src/store/user/store.ts b/src/renderer/src/store/user/store.ts index abde637ca..6bcbd5ae8 100644 --- a/src/renderer/src/store/user/store.ts +++ b/src/renderer/src/store/user/store.ts @@ -17,7 +17,9 @@ class UserActions { set((state) => produce(state, (state) => { for (const u of user) { - state.users[u.id] = u + if (u?.id) { + state.users[u.id] = u + } } }), )