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:
parent
0f8309b203
commit
9d5a81112e
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
56
src/hono.ts
56
src/hono.ts
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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]))
|
||||
|
|
|
|||
|
|
@ -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 ?
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue