Merge branch 'main' into feature/actions
This commit is contained in:
commit
5e5af68c8c
|
|
@ -1,12 +1,10 @@
|
|||
import { useAsRead } from "@renderer/hooks/useAsRead"
|
||||
import { useEntryActions } from "@renderer/hooks/useEntryActions"
|
||||
import { apiClient } from "@renderer/lib/api-fetch"
|
||||
import { useEntryActions, useRead } from "@renderer/hooks/useEntryActions"
|
||||
import { views } from "@renderer/lib/constants"
|
||||
import { showNativeMenu } from "@renderer/lib/native-menu"
|
||||
import { cn } from "@renderer/lib/utils"
|
||||
import { feedActions, useFeedStore } from "@renderer/store"
|
||||
import { entryActions, useEntry } from "@renderer/store/entry"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { useEntry } from "@renderer/store/entry"
|
||||
|
||||
import { ReactVirtuosoItemPlaceholder } from "../ui/placeholder"
|
||||
|
||||
|
|
@ -29,20 +27,7 @@ export function EntryItemWrapper({
|
|||
|
||||
const asRead = useAsRead(entry)
|
||||
|
||||
const read = useMutation({
|
||||
mutationFn: async () =>
|
||||
apiClient.reads.$post({
|
||||
json: {
|
||||
entryIds: [entry.entries.id],
|
||||
},
|
||||
}),
|
||||
onMutate: () => {
|
||||
entryActions.optimisticUpdate(entry.entries.id, {
|
||||
read: true,
|
||||
})
|
||||
},
|
||||
// TODO fallback
|
||||
})
|
||||
const markReadMutation = useRead(entry)
|
||||
|
||||
// NOTE: prevent 0 height element, react virtuoso will not stop render any more
|
||||
if (!entry) return <ReactVirtuosoItemPlaceholder />
|
||||
|
|
@ -52,15 +37,17 @@ export function EntryItemWrapper({
|
|||
<div
|
||||
className={cn(
|
||||
"rounded-md bg-background transition-colors",
|
||||
!views[view || 0].wideMode && activeEntry === entry.entries.id && "bg-theme-item-active",
|
||||
!views[view || 0].wideMode &&
|
||||
activeEntry === entry.entries.id &&
|
||||
"bg-theme-item-active",
|
||||
asRead ? "text-zinc-500/90" : "text-zinc-900 dark:text-white/90",
|
||||
)}
|
||||
// ref={ref}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
feedActions.setActiveEntry(entry.entries.id)
|
||||
if (!entry.read) {
|
||||
read.mutate()
|
||||
if (!asRead) {
|
||||
markReadMutation.mutate()
|
||||
}
|
||||
}}
|
||||
onDoubleClick={() =>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,11 @@ import type { FeedViewType } from "@renderer/lib/enum"
|
|||
import { cn } from "@renderer/lib/utils"
|
||||
import type { FeedListModel, SubscriptionResponse } from "@renderer/models"
|
||||
import { Queries } from "@renderer/queries"
|
||||
import { feedActions, useFeedActiveList, useUnreadStore } from "@renderer/store"
|
||||
import {
|
||||
feedActions,
|
||||
useFeedActiveList,
|
||||
useUnreadStore,
|
||||
} from "@renderer/store"
|
||||
import { useMemo, useState } from "react"
|
||||
import { Link } from "react-router-dom"
|
||||
import { parse } from "tldts"
|
||||
|
|
@ -13,11 +17,11 @@ import { parse } from "tldts"
|
|||
import { FeedCategory } from "./category"
|
||||
|
||||
const useData = (view?: FeedViewType) => {
|
||||
const subscriptions = useBizQuery(Queries.subscription.byView(view))
|
||||
const query = useBizQuery(Queries.subscription.byView(view))
|
||||
|
||||
// TODO Refactor this into category store
|
||||
return useMemo(() => {
|
||||
if (!subscriptions.data) return null
|
||||
if (!query.data) return null
|
||||
const categories = {
|
||||
list: {},
|
||||
} as {
|
||||
|
|
@ -29,10 +33,10 @@ const useData = (view?: FeedViewType) => {
|
|||
>
|
||||
}
|
||||
const domains: Record<string, number> = {}
|
||||
const data = structuredClone(subscriptions.data)
|
||||
const subscriptions = structuredClone(query.data)
|
||||
|
||||
if (subscriptions) {
|
||||
for (const subscription of data.subscriptions) {
|
||||
if (query) {
|
||||
for (const subscription of subscriptions) {
|
||||
if (!subscription.category && subscription.feeds.siteUrl) {
|
||||
const { domain } = parse(subscription.feeds.siteUrl)
|
||||
if (domain) {
|
||||
|
|
@ -44,8 +48,8 @@ const useData = (view?: FeedViewType) => {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (subscriptions) {
|
||||
for (const subscription of data.subscriptions) {
|
||||
if (query) {
|
||||
for (const subscription of subscriptions) {
|
||||
if (!subscription.category) {
|
||||
if (subscription.feeds.siteUrl) {
|
||||
const { domain } = parse(subscription.feeds.siteUrl)
|
||||
|
|
@ -64,8 +68,6 @@ const useData = (view?: FeedViewType) => {
|
|||
}
|
||||
}
|
||||
|
||||
const unread = data.unreads[subscription.feedId] || 0
|
||||
subscription.unread = unread
|
||||
categories.list[subscription.category].list.push(subscription)
|
||||
}
|
||||
}
|
||||
|
|
@ -77,7 +79,7 @@ const useData = (view?: FeedViewType) => {
|
|||
return {
|
||||
list,
|
||||
} as FeedListModel
|
||||
}, [subscriptions])
|
||||
}, [query])
|
||||
}
|
||||
export function FeedList({
|
||||
className,
|
||||
|
|
@ -92,6 +94,8 @@ export function FeedList({
|
|||
const data = useData(view)
|
||||
const activeList = useFeedActiveList()
|
||||
|
||||
useBizQuery(Queries.subscription.unreadAll())
|
||||
|
||||
const totalUnread = useUnreadStore((state) => {
|
||||
let unread = 0
|
||||
data?.list.forEach((a) => {
|
||||
|
|
@ -154,8 +158,7 @@ export function FeedList({
|
|||
<div
|
||||
className={cn(
|
||||
"flex h-8 w-full items-center rounded-md px-2.5 transition-colors",
|
||||
activeList?.id === "collections" &&
|
||||
"bg-native-active",
|
||||
activeList?.id === "collections" && "bg-native-active",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
|
|
@ -172,7 +175,7 @@ export function FeedList({
|
|||
<i className="i-mingcute-star-fill mr-2 text-orange-500" />
|
||||
Starred
|
||||
</div>
|
||||
{data?.list?.length ? (
|
||||
{data?.list?.length ?
|
||||
sortedByUnread?.map((category) => (
|
||||
<FeedCategory
|
||||
key={category.name}
|
||||
|
|
@ -180,20 +183,18 @@ export function FeedList({
|
|||
view={view}
|
||||
expansion={expansion}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
!hideTitle && (
|
||||
<div className="flex h-full flex-1 items-center text-zinc-500">
|
||||
<Link
|
||||
to="/discover"
|
||||
className="-mt-36 flex h-full flex-1 flex-col items-center justify-center gap-2"
|
||||
>
|
||||
<i className="i-mingcute-add-line text-3xl " />
|
||||
Add some feeds
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
)) :
|
||||
!hideTitle && (
|
||||
<div className="flex h-full flex-1 items-center text-zinc-500">
|
||||
<Link
|
||||
to="/discover"
|
||||
className="-mt-36 flex h-full flex-1 flex-col items-center justify-center gap-2"
|
||||
>
|
||||
<i className="i-mingcute-add-line text-3xl " />
|
||||
Add some feeds
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ declare const routes: hono_hono_base.HonoBase<hono.Env, {
|
|||
$get: {
|
||||
input: {
|
||||
query: {
|
||||
view: string;
|
||||
view?: string | undefined;
|
||||
};
|
||||
};
|
||||
output: {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,87 @@ import type { FetchError } from "ofetch"
|
|||
import { ofetch } from "ofetch"
|
||||
import { toast } from "sonner"
|
||||
|
||||
export const useCollect = (entry: EntryModel | undefined) =>
|
||||
useMutation({
|
||||
mutationFn: async () =>
|
||||
entry &&
|
||||
apiClient.collections.$post({
|
||||
json: {
|
||||
entryId: entry?.entries.id,
|
||||
},
|
||||
}),
|
||||
|
||||
onMutate() {
|
||||
if (!entry) return
|
||||
entryActions.optimisticUpdate(entry.entries.id, {
|
||||
collections: {
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast("Collected.", {
|
||||
duration: 1000,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const useUnCollect = (entry: EntryModel | undefined) =>
|
||||
useMutation({
|
||||
mutationFn: async () =>
|
||||
entry &&
|
||||
apiClient.collections.$delete({
|
||||
json: {
|
||||
entryId: entry?.entries.id,
|
||||
},
|
||||
}),
|
||||
|
||||
onMutate() {
|
||||
if (!entry) return
|
||||
entryActions.optimisticUpdate(entry.entries.id, {
|
||||
collections: undefined,
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast("Uncollected.", {
|
||||
duration: 1000,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const useRead = (entry: EntryModel | undefined) =>
|
||||
useMutation({
|
||||
mutationFn: async () =>
|
||||
entry &&
|
||||
apiClient.reads.$post({
|
||||
json: {
|
||||
entryIds: [entry.entries.id],
|
||||
},
|
||||
}),
|
||||
|
||||
onMutate: () => {
|
||||
if (!entry) return
|
||||
|
||||
entryActions.markRead(entry.feeds.id, entry.entries.id, true)
|
||||
},
|
||||
})
|
||||
export const useUnread = (entry: EntryModel | undefined) =>
|
||||
useMutation({
|
||||
mutationFn: async () =>
|
||||
entry &&
|
||||
apiClient.reads.$delete({
|
||||
json: {
|
||||
entryId: entry.entries.id,
|
||||
},
|
||||
}),
|
||||
|
||||
onMutate: () => {
|
||||
if (!entry) return
|
||||
|
||||
entryActions.markRead(entry.feeds.id, entry.entries.id, false)
|
||||
},
|
||||
})
|
||||
|
||||
export const useEntryActions = ({
|
||||
view,
|
||||
entry,
|
||||
|
|
@ -27,76 +108,10 @@ export const useEntryActions = ({
|
|||
},
|
||||
})
|
||||
|
||||
const collect = useMutation({
|
||||
mutationFn: async () =>
|
||||
entry && apiClient.collections.$post({
|
||||
json: {
|
||||
entryId: entry?.entries.id,
|
||||
},
|
||||
}),
|
||||
|
||||
onMutate() {
|
||||
if (!entry) return
|
||||
entryActions.optimisticUpdate(entry.entries.id, {
|
||||
collections: {
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast("Collected.", {
|
||||
duration: 1000,
|
||||
})
|
||||
},
|
||||
})
|
||||
const uncollect = useMutation({
|
||||
mutationFn: async () =>
|
||||
entry && apiClient.collections.$delete({
|
||||
json: {
|
||||
entryId: entry?.entries.id,
|
||||
},
|
||||
}),
|
||||
|
||||
onMutate() {
|
||||
if (!entry) return
|
||||
entryActions.optimisticUpdate(entry.entries.id, {
|
||||
collections: undefined,
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast("Uncollected.", {
|
||||
duration: 1000,
|
||||
})
|
||||
},
|
||||
})
|
||||
const read = useMutation({
|
||||
mutationFn: async () =>
|
||||
entry &&
|
||||
apiClient.reads.$post({
|
||||
json: {
|
||||
entryIds: [entry.entries.id],
|
||||
},
|
||||
}),
|
||||
|
||||
onMutate: () => {
|
||||
if (!entry) return
|
||||
|
||||
entryActions.markRead(entry.feeds.id, entry.entries.id, true)
|
||||
},
|
||||
})
|
||||
const unread = useMutation({
|
||||
mutationFn: async () => entry && apiClient.reads.$delete({
|
||||
json: {
|
||||
entryId: entry.entries.id,
|
||||
},
|
||||
}),
|
||||
|
||||
onMutate: () => {
|
||||
if (!entry) return
|
||||
|
||||
entryActions.markRead(entry.feeds.id, entry.entries.id, false)
|
||||
},
|
||||
})
|
||||
const collect = useCollect(entry)
|
||||
const uncollect = useUnCollect(entry)
|
||||
const read = useRead(entry)
|
||||
const unread = useUnread(entry)
|
||||
|
||||
if (!entry?.entries.url || view === undefined) return { items: [] }
|
||||
|
||||
|
|
|
|||
|
|
@ -7,16 +7,7 @@ export const subscription = {
|
|||
byView: (view?: FeedViewType) =>
|
||||
defineQuery(
|
||||
["subscriptions", view],
|
||||
async () => {
|
||||
const [subscriptions, unreads] = await Promise.all([
|
||||
subscriptionActions.fetchByView(view),
|
||||
unreadActions.fetchUnreadByView(view),
|
||||
])
|
||||
return {
|
||||
subscriptions,
|
||||
unreads,
|
||||
}
|
||||
},
|
||||
async () => subscriptionActions.fetchByView(view),
|
||||
{
|
||||
rootKey: ["subscriptions"],
|
||||
},
|
||||
|
|
@ -29,4 +20,7 @@ export const subscription = {
|
|||
|
||||
return res.data
|
||||
}),
|
||||
|
||||
unreadAll: () =>
|
||||
defineQuery(["unread-all"], async () => unreadActions.fetchUnreadAll()),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ export const useEntryStore = createZustandStore<EntryState & EntryActions>(
|
|||
},
|
||||
|
||||
markRead: (feedId: string, entryId: string, read: boolean) => {
|
||||
console.log(feedId, "feedId")
|
||||
unreadActions.incrementByFeedId(feedId, read ? -1 : 1)
|
||||
entryActions.optimisticUpdate(entryId, {
|
||||
read,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ interface UnreadState {
|
|||
interface UnreadActions {
|
||||
updateByFeedId: (feedId: string, unread: number) => void
|
||||
fetchUnreadByView: (view?: FeedViewType) => Promise<Record<string, number>>
|
||||
fetchUnreadAll: () => Promise<Record<string, number>>
|
||||
incrementByFeedId: (feedId: string, inc: number) => void
|
||||
|
||||
internal_reset: () => void
|
||||
|
|
@ -19,7 +20,7 @@ export const useUnreadStore = createZustandStore<UnreadState & UnreadActions>(
|
|||
{
|
||||
version: 0,
|
||||
},
|
||||
)((set, get) => ({
|
||||
)((set) => ({
|
||||
data: {},
|
||||
|
||||
internal_reset() {
|
||||
|
|
@ -32,14 +33,28 @@ export const useUnreadStore = createZustandStore<UnreadState & UnreadActions>(
|
|||
})
|
||||
|
||||
const { data } = unread
|
||||
|
||||
set((state) =>
|
||||
produce(state, (state) => {
|
||||
get().internal_reset()
|
||||
for (const feedId in data) {
|
||||
state.data[feedId] = data[feedId]
|
||||
return state
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
state.data[key] = value
|
||||
}
|
||||
}),
|
||||
)
|
||||
return data
|
||||
},
|
||||
async fetchUnreadAll() {
|
||||
const unread = await apiClient.reads.$get({
|
||||
query: {},
|
||||
})
|
||||
|
||||
const { data } = unread
|
||||
this.internal_reset()
|
||||
set((state) =>
|
||||
produce(state, (state) => {
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
state.data[key] = value
|
||||
}
|
||||
return state
|
||||
}),
|
||||
)
|
||||
return data
|
||||
|
|
|
|||
Loading…
Reference in New Issue