feat: date item in entry column (#199)

* feat: date item in entry column

* chore: styles and mutation transaction wrapper

Signed-off-by: Innei <i@innei.in>

---------

Signed-off-by: Innei <i@innei.in>
Co-authored-by: Innei <i@innei.in>
This commit is contained in:
DIYgod 2024-08-04 13:48:12 +08:00 committed by GitHub
parent 0f8309b203
commit 9d5a81112e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 380 additions and 191 deletions

View File

@ -135,6 +135,7 @@
"@electron-forge/plugin-fuses": "7.4.0",
"@electron-forge/publisher-github": "7.4.0",
"@electron-toolkit/tsconfig": "^1.0.1",
"@hono/node-server": "1.12.0",
"@iconify-json/mingcute": "1.1.18",
"@pengx17/electron-forge-maker-appimage": "1.2.1",
"@tailwindcss/container-queries": "0.1.1",

View File

@ -329,6 +329,9 @@ importers:
'@electron-toolkit/tsconfig':
specifier: ^1.0.1
version: 1.0.1(@types/node@20.14.12)
'@hono/node-server':
specifier: 1.12.0
version: 1.12.0
'@iconify-json/mingcute':
specifier: 1.1.18
version: 1.1.18
@ -1016,6 +1019,10 @@ packages:
hono: '>=3.*'
react: '>=18'
'@hono/node-server@1.12.0':
resolution: {integrity: sha512-e6oHjNiErRxsZRZBmc2KucuvY3btlO/XPncIpP2X75bRdTilF9GLjm3NHvKKunpJbbJJj31/FoPTksTf8djAVw==}
engines: {node: '>=18.14.1'}
'@hookform/resolvers@3.9.0':
resolution: {integrity: sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==}
peerDependencies:
@ -7739,6 +7746,8 @@ snapshots:
hono: 4.4.7(patch_hash=ycbk46disqruhfjducp47b5fl4)
react: 18.3.1
'@hono/node-server@1.12.0': {}
'@hookform/resolvers@3.9.0(react-hook-form@7.52.1(react@18.3.1))':
dependencies:
react-hook-form: 7.52.1(react@18.3.1)

View File

@ -1,13 +1,17 @@
import * as hono_hono_base from 'hono/hono-base';
import * as hono_utils_http_status from 'hono/utils/http-status';
import * as hono from 'hono';
import * as hono_types from 'hono/types';
import { HttpBindings } from '@hono/node-server';
import * as drizzle_orm from 'drizzle-orm';
import { InferInsertModel } from 'drizzle-orm';
import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
import * as zod from 'zod';
import { z } from 'zod';
type Env = {
Bindings: HttpBindings;
};
declare const actions: drizzle_orm_pg_core.PgTableWithColumns<{
name: "actions";
schema: undefined;
@ -2424,7 +2428,7 @@ declare const feedPowerTokensRelations: drizzle_orm.Relations<"feedPowerTokens",
feed: drizzle_orm.One<"feeds", true>;
}>;
declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
declare const _routes: hono_hono_base.HonoBase<Env, {
"/metrics": {
$get: {
input: {
@ -2433,6 +2437,7 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
};
};
output: {
code: 0;
data: {
data: number[];
count: number;
@ -2442,7 +2447,6 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
prevCount: number;
};
};
code: 0;
};
outputFormat: "json";
status: 200;
@ -2457,6 +2461,7 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
};
};
output: {
code: 0;
data: {
name: string | null;
id: string;
@ -2466,7 +2471,6 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
handle: string | null;
createdAt: string;
};
code: 0;
};
outputFormat: "json";
status: 200;
@ -2477,8 +2481,8 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
$post: {
input: {};
output: {
data: string;
code: 0;
data: string;
};
outputFormat: "json";
status: 200;
@ -2509,10 +2513,10 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
};
};
output: {
code: 0;
data: {
transactionHash: string;
};
code: 0;
};
outputFormat: "json";
status: 200;
@ -2532,6 +2536,7 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
};
};
output: {
code: 0;
data: {
type: "tip" | "mint" | "burn" | "withdraw";
createdAt: string;
@ -2575,7 +2580,6 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
ownerUserId: string | null;
} | null;
}[];
code: 0;
};
outputFormat: "json";
status: 200;
@ -2585,10 +2589,10 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
$post: {
input: {};
output: {
code: 0;
data: {
transactionHash: string;
};
code: 0;
};
outputFormat: "json";
status: 200;
@ -2598,10 +2602,10 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
$get: {
input: {};
output: {
code: 0;
data: {
ttl: number;
};
code: 0;
};
outputFormat: "json";
status: 200;
@ -2615,12 +2619,12 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
};
};
output: {
code: 0;
data: {
title: string | null;
id: string;
powerToken: string;
}[];
code: 0;
};
outputFormat: "json";
status: 200;
@ -2634,10 +2638,10 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
};
};
output: {
code: 0;
data: {
transactionHash: string;
};
code: 0;
};
outputFormat: "json";
status: 200;
@ -2652,6 +2656,7 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
};
};
output: {
code: 0;
data: {
userId: string;
createdAt: string;
@ -2660,7 +2665,6 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
dailyPowerToken: string;
cashablePowerToken: string;
}[];
code: 0;
};
outputFormat: "json";
status: 200;
@ -2668,6 +2672,7 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
$post: {
input: {};
output: {
code: 0;
data: {
userId: string;
createdAt: string;
@ -2676,7 +2681,6 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
dailyPowerToken: string;
cashablePowerToken: string;
};
code: 0;
};
outputFormat: "json";
status: 200;
@ -2819,10 +2823,10 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
};
};
output: {
code: 0;
data: {
[x: string]: number;
};
code: 0;
};
outputFormat: "json";
status: 200;
@ -2835,6 +2839,8 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
view?: number | undefined;
feedId?: string | undefined;
feedIdList?: string[] | undefined;
startTime?: number | undefined;
endTime?: number | undefined;
};
};
output: {
@ -2848,10 +2854,10 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
$get: {
input: {};
output: {
code: 0;
data: {
count: number;
};
code: 0;
};
outputFormat: "json";
status: 200;
@ -2866,8 +2872,8 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
};
};
output: {
data: boolean;
code: 0;
data: boolean;
};
outputFormat: "json";
status: 200;
@ -2947,13 +2953,13 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
};
};
output: {
code: 0;
data: {
json: string;
xml: string;
description: string;
xml: string;
content: string;
};
code: 0;
};
outputFormat: "json";
status: 200;
@ -2982,6 +2988,7 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
};
};
output: {
code: 0;
data: {
readCount: number;
feed: {
@ -3009,7 +3016,6 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
isPrivate: boolean;
} | undefined;
};
code: 0;
};
outputFormat: "json";
status: 200;
@ -3106,13 +3112,13 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
url: string;
routes: {
[x: string]: {
path: string;
example: string;
description: string;
name: string;
parameters: {
[x: string]: string;
};
path: string;
example: string;
name: string;
categories: string[];
maintainers: string[];
location: string;
@ -3131,12 +3137,12 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
$post: {
input: {};
output: {
code: 0;
data: {
userId: string;
sessionToken: string;
expires: string;
};
code: 0;
};
outputFormat: "json";
status: 200;
@ -3338,6 +3344,7 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
};
};
output: {
code: 0;
data: {
description: string | null;
title: string | null;
@ -3365,7 +3372,6 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
size_in_bytes?: number | undefined;
}[] | null | undefined;
}[];
code: 0;
};
outputFormat: "json";
status: 200;
@ -3381,6 +3387,7 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
};
};
output: {
code: 0;
data: {
title: string | null;
view: number;
@ -3404,7 +3411,6 @@ declare const _routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
category: string | null;
isPrivate: boolean;
}[];
code: 0;
};
outputFormat: "json";
status: 200;

View File

@ -0,0 +1,39 @@
import { FeedViewType } from "@renderer/lib/enum"
import { cn } from "@renderer/lib/utils"
import { MarkAllButton } from "./mark-all-button"
import { SocialMediaDateItem } from "./social-media-item"
export const DateItem = ({
date,
view,
isFirst,
}: {
date: string
view: FeedViewType
isFirst: boolean
}) => {
const dateObj = new Date(date)
const dateString = dateObj.toLocaleDateString("en-US", { weekday: "long", month: "short", day: "numeric" })
const startOfDay = new Date(dateObj.setHours(0, 0, 0, 0)).getTime()
const endOfDay = new Date(dateObj.setHours(23, 59, 59, 999)).getTime()
const className = cn(isFirst ? "pt-2" : "pt-8", "flex items-center gap-1 pl-2 text-sm font-bold text-zinc-800 dark:text-neutral-400")
if (view === FeedViewType.SocialMedia) {
return <SocialMediaDateItem date={date} className={className} />
}
return (
<div className={className}>
<MarkAllButton
className="size-7 text-base"
filter={{
startTime: startOfDay,
endTime: endOfDay,
}}
/>
{dateString}
</div>
)
}

View File

@ -62,7 +62,10 @@ export const useEntriesByView = ({ onReset }: { onReset?: () => void }) => {
const { feedId, view, isAllFeeds, isCollection } = routeParams
const folderIds = useFolderFeedsByFeedId(feedId)
const folderIds = useFolderFeedsByFeedId({
feedId,
view,
})
const entriesOptions = {
id: folderIds?.join(",") || feedId,
@ -107,7 +110,7 @@ export const useEntriesByView = ({ onReset }: { onReset?: () => void }) => {
)
const currentEntries = useEntryIdsByFeedIdOrView(
isAllFeeds ? view : feedId!,
isAllFeeds ? view : folderIds || feedId!,
{
unread: unreadOnly,
view,
@ -152,10 +155,30 @@ export const useEntriesByView = ({ onReset }: { onReset?: () => void }) => {
setMergedEntries(nextIds)
}, [entryIdsAsDeps])
const sortEntries = () =>
isCollection ?
sortEntriesIdByStarAt(mergedEntries) :
sortEntriesIdByEntryPublishedAt(mergedEntries)
const sortEntries = isCollection ?
sortEntriesIdByStarAt(mergedEntries) :
sortEntriesIdByEntryPublishedAt(mergedEntries)
const entriesWithDate = useMemo(() => {
if (views[view].gridMode) {
return sortEntries
}
const entriesId2Map = entryActions.getFlattenMapEntries()
let lastDate = ""
const entriesWithDate = [] as string[]
for (const id of sortEntries) {
const entry = entriesId2Map[id]
if (entry) {
const date = new Date(entry.entries.publishedAt).toDateString()
if (date !== lastDate) {
entriesWithDate.push(date)
lastDate = date
}
}
entriesWithDate.push(id)
}
return entriesWithDate
}, [sortEntries, view])
return {
...query,
@ -164,7 +187,7 @@ export const useEntriesByView = ({ onReset }: { onReset?: () => void }) => {
refetch: useCallback(() => {
query.refetch()
}, [query]),
entriesIds: sortEntries(),
entriesIds: entriesWithDate,
totalCount: query.data?.pages?.[0]?.total ?? mergedEntries.length,
}
}

View File

@ -6,40 +6,28 @@ import { useMe } from "@renderer/atoms/user"
import { m } from "@renderer/components/common/Motion"
import { EmptyIcon } from "@renderer/components/icons/empty"
import { AutoResizeHeight } from "@renderer/components/ui/auto-resize-height"
import { ActionButton, StyledButton } from "@renderer/components/ui/button"
import { ActionButton } from "@renderer/components/ui/button"
import { DividerVertical } from "@renderer/components/ui/divider"
import { LoadingCircle } from "@renderer/components/ui/loading"
import {
Popover,
PopoverClose,
PopoverContent,
PopoverTrigger,
} from "@renderer/components/ui/popover"
import { ScrollArea } from "@renderer/components/ui/scroll-area"
import { EllipsisHorizontalTextWithTooltip } from "@renderer/components/ui/typography"
import {
FEED_COLLECTION_LIST,
ROUTE_ENTRY_PENDING,
ROUTE_FEED_IN_FOLDER,
views,
} from "@renderer/constants"
import { shortcuts } from "@renderer/constants/shortcuts"
import { useNavigateEntry } from "@renderer/hooks/biz/useNavigateEntry"
import { useRouteParms } from "@renderer/hooks/biz/useRouteParams"
import { useIsOnline } from "@renderer/hooks/common/useIsOnline"
import { apiClient } from "@renderer/lib/api-fetch"
import { cn, getEntriesParams, getOS, isBizId } from "@renderer/lib/utils"
import { cn, getOS, isBizId } from "@renderer/lib/utils"
import { EntryHeader } from "@renderer/modules/entry-content/header"
import { useRefreshFeedMutation } from "@renderer/queries/feed"
import { entryActions, useEntry } from "@renderer/store/entry"
import { useFeedById, useFeedHeaderTitle } from "@renderer/store/feed"
import {
subscriptionActions,
useFolderFeedsByFeedId,
} from "@renderer/store/subscription"
import type { HTMLMotionProps } from "framer-motion"
import type { FC } from "react"
import { forwardRef, useCallback, useEffect, useRef, useState } from "react"
import { forwardRef, useCallback, useEffect, useRef } from "react"
import type {
ScrollSeekConfiguration,
VirtuosoHandle,
@ -47,6 +35,7 @@ import type {
} from "react-virtuoso"
import { Virtuoso, VirtuosoGrid } from "react-virtuoso"
import { DateItem } from "./date-item"
import { EntryColumnShortcutHandler } from "./EntryColumnShortcutHandler"
import { useEntriesByView, useEntryMarkReadHandler } from "./hooks"
import {
@ -54,6 +43,7 @@ import {
EntryItemSkeleton,
EntryItemSkeletonWithDelayShow,
} from "./item"
import { MarkAllButton } from "./mark-all-button"
import { girdClassNames } from "./styles"
const scrollSeekConfiguration: ScrollSeekConfiguration = {
@ -129,10 +119,14 @@ export function EntryColumn() {
}
},
itemContent: useCallback(
(_, entryId: string) => {
(index, entryId) => {
if (!entryId) return null
return <EntryItem key={entryId} entryId={entryId} view={view} />
if (entryId.includes(" ")) {
return <DateItem date={entryId} view={view} isFirst={index === 0} />
} else {
return <EntryItem key={entryId} entryId={entryId} view={view} />
}
},
[view],
),
@ -215,33 +209,6 @@ const ListHeader: FC<{
const unreadOnly = useGeneralSettingKey("unreadOnly")
const { feedId, entryId, view } = routerParams
const folderIds = useFolderFeedsByFeedId(feedId)
const [markPopoverOpen, setMarkPopoverOpen] = useState(false)
const handleMarkAllAsRead = useCallback(async () => {
if (!routerParams) return
await apiClient.reads.all.$post({
json: {
...getEntriesParams({
id: folderIds?.join(",") || feedId,
view: routerParams?.view,
}),
},
})
if (typeof routerParams.feedId === "number" || routerParams.isAllFeeds) {
subscriptionActions.markReadByView(routerParams.view)
} else if (routerParams.feedId?.startsWith(ROUTE_FEED_IN_FOLDER)) {
subscriptionActions.markReadByFolder(
routerParams.feedId.replace(ROUTE_FEED_IN_FOLDER, ""),
)
} else {
routerParams.feedId?.split(",").forEach((feedId) => {
entryActions.markReadByFeedId(feedId)
})
}
setMarkPopoverOpen(false)
}, [feedId, folderIds, routerParams])
const headerTitle = useFeedHeaderTitle()
const os = getOS()
@ -275,12 +242,12 @@ const ListHeader: FC<{
const feed = useFeedById(routerParams.feedId)
const titleStyleBasedView = [
"pl-11",
"pl-4",
"pl-12",
"pl-7",
"pl-7",
"pl-7",
"px-5",
"pl-11",
"pl-12",
]
return (
@ -361,28 +328,7 @@ const ListHeader: FC<{
<i className="i-mgc-round-cute-re" />
)}
</ActionButton>
<Popover open={markPopoverOpen} onOpenChange={setMarkPopoverOpen}>
<PopoverTrigger asChild>
<ActionButton
shortcut={shortcuts.entries.markAllAsRead.key}
tooltip="Mark All as Read"
>
<i className="i-mgc-check-circle-cute-re" />
</ActionButton>
</PopoverTrigger>
<PopoverContent className="flex w-fit flex-col items-center justify-center gap-3 text-[0.94rem] font-medium">
<div>Mark all as read?</div>
<div className="space-x-4">
<PopoverClose>
<StyledButton variant="outline">Cancel</StyledButton>
</PopoverClose>
{/* TODO */}
<StyledButton onClick={handleMarkAllAsRead}>
Confirm
</StyledButton>
</div>
</PopoverContent>
</Popover>
<MarkAllButton />
</div>
</div>
{titleAtBottom && titleInfo}

View File

@ -0,0 +1,77 @@
import { ActionButton, StyledButton } from "@renderer/components/ui/button"
import {
Popover,
PopoverClose,
PopoverContent,
PopoverTrigger,
} from "@renderer/components/ui/popover"
import { shortcuts } from "@renderer/constants/shortcuts"
import { useRouteParms } from "@renderer/hooks/biz/useRouteParams"
import { subscriptionActions, useFolderFeedsByFeedId } from "@renderer/store/subscription"
import { useCallback, useState } from "react"
export const MarkAllButton = ({
filter,
className,
}: {
filter?: {
startTime: number
endTime: number
}
className?: string
}) => {
const [markPopoverOpen, setMarkPopoverOpen] = useState(false)
const routerParams = useRouteParms()
const { feedId, view } = routerParams
const folderIds = useFolderFeedsByFeedId({
feedId,
view,
})
const handleMarkAllAsRead = useCallback(async () => {
if (!routerParams) return
if (typeof routerParams.feedId === "number" || routerParams.isAllFeeds) {
subscriptionActions.markReadByView(view, filter)
} else if (folderIds) {
subscriptionActions.markReadByFeedIds(
view,
folderIds,
filter,
)
} else if (routerParams.feedId) {
subscriptionActions.markReadByFeedIds(view, routerParams.feedId?.split(","), filter)
}
}, [feedId, folderIds, filter, routerParams])
return (
<Popover open={markPopoverOpen} onOpenChange={setMarkPopoverOpen}>
<PopoverTrigger asChild>
<ActionButton
shortcut={shortcuts.entries.markAllAsRead.key}
tooltip="Mark All as Read"
className={className}
>
<i className="i-mgc-check-circle-cute-re" />
</ActionButton>
</PopoverTrigger>
<PopoverContent className="flex w-fit flex-col items-center justify-center gap-3 text-[0.94rem] font-medium">
<div>Mark all as read?</div>
<div className="space-x-4">
<PopoverClose>
<StyledButton variant="outline">Cancel</StyledButton>
</PopoverClose>
{/* TODO */}
<StyledButton onClick={() => {
handleMarkAllAsRead()
setMarkPopoverOpen(false)
}}
>
Confirm
</StyledButton>
</div>
</PopoverContent>
</Popover>
)
}

View File

@ -13,6 +13,7 @@ import { useEntry } from "@renderer/store/entry/hooks"
import { useFeedById } from "@renderer/store/feed"
import { ReactVirtuosoItemPlaceholder } from "../../components/ui/placeholder"
import { MarkAllButton } from "./mark-all-button"
import { StarIcon } from "./star-icon"
import { EntryTranslation } from "./translation"
import type { EntryListItemFC } from "./types"
@ -164,3 +165,28 @@ export const SocialMediaItemSkeleton = (
</div>
</div>
)
export const SocialMediaDateItem = ({
date,
className,
}: {
date: string
className?: string
}) => {
const dateObj = new Date(date)
const dateString = dateObj.toLocaleDateString("en-US", { weekday: "long", month: "short", day: "numeric" })
const startOfDay = new Date(dateObj.setHours(0, 0, 0, 0)).getTime()
const endOfDay = new Date(dateObj.setHours(23, 59, 59, 999)).getTime()
return (
<div className={cn(className, "m-auto w-[75ch] gap-3 pl-5 text-base")}>
<MarkAllButton
filter={{
startTime: startOfDay,
endTime: endOfDay,
}}
/>
{dateString}
</div>
)
}

View File

@ -49,14 +49,14 @@ export const ClaimDailyReward = () => {
disabled={!canClaim}
>
{canClaim ?
"Claim Daily Power" :
`Claim in ${hour}:${minute}`}
"Mint Daily Power" :
`Mint in ${hour}:${minute}`}
</StyledButton>
</TooltipTrigger>
<TooltipContent>
{canClaim ?
"Claim your 2 Daily Power now!" :
`You can claim your Daily Power in ${hour}:${minute}.`}
"Mint your 2 Daily Power now!" :
`You can mint your Daily Power in ${hour}:${minute}.`}
</TooltipContent>
</Tooltip>
)

View File

@ -25,7 +25,11 @@ export const wallet = {
apiClient.wallets.transactions.claim_daily_ttl.$get()),
transactions: {
get: (query: Parameters<typeof apiClient.wallets.transactions.$get>[0]["query"] = {}) =>
get: (
query: Parameters<
typeof apiClient.wallets.transactions.$get
>[0]["query"] = {},
) =>
defineQuery(
["wallet", "transactions", query].filter(Boolean),
async () => {
@ -41,15 +45,11 @@ export const wallet = {
}
export const useWallet = ({ userId }: { userId?: string } = {}) =>
useAuthQuery(
wallet.get({ userId }),
{ enabled: !!userId },
)
useAuthQuery(wallet.get({ userId }), { enabled: !!userId })
export const useWalletTransactions = (query: Parameters<typeof wallet.transactions.get>[0] = {}) =>
useAuthQuery(
wallet.transactions.get(query),
)
export const useWalletTransactions = (
query: Parameters<typeof wallet.transactions.get>[0] = {},
) => useAuthQuery(wallet.transactions.get(query))
export const useCreateWalletMutation = () =>
useMutation({
@ -65,36 +65,41 @@ export const useCreateWalletMutation = () =>
})
export const useClaimWalletDailyRewardTtl = () =>
useAuthQuery(
wallet.claimDailyRewardTtl(),
{ refetchInterval: 5000 },
)
useAuthQuery(wallet.claimDailyRewardTtl(), { refetchInterval: 5000 })
export const useClaimWalletDailyRewardMutation = () => useMutation({
mutationKey: ["claimWalletDailyReward"],
mutationFn: () => apiClient.wallets.transactions.claim_daily.$post(),
async onError(err) {
toast.error(getFetchErrorMessage(err))
},
onSuccess() {
wallet.get().invalidate()
wallet.claimDailyRewardTtl().invalidate()
window.posthog?.capture("daily_reward_claimed")
toast("🎉 Daily reward claimed.")
},
})
export const useClaimWalletDailyRewardMutation = () =>
useMutation({
mutationKey: ["claimWalletDailyReward"],
mutationFn: () => apiClient.wallets.transactions.claim_daily.$post(),
async onError(err) {
toast.error(getFetchErrorMessage(err))
},
onSuccess() {
wallet.get().invalidate()
wallet.claimDailyRewardTtl().invalidate()
window.posthog?.capture("daily_reward_claimed")
toast("🎉 Daily reward claimed.")
},
})
export const useWalletTipMutation = () => useMutation({
mutationKey: ["walletTip"],
mutationFn: (data: Parameters<typeof apiClient.wallets.transactions.tip.$post>[0]["json"]) =>
apiClient.wallets.transactions.tip.$post({ json: data }),
async onError(err) {
toast.error(getFetchErrorMessage(err))
},
onSuccess(_, variables) {
wallet.get().invalidate()
wallet.transactions.get().invalidate()
window.posthog?.capture("tip_sent", { amount: variables.amount, feedId: variables.feedId })
toast("🎉 Tipped.")
},
})
export const useWalletTipMutation = () =>
useMutation({
mutationKey: ["walletTip"],
mutationFn: (
data: Parameters<
typeof apiClient.wallets.transactions.tip.$post
>[0]["json"],
) => apiClient.wallets.transactions.tip.$post({ json: data }),
async onError(err) {
toast.error(getFetchErrorMessage(err))
},
onSuccess(_, variables) {
wallet.get().invalidate()
wallet.transactions.get().invalidate()
window.posthog?.capture("tip_sent", {
amount: variables.amount,
feedId: variables.feedId,
})
toast("🎉 Tipped.")
},
})

View File

@ -6,7 +6,7 @@ import type { FeedViewType } from "@renderer/lib/enum"
import type { EntryReadHistoriesModel } from "src/hono"
import { useShallow } from "zustand/react/shallow"
import { useFeedIdByView, useFolderFeedsByFeedId } from "../subscription"
import { useFeedIdByView } from "../subscription"
import { getEntryIsInView, getFilteredFeedIds } from "./helper"
import { useEntryStore } from "./store"
import type { EntryFilter, FlatEntryModel } from "./types"
@ -72,30 +72,31 @@ export const useEntryIdsByView = (view: FeedViewType, filter?: EntryFilter) => {
return useEntryStore(useShallow(() => getFilteredFeedIds(feedIds, filter)))
}
export const useEntryIdsByFolderName = (
folderName: string,
export const useEntryIdsByFeedIds = (
feedIds: string[],
filter: EntryFilter = {},
) => {
const feedIds = useFolderFeedsByFeedId(folderName)
return useEntryStore(
useShallow(() => {
if (!feedIds) return null
) => useEntryStore(
useShallow(() => {
if (!feedIds) return null
if ((!Array.isArray(feedIds))) return null
return getFilteredFeedIds(feedIds, filter)
}),
)
}
return getFilteredFeedIds(feedIds, filter)
}),
)
export const useEntryIdsByFeedIdOrView = (
feedIdOrView: string | FeedViewType,
feedIdOrView: string | string[] | FeedViewType,
filter: EntryFilter = {},
) => {
const byView = useEntryIdsByView(feedIdOrView as FeedViewType, filter)
const byId = useEntryIdsByFeedId(feedIdOrView as string, filter)
const byFolder = useEntryIdsByFolderName(feedIdOrView as string, filter)
if (typeof feedIdOrView === "string") {
return feedIdOrView.startsWith(ROUTE_FEED_IN_FOLDER) ? byFolder : byId
const byFolder = useEntryIdsByFeedIds(feedIdOrView as string[], filter)
if (Array.isArray(feedIdOrView)) {
return byFolder
} else if (typeof feedIdOrView === "string") {
return byId
} else {
return byView
}
return byView
}
export const useEntryReadHistory = (entryId: string): Omit<EntryReadHistoriesModel, "entryId"> | null => useEntryStore(useShallow((state) => state.readHistory[entryId]))

View File

@ -121,13 +121,25 @@ class EntryActions {
)
}
patchManyByFeedId(feedId: string, changed: Partial<CombinedEntryModel>) {
patchManyByFeedId(feedId: string, changed: Partial<CombinedEntryModel>, filter?: {
startTime: number
endTime: number
}) {
set((state) =>
produce(state, (draft) => {
const ids = draft.entries[feedId]
if (!ids) return
ids.forEach((entryId) => {
if (filter) {
const entry = draft.flatMapEntries[entryId]
if (
+new Date(entry.entries.publishedAt) < filter.startTime ||
+new Date(entry.entries.publishedAt) > filter.endTime
) {
return
}
}
Object.assign(draft.flatMapEntries[entryId], changed)
})
@ -136,6 +148,15 @@ class EntryActions {
)
}
async markReadByFeedId(feedId: string) {
const state = get()
const entries = state.entries[feedId] || []
await Promise.all(
entries.map((entryId) => this.markRead(feedId, entryId, true)),
)
feedUnreadActions.updateByFeedId(feedId, 0)
}
private patchAll(changed: Partial<CombinedEntryModel>) {
set((state) =>
produce(state, (draft) => {
@ -308,15 +329,6 @@ class EntryActions {
)
}
async markReadByFeedId(feedId: string) {
const state = get()
const entries = state.entries[feedId] || []
await Promise.all(
entries.map((entryId) => this.markRead(feedId, entryId, true)),
)
feedUnreadActions.updateByFeedId(feedId, 0)
}
async markStar(entryId: string, star: boolean) {
this.patch(entryId, {
collections: star ?

View File

@ -17,7 +17,13 @@ export const useSubscriptionByView = (view: FeedViewType) =>
export const useSubscriptionByFeedId = (feedId: FeedId) =>
useSubscriptionStore((state) => state.data[feedId])
export const useFolderFeedsByFeedId = (feedId?: string) =>
export const useFolderFeedsByFeedId = ({
feedId,
view,
}: {
feedId?: string
view: FeedViewType
}) =>
useSubscriptionStore((state): string[] | null => {
if (typeof feedId !== "string") return null
if (feedId === FEED_COLLECTION_LIST) {
@ -32,7 +38,7 @@ export const useFolderFeedsByFeedId = (feedId?: string) =>
const feedIds: string[] = []
for (const feedId in state.data) {
const subscription = state.data[feedId]
if (subscription.category === folderName || subscription.defaultCategory === folderName) {
if (subscription.view === view && (subscription.category === folderName || subscription.defaultCategory === folderName)) {
feedIds.push(feedId)
}
}

View File

@ -66,6 +66,11 @@ export const useSubscriptionStore = createZustandStore<SubscriptionState>(
const set = useSubscriptionStore.setState
const get = useSubscriptionStore.getState
type MarkReadFilter = {
startTime: number
endTime: number
}
class SubscriptionActions {
async fetchByView(view?: FeedViewType) {
const res = await apiClient.subscriptions.$get({
@ -110,24 +115,53 @@ class SubscriptionActions {
)
}
markReadByView(view?: FeedViewType) {
const state = get()
for (const feedId in state.data) {
if (state.data[feedId].view === view) {
feedUnreadActions.updateByFeedId(feedId, 0)
entryActions.patchManyByFeedId(feedId, { read: true })
}
}
async markReadByView(view: FeedViewType, filter?: MarkReadFilter) {
doMutationAndTransaction(
() =>
apiClient.reads.all.$post({
json: {
view,
...filter,
},
}),
async () => {
const state = get()
for (const feedId in state.data) {
if (state.data[feedId].view === view) {
feedUnreadActions.updateByFeedId(feedId, 0)
entryActions.patchManyByFeedId(feedId, { read: true }, filter)
}
}
if (filter) {
feedUnreadActions.fetchUnreadByView(view)
}
},
)
}
markReadByFolder(folder: string) {
const state = get()
for (const feedId in state.data) {
if (state.data[feedId].category === folder) {
feedUnreadActions.updateByFeedId(feedId, 0)
entryActions.patchManyByFeedId(feedId, { read: true })
}
}
async markReadByFeedIds(
view: FeedViewType,
feedIds: string[],
filter?: MarkReadFilter,
) {
doMutationAndTransaction(
() =>
apiClient.reads.all.$post({
json: {
feedIdList: feedIds,
...filter,
},
}),
async () => {
for (const feedId of feedIds) {
feedUnreadActions.updateByFeedId(feedId, 0)
entryActions.patchManyByFeedId(feedId, { read: true }, filter)
}
if (filter) {
feedUnreadActions.fetchUnreadByView(view)
}
},
)
}
clear() {
@ -253,7 +287,11 @@ class SubscriptionActions {
}),
)
await Promise.all(folderFeedIds.map((feedId) => SubscriptionService.changeView(feedId, changeToView)))
await Promise.all(
folderFeedIds.map((feedId) =>
SubscriptionService.changeView(feedId, changeToView),
),
)
}
}