feat: display a tip button and tip users at the bottom of the entry content.
This commit is contained in:
parent
0a61fa37c3
commit
971f81d25d
|
|
@ -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",
|
||||
|
|
|
|||
18
src/hono.ts
18
src/hono.ts
|
|
@ -2614,23 +2614,23 @@ declare const _routes: hono_hono_base.HonoBase<Env, {
|
|||
toEntryId: string | null;
|
||||
powerToken: string;
|
||||
comment: string | null;
|
||||
fromUser: {
|
||||
fromUser?: {
|
||||
name: string | null;
|
||||
id: string;
|
||||
emailVerified: string | null;
|
||||
image: string | null;
|
||||
handle: string | null;
|
||||
createdAt: string;
|
||||
} | null;
|
||||
toUser: {
|
||||
} | null | undefined;
|
||||
toUser?: {
|
||||
name: string | null;
|
||||
id: string;
|
||||
emailVerified: string | null;
|
||||
image: string | null;
|
||||
handle: string | null;
|
||||
createdAt: string;
|
||||
} | null;
|
||||
toFeed: {
|
||||
} | null | undefined;
|
||||
toFeed?: {
|
||||
description: string | null;
|
||||
title: string | null;
|
||||
id: string;
|
||||
|
|
@ -2644,7 +2644,7 @@ declare const _routes: hono_hono_base.HonoBase<Env, {
|
|||
errorMessage: string | null;
|
||||
errorAt: string | null;
|
||||
ownerUserId: string | null;
|
||||
} | null;
|
||||
} | null | undefined;
|
||||
}[];
|
||||
};
|
||||
outputFormat: "json" | "text";
|
||||
|
|
@ -3383,6 +3383,12 @@ declare const _routes: hono_hono_base.HonoBase<Env, {
|
|||
errorMessage: string | null;
|
||||
errorAt: string | null;
|
||||
ownerUserId: string | null;
|
||||
tipUsers: {
|
||||
name: string | null;
|
||||
id: string;
|
||||
image: string | null;
|
||||
handle: string | null;
|
||||
}[];
|
||||
};
|
||||
users: {
|
||||
[x: string]: {
|
||||
|
|
|
|||
|
|
@ -141,17 +141,22 @@ ProfileButton.displayName = "ProfileButton"
|
|||
|
||||
export function UserAvatar({
|
||||
className,
|
||||
avatarClassName,
|
||||
hideName,
|
||||
userId,
|
||||
enableModal,
|
||||
...props
|
||||
}: {
|
||||
className?: string
|
||||
avatarClassName?: string
|
||||
hideName?: boolean
|
||||
userId?: string
|
||||
enableModal?: boolean
|
||||
} & LoginProps) {
|
||||
const { session, status } = useSession({
|
||||
enabled: !userId,
|
||||
})
|
||||
const presentUserProfile = usePresentUserProfileModal("drawer")
|
||||
|
||||
const profile = useAuthQuery(
|
||||
defineQuery(["profiles", userId], async () => {
|
||||
|
|
@ -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}
|
||||
>
|
||||
<Avatar className="aspect-square h-full w-auto overflow-hidden rounded-full border bg-stone-300">
|
||||
<Avatar
|
||||
className={cn(
|
||||
"aspect-square h-full w-auto overflow-hidden rounded-full border bg-stone-300",
|
||||
avatarClassName,
|
||||
)}
|
||||
>
|
||||
<AvatarImage
|
||||
className="duration-200 animate-in fade-in-0"
|
||||
src={(profile.data || session?.user)?.image || undefined}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ import type { apiClient } from "@renderer/lib/api-fetch"
|
|||
import type { languageSchema, users } from "src/hono"
|
||||
import type { z } from "zod"
|
||||
|
||||
export type UserModel = Omit<Omit<typeof users.$inferSelect, "emailVerified">, "email">
|
||||
export type UserModel = Omit<
|
||||
Omit<Omit<typeof users.$inferSelect, "emailVerified">, "email">,
|
||||
"createdAt"
|
||||
>
|
||||
|
||||
export type ExtractBizResponse<T extends (...args: any[]) => any> = Exclude<
|
||||
Awaited<ReturnType<T>>,
|
||||
|
|
@ -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<typeof apiClient.feeds.$get>["data"]["feed"] & {
|
||||
owner?: Omit<UserModel, "createdAt"> | null
|
||||
owner?: UserModel | null
|
||||
tipUsers?: UserModel[] | null
|
||||
}
|
||||
|
||||
export type SubscriptionResponse = Array<
|
||||
|
|
|
|||
|
|
@ -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
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{feed?.ownerUserId && (
|
||||
<>
|
||||
<Divider />
|
||||
|
||||
<div className="my-32 flex flex-col items-center gap-8">
|
||||
<UserAvatar
|
||||
className="w-40 flex-col gap-3 p-0"
|
||||
avatarClassName="size-12"
|
||||
userId={feed.ownerUserId}
|
||||
enableModal={true}
|
||||
/>
|
||||
<Button className="text-base" onClick={() => openTipModal()}>
|
||||
<i className="i-mgc-power-outline mr-1.5 text-lg" />
|
||||
{t("entry_content.support_creator")}
|
||||
</Button>
|
||||
{feed.tipUsers?.length && (
|
||||
<>
|
||||
<div className="text-sm text-zinc-500">
|
||||
{t("entry_content.support_amount", { amount: feed.tipUsers.length })}
|
||||
</div>
|
||||
<div className="flex w-fit max-w-80 flex-wrap gap-4">
|
||||
{feed.tipUsers?.map((user) => (
|
||||
<div key={user.id} className="size-8">
|
||||
<UserAvatar
|
||||
className="h-auto p-0"
|
||||
avatarClassName="size-8"
|
||||
userId={user?.id}
|
||||
enableModal={true}
|
||||
hideName={true}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</article>
|
||||
</div>
|
||||
</ScrollArea.ScrollArea>
|
||||
|
|
|
|||
|
|
@ -114,7 +114,11 @@ const TipModalContent_: FC<{
|
|||
{userId ? (
|
||||
<>
|
||||
<p className="text-sm font-medium">{t("tip_modal.feed_owner")}</p>
|
||||
<UserAvatar className="h-8 justify-start bg-transparent p-0" userId={userId} />
|
||||
<UserAvatar
|
||||
className="h-8 justify-start bg-transparent p-0"
|
||||
userId={userId}
|
||||
enableModal={true}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue