feat: polling update read count and invalidate query when page active (#154)

* feat: polling update read count and invalidate query when page active

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

* fix: posthog node

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

* update

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

* cleanup

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

* fix: ci

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

---------

Signed-off-by: Innei <i@innei.in>
This commit is contained in:
Innei 2024-07-25 20:31:23 +08:00 committed by GitHub
parent cdc8703869
commit 917f3a3370
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
49 changed files with 3864 additions and 3521 deletions

View File

@ -15,6 +15,7 @@ export default defineConfig({
alias: {
"@shared": resolve("src/shared/src"),
"@env": resolve("./src/env.ts"),
"@pkg": resolve("./package.json"),
},
},
},
@ -22,6 +23,7 @@ export default defineConfig({
resolve: {
alias: {
"@env": resolve("./src/env.ts"),
"@pkg": resolve("./package.json"),
},
},
},

View File

@ -10,7 +10,7 @@ export default defineConfig(
lineBreak: "after",
},
lessOpinionated: true,
ignores: ["src/renderer/src/hono.ts"],
ignores: ["src/renderer/src/hono.ts", "src/hono.ts"],
preferESM: false,
},
{

View File

@ -94,6 +94,7 @@
"ofetch": "1.3.4",
"path-to-regexp": "7.0.0",
"posthog-js": "1.148.0",
"posthog-node": "4.0.1",
"react-error-boundary": "4.0.13",
"react-fast-marquee": "1.6.5",
"react-hook-form": "7.52.1",

View File

@ -211,6 +211,9 @@ importers:
posthog-js:
specifier: 1.148.0
version: 1.148.0
posthog-node:
specifier: 4.0.1
version: 4.0.1
react-error-boundary:
specifier: 4.0.13
version: 4.0.13(react@18.3.1)
@ -5643,6 +5646,10 @@ packages:
posthog-js@1.148.0:
resolution: {integrity: sha512-nydf+aret26YViDBwH7DdN/c8dmCzQGNQ1yredeE47UlbpJ3J/iTgskNdmGJIofFcdSagcwAE/FF7s++YfTtvg==}
posthog-node@4.0.1:
resolution: {integrity: sha512-rtqm2h22QxLGBrW2bLYzbRhliIrqgZ0k+gF0LkQ1SNdeD06YE5eilV0MxZppFSxC8TfH0+B0cWCuebEnreIDgQ==}
engines: {node: '>=15.0.0'}
postject@1.0.0-alpha.6:
resolution: {integrity: sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==}
engines: {node: '>=14.0.0'}
@ -6004,6 +6011,9 @@ packages:
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
rusha@0.8.14:
resolution: {integrity: sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA==}
safe-buffer@5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
@ -13135,6 +13145,13 @@ snapshots:
preact: 10.22.1
web-vitals: 4.2.1
posthog-node@4.0.1:
dependencies:
axios: 1.7.2
rusha: 0.8.14
transitivePeerDependencies:
- debug
postject@1.0.0-alpha.6:
dependencies:
commander: 9.5.0
@ -13527,6 +13544,8 @@ snapshots:
dependencies:
queue-microtask: 1.2.3
rusha@0.8.14: {}
safe-buffer@5.1.2: {}
safer-buffer@2.1.2: {}

3409
src/hono.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
// 5min
export const UNREAD_BACKGROUND_POLLING_INTERVAL = 1000 * 60 * 5

View File

@ -8,6 +8,7 @@ import { app, BrowserWindow, session } from "electron"
import { env } from "../env"
import { isDev } from "./env"
import { initializationApp } from "./init"
import { setAuthSessionToken } from "./lib/user"
import { registerUpdater } from "./updater"
import { createMainWindow, createWindow } from "./window"
@ -110,6 +111,7 @@ function bootsharp() {
const apiURL =
process.env["VITE_API_URL"] || import.meta.env.VITE_API_URL
if (token && apiURL) {
setAuthSessionToken(token)
mainWindow.webContents.session.cookies.set({
url: apiURL,
name: "authjs.session-token",

View File

@ -0,0 +1,45 @@
import { env } from "@env"
import PKG from "@pkg"
import { hc } from "hono/client"
import { ofetch } from "ofetch"
import type { AppType } from "../../hono"
import { logger } from "../logger"
import { getAuthSessionToken } from "./user"
const abortController = new AbortController()
export const apiFetch = ofetch.create({
baseURL: env.VITE_API_URL,
credentials: "include",
signal: abortController.signal,
retry: false,
onRequest({ request }) {
const authSessionToken = getAuthSessionToken()
if (!authSessionToken) {
abortController.abort()
return
}
logger.info(`API Request: ${request.toString()}`)
},
onRequestError(context) {
if (context.error.name === "AbortError") {
return
}
},
})
export const apiClient = hc<AppType>("", {
fetch: async (input, options = {}) => apiFetch(input.toString(), options),
headers() {
const authSessionToken = getAuthSessionToken()
return {
"X-App-Version": PKG.version,
"X-App-Dev": process.env.NODE_ENV === "development" ? "1" : "0",
"Cookie": authSessionToken ?
`authjs.session-token=${authSessionToken}` :
"",
}
},
})

15
src/main/lib/dock.ts Normal file
View File

@ -0,0 +1,15 @@
import { app } from "electron"
export const setDockCount = (
input: number,
) => {
if (app.dock) {
if (input === 0) {
app.dock.setBadge("")
} else {
app.dock.setBadge(input.toString())
}
} else {
app.setBadgeCount(input)
}
}

19
src/main/lib/posthog.ts Normal file
View File

@ -0,0 +1,19 @@
import { env } from "@env"
import { PostHog } from "posthog-node"
import { getUser } from "./user"
export const initPosthog = () => {
if (env.VITE_POSTHOG_KEY === undefined) return
return new PostHog(env.VITE_POSTHOG_KEY, {})
}
export const posthog = initPosthog()
// posthog?.debug(true)
export const trackEvent = (event: string, properties?: Record<string, any>) => {
const userId = getUser()?.id
if (!userId) return
posthog?.capture({ event, properties, distinctId: userId })
}

1
src/main/lib/until.ts Normal file
View File

@ -0,0 +1 @@
export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

17
src/main/lib/user.ts Normal file
View File

@ -0,0 +1,17 @@
import type { User } from "@auth/core/types"
import { store } from "./store"
const AuthKey = "authSessionToken"
export const setAuthSessionToken = (token: string) => store.set(AuthKey, token)
export const getAuthSessionToken = (): string | null => store.get(AuthKey)
export const cleanAuthSessionToken = () => store.set(AuthKey, null)
const UserKey = "user"
export const setUser = (user: User) => store.set(UserKey, JSON.stringify(user))
export const getUser = (): User | null => {
const user = store.get(UserKey)
return user ? JSON.parse(user) : null
}
export const cleanUser = () => store.set(UserKey, null)

View File

@ -1,16 +0,0 @@
import { env } from "@env"
import Posthog from "posthog-js"
export const initPosthog = () => {
if (env.VITE_POSTHOG_KEY === undefined) return
Posthog.init(env.VITE_POSTHOG_KEY, {
person_profiles: "identified_only",
debug: true,
})
return Posthog
}
export const posthog = initPosthog()
export const trackEvent = (event: string, properties?: Record<string, any>) => {
posthog?.capture(event, properties)
}

View File

@ -1,13 +1,13 @@
import { getRendererHandlers } from "@egoist/tipc/main"
import type { BrowserWindow } from "electron"
import { cleanAuthSessionToken, cleanUser } from "../lib/user"
import type { RendererHandlers } from "../renderer-handlers"
import { quitAndInstall } from "../updater"
import { getMainWindow } from "../window"
import { t } from "./_instance"
export const appRoute = {
saveToEagle: t.procedure
.input<{ url: string, mediaUrls: string[] }>()
.action(async ({ input }) => {
@ -76,6 +76,11 @@ export const appRoute = {
quitAndInstall: t.procedure.action(async () => {
quitAndInstall()
}),
cleanAuthSessionToken: t.procedure.action(async () => {
cleanAuthSessionToken()
cleanUser()
}),
}
interface Sender extends Electron.WebContents {
getOwnerBrowserWindow: () => Electron.BrowserWindow | null

46
src/main/tipc/dock.ts Normal file
View File

@ -0,0 +1,46 @@
import { UNREAD_BACKGROUND_POLLING_INTERVAL } from "../constants/app"
import { apiClient } from "../lib/api-client"
import { setDockCount } from "../lib/dock"
import { sleep } from "../lib/until"
import { t } from "./_instance"
const timerMap = {
unread: undefined as any,
}
const pollingMap = {
unread: false,
}
export const dockRoute = {
pollingUpdateUnreadCount: t.procedure.action(pollingUpdateUnreadCount),
cancelPollingUpdateUnreadCount: t.procedure.action(
cancelPollingUpdateUnreadCount,
),
updateUnreadCount: t.procedure.action(async () => {
await updateUnreadCount()
}),
}
async function updateUnreadCount() {
const res = await apiClient.reads["total-count"].$get()
setDockCount(res.data.count)
}
export async function pollingUpdateUnreadCount() {
if (timerMap.unread) {
timerMap.unread = clearTimeout(timerMap.unread)
}
pollingMap.unread = true
while (pollingMap.unread) {
await sleep(UNREAD_BACKGROUND_POLLING_INTERVAL)
if (pollingMap.unread) { await updateUnreadCount() }
}
}
export async function cancelPollingUpdateUnreadCount() {
pollingMap.unread = false
timerMap.unread = clearTimeout(timerMap.unread)
}

View File

@ -1,5 +1,6 @@
import { appRoute } from "./app"
import { debugRoute } from "./debug"
import { dockRoute } from "./dock"
import { menuRoute } from "./menu"
import { settingRoute } from "./setting"
import { trackerRoute } from "./tracker"
@ -10,6 +11,7 @@ export const router = {
...settingRoute,
...appRoute,
...trackerRoute,
...dockRoute,
}
export type Router = typeof router

View File

@ -1,46 +1,46 @@
import { app, nativeTheme } from "electron"
import { setDockCount } from "../lib/dock"
import { createSettingWindow } from "../window"
import { t } from "./_instance"
export const settingRoute = { getLoginItemSettings: t.procedure
.input<void>()
.action(async () => await app.getLoginItemSettings()), setLoginItemSettings: t.procedure
.input<boolean>()
.action(async ({ input }) => {
app.setLoginItemSettings({
openAtLogin: input,
})
}), openSettingWindow: t.procedure.action(async () => createSettingWindow()), getSystemFonts: t.procedure.action(
async (): Promise<string[]> =>
new Promise((resolve) => {
// NOTE: should external font-list deps
// use `require` to avoid bundling, vite behavior
require("font-list")
.getFonts()
.then((fonts) => {
resolve(fonts.map((font) => font.replaceAll("\"", "")))
})
export const settingRoute = {
getLoginItemSettings: t.procedure
.input<void>()
.action(async () => await app.getLoginItemSettings()),
setLoginItemSettings: t.procedure
.input<boolean>()
.action(async ({ input }) => {
app.setLoginItemSettings({
openAtLogin: input,
})
}),
), setAppearance: t.procedure
.input<"light" | "dark" | "system">()
.action(async ({ input }) => {
// NOTE: Temporarily changing to system to get the color mode that system is in at the moment may cause a bit of a problem.
// On macos, there is a bug, traffic lights flicker
nativeTheme.themeSource = "system"
const systemColorMode = nativeTheme.shouldUseDarkColors ?
"dark" :
"light"
openSettingWindow: t.procedure.action(async () => createSettingWindow()),
getSystemFonts: t.procedure.action(
async (): Promise<string[]> =>
new Promise((resolve) => {
// NOTE: should external font-list deps
// use `require` to avoid bundling, vite behavior
require("font-list")
.getFonts()
.then((fonts) => {
resolve(fonts.map((font) => font.replaceAll("\"", "")))
})
}),
),
setAppearance: t.procedure
.input<"light" | "dark" | "system">()
.action(async ({ input }) => {
// NOTE: Temporarily changing to system to get the color mode that system is in at the moment may cause a bit of a problem.
// On macos, there is a bug, traffic lights flicker
nativeTheme.themeSource = "system"
const systemColorMode = nativeTheme.shouldUseDarkColors ?
"dark" :
"light"
nativeTheme.themeSource = systemColorMode === input ? "system" : input
}), setDockBadge: t.procedure.input<number>().action(async ({ input }) => {
if (app.dock) {
if (input === 0) {
app.dock.setBadge("")
} else {
app.dock.setBadge(input.toString())
}
} else {
app.setBadgeCount(input)
}
}) }
nativeTheme.themeSource = systemColorMode === input ? "system" : input
}),
setDockBadge: t.procedure.input<number>().action(async ({ input }) => {
setDockCount(input)
}),
}

View File

@ -1,6 +1,7 @@
import type { User } from "@auth/core/types"
import { posthog } from "../posthog"
import { posthog } from "../lib/posthog"
import { setUser } from "../lib/user"
import { t } from "./_instance"
export const trackerRoute = {
@ -11,9 +12,14 @@ export const trackerRoute = {
.action(async ({ input }) => {
const { user } = input
posthog?.identify(user.id, {
name: user.name,
handle: user.handle,
setUser(user)
posthog?.capture({
event: "login",
distinctId: user.id,
properties: {
name: user.name,
handle: user.handle,
},
})
}),
}

View File

@ -2,8 +2,8 @@ import { getRendererHandlers } from "@egoist/tipc/main"
import { autoUpdater } from "electron-updater"
import { channel, isDev } from "../env"
import { trackEvent } from "../lib/posthog"
import { logger } from "../logger"
import { trackEvent } from "../posthog"
import type { RendererHandlers } from "../renderer-handlers"
import { getMainWindow } from "../window"
import { CustomGitHubProvider } from "./custom-github-provider"

View File

@ -7,7 +7,11 @@ import type { BrowserWindowConstructorOptions } from "electron"
import { BrowserWindow, shell } from "electron"
import { getIconPath } from "./helper"
import { store } from "./store"
import { store } from "./lib/store"
import {
cancelPollingUpdateUnreadCount,
pollingUpdateUnreadCount,
} from "./tipc/dock"
const windows = {
settingWindow: null as BrowserWindow | null,
@ -144,6 +148,18 @@ export const createMainWindow = () => {
}
})
window.on("show", () => {
cancelPollingUpdateUnreadCount()
})
window.on("hide", async () => {
const settings = await callGlobalContextMethod(window, "getUISettings")
if (settings.showDockBadge) {
pollingUpdateUnreadCount()
}
})
return window
}

View File

@ -1,8 +1,9 @@
import { jotaiStore } from "@renderer/lib/jotai"
import type { GeneralSettings } from "@shared/interface/settings"
import { createSettingAtom } from "./helper"
const createDefaultSettings = () => ({
const createDefaultSettings = (): GeneralSettings => ({
// App
appLaunchOnStartup: false,
// Data control
@ -18,6 +19,7 @@ const createDefaultSettings = () => ({
// UX
// autoHideFeedColumn: true,
})
export const {
useSettingKey: useGeneralSettingKey,
useSettingSelector: useGeneralSettingSelector,

View File

@ -5,7 +5,7 @@ import { useAtomValue } from "jotai"
import { atomWithStorage, selectAtom } from "jotai/utils"
import { useMemo } from "react"
export const createSettingAtom = <T extends Record<string, unknown>>(
export const createSettingAtom = <T extends object>(
settingKey: string,
createDefaultSettings: () => T,
) => {

View File

@ -1,6 +1,8 @@
import type { UISettings } from "@shared/interface/settings"
import { createSettingAtom } from "./helper"
const createDefaultSettings = () => ({
const createDefaultSettings = (): UISettings => ({
// Sidebar
entryColWidth: 340,
feedColWidth: 256,
@ -23,6 +25,7 @@ const createDefaultSettings = () => ({
readerRenderInlineStyle: false,
codeHighlightTheme: "github-dark",
})
export const {
useSettingKey: useUISettingKey,
useSettingSelector: useUISettingSelector,

View File

@ -1,5 +1,5 @@
import type { MediaModel } from "@renderer/hono"
import { useCallback } from "react"
import type { MediaModel } from "src/hono"
import { useModalStack } from "../modal/stacked/hooks"
import { NoopChildren } from "../modal/stacked/utils"

View File

@ -1,11 +1,11 @@
import { m } from "@renderer/components/common/Motion"
import { COPY_MAP } from "@renderer/constants"
import type { MediaModel } from "@renderer/hono"
import { tipcClient } from "@renderer/lib/client"
import { stopPropagation } from "@renderer/lib/dom"
import { showNativeMenu } from "@renderer/lib/native-menu"
import type { FC } from "react"
import { useCallback, useState } from "react"
import type { MediaModel } from "src/hono"
import { Keyboard, Mousewheel, Scrollbar } from "swiper/modules"
import { Swiper, SwiperSlide } from "swiper/react"

View File

@ -3,11 +3,11 @@ import "swiper/css/navigation"
import "swiper/css/scrollbar"
import { Media } from "@renderer/components/ui/media"
import type { MediaModel } from "@renderer/hono"
import { cn } from "@renderer/lib/utils"
import { useHover } from "@use-gesture/react"
import { uniqBy } from "lodash-es"
import { useRef, useState } from "react"
import type { MediaModel } from "src/hono"
import { Mousewheel, Navigation, Scrollbar } from "swiper/modules"
import { Swiper, SwiperSlide } from "swiper/react"

File diff suppressed because it is too large Load Diff

1
src/renderer/src/hono.ts Symbolic link
View File

@ -0,0 +1 @@
../../hono.ts

View File

@ -1,6 +1,7 @@
import { signOut } from "@hono/auth-js/react"
import { setMe } from "@renderer/atoms/user"
import { QUERY_PERSIST_KEY } from "@renderer/constants"
import { tipcClient } from "@renderer/lib/client"
import { clearStorage } from "@renderer/lib/ns"
import { clearLocalPersistStoreData } from "@renderer/store/utils/clear"
import { useCallback } from "react"
@ -19,7 +20,7 @@ export const useSignOut = () =>
// Clear local storage
clearStorage()
window.posthog?.reset()
tipcClient?.cleanAuthSessionToken()
// Sign out
await signOut()
}, [])

View File

@ -1,5 +1,8 @@
export * from "./useBizQuery"
export * from "./useDark"
export * from "./useInputComposition"
export * from "./useIsOnline"
export * from "./usePageVisibility"
export * from "./usePrevious"
export * from "./useRefValue"
export * from "./useTitle"

View File

@ -0,0 +1,56 @@
import { useEffect, useRef } from "react"
import { useIsomorphicLayoutEffect } from "usehooks-ts"
export function useAccurateInterval(
callback: () => void,
options: {
delay: number
enable?: boolean
immediately?: boolean
},
) {
const { delay, enable = true, immediately = true } = options
const savedCallback = useRef(callback)
const nextTick = useRef(Date.now() + (delay || 0))
const triggerCountRef = useRef(0)
const timerRef = useRef<any | null | undefined>(null)
// Remember the latest callback if it changes.
useIsomorphicLayoutEffect(() => {
savedCallback.current = callback
}, [callback])
// Set up the interval.
useEffect(() => {
if (!enable) return
// Don't schedule if no delay is specified.
// Note: 0 is a valid value for delay.
if (!delay && delay !== 0) {
return
}
function tick() {
if (immediately || triggerCountRef.current > 0) {
savedCallback.current()
}
triggerCountRef.current++
const now = Date.now()
const expectedNextTick = nextTick.current
const actualDelay = Math.max(0, expectedNextTick - now)
// Compensate for the time taken by the task
nextTick.current = now + delay + actualDelay
timerRef.current = setTimeout(tick, nextTick.current - now)
}
tick()
return () => {
nextTick.current = Date.now() // Reset for the next run
timerRef.current = clearTimeout(timerRef.current)
}
}, [delay, enable, immediately])
}

View File

@ -0,0 +1,17 @@
import { useLayoutEffect, useState } from "react"
export function usePageVisibility() {
const [isPageVisible, setIsPageVisible] = useState(!document.hidden)
useLayoutEffect(() => {
const handleVisibility = () => {
setIsPageVisible(!document.hidden)
}
document.addEventListener("visibilitychange", handleVisibility)
return () => {
document.removeEventListener("visibilitychange", handleVisibility)
}
}, [])
return isPageVisible
}

View File

@ -1,6 +1,7 @@
import { env } from "@env"
import { authConfigManager } from "@hono/auth-js/react"
import { repository } from "@pkg"
import { getUISettings } from "@renderer/atoms/settings/ui"
import { browserDB } from "@renderer/database"
import { CleanerService } from "@renderer/services/cleaner"
import { registerGlobalContext } from "@shared/bridge"
@ -58,6 +59,8 @@ export const initializeApp = async () => {
registerGlobalContext({
showSetting: () => window.router.showSettings(),
getGeneralSettings,
getUISettings,
toast,
})

View File

@ -2,9 +2,9 @@ import { getCsrfToken } from "@hono/auth-js/react"
import PKG from "@pkg"
import { NetworkStatus, setApiStatus } from "@renderer/atoms/network"
import { setLoginModalShow } from "@renderer/atoms/user"
import type { AppType } from "@renderer/hono"
import { hc } from "hono/client"
import { FetchError, ofetch } from "ofetch"
import type { AppType } from "src/hono"
let csrfTokenPromise: Promise<string> | null = null
export const apiFetch = ofetch.create({

View File

@ -9,7 +9,7 @@ import { QUERY_PERSIST_KEY } from "../constants/app"
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: Infinity,
refetchOnWindowFocus: false,
retryDelay: 1000,
retry(failureCount, error) {
console.error(error)

View File

@ -1,5 +1,5 @@
import type { users } from "@renderer/hono"
import type { apiClient } from "@renderer/lib/api-fetch"
import type { users } from "src/hono"
export type UserModel = Omit<typeof users.$inferSelect, "emailVerified">

View File

@ -1,16 +1,22 @@
import { env } from "@env"
import {
Tooltip,
TooltipContent,
TooltipPortal,
TooltipTrigger,
} from "@renderer/components/ui/tooltip"
export const EnvironmentIndicator = () => (
<Tooltip>
<TooltipTrigger>
<TooltipTrigger asChild>
<div className="fixed bottom-0 left-0 rounded-tr bg-theme-accent px-1 py-0.5 text-xs text-white">
{import.meta.env.MODE}
</div>
</TooltipTrigger>
<TooltipContent>{import.meta.env.BASE_URL}</TooltipContent>
<TooltipPortal>
<TooltipContent className="max-w-max break-all" side="top">
<pre>{JSON.stringify({ ...env }, null, 2)}</pre>
</TooltipContent>
</TooltipPortal>
</Tooltip>
)

View File

@ -50,10 +50,7 @@ const useBackHome = (active: number) => {
}
const useUnreadByView = () => {
useAuthQuery(Queries.subscription.byView(), {
refetchOnWindowFocus: false,
refetchInterval: false,
})
useAuthQuery(Queries.subscription.byView())
const idByView = useSubscriptionStore((state) => state.feedIdByView)
const totalUnread = useFeedUnreadStore((state) => {
const unread = {} as Record<number, number>

View File

@ -38,6 +38,28 @@ const useGroupedData = (view: FeedViewType) => {
return groupFolder
}, [data])
}
const useUpdateUnreadCount = () => {
useAuthQuery(Queries.subscription.unreadAll(), {
refetchInterval: false,
})
// const isPageVisible = usePageVisibility()
// useAccurateInterval(
// () => {
// if (!isPageVisible) {
// refetch()
// }
// },
// {
// enable: !isPageVisible,
// delay:
// // 10s
// 10_000,
// },
// )
}
export function FeedList({
className,
view,
@ -48,7 +70,7 @@ export function FeedList({
const [expansion, setExpansion] = useState(false)
const data = useGroupedData(view)
useAuthQuery(Queries.subscription.unreadAll())
useUpdateUnreadCount()
const totalUnread = useFeedUnreadStore((state) => {
let unread = 0

View File

@ -0,0 +1,27 @@
import { usePageVisibility } from "@renderer/hooks/common"
import { appLog } from "@renderer/lib/log"
import { useQueryClient } from "@tanstack/react-query"
import { useEffect, useRef } from "react"
/**
* InvalidateQuery when electron app window switch to visible, macOS dock icon click
*/
export const InvalidateQueryProvider = () => {
const queryClient = useQueryClient()
const currentVisibilityRef = useRef(!document.hidden)
const pageVisibility = usePageVisibility()
useEffect(() => {
if (currentVisibilityRef.current === pageVisibility) {
return
}
currentVisibilityRef.current = pageVisibility
if (pageVisibility) {
appLog("Window switch to visible, invalidate all queries")
queryClient.invalidateQueries()
}
}, [pageVisibility, queryClient])
return null
}

View File

@ -12,6 +12,7 @@ import { HotkeysProvider } from "react-hotkeys-hook"
import { ContextMenuProvider } from "./context-menu-provider"
import { EventProvider } from "./event-provider"
import { InvalidateQueryProvider } from "./invalidate-query-provider"
import { StableRouterProvider } from "./stable-router-provider"
import { SettingSync } from "./ui-setting-sync"
import { UserProvider } from "./user-provider"
@ -31,6 +32,7 @@ export const RootProviders: FC<PropsWithChildren> = ({ children }) => (
persistOptions={persistConfig}
client={queryClient}
>
<TooltipProvider>
<HotkeysProvider initiallyActiveScopes={["home"]}>
<Provider store={jotaiStore}>
@ -45,6 +47,7 @@ export const RootProviders: FC<PropsWithChildren> = ({ children }) => (
</Provider>
</HotkeysProvider>
</TooltipProvider>
<InvalidateQueryProvider />
</PersistQueryClientProvider>
</MotionConfig>
<Toaster />

View File

@ -1,8 +1,8 @@
// @ts-nocheck
import { browserDB } from "@renderer/database"
import type { FeedModel } from "@renderer/hono"
import type { EntryModel } from "@renderer/models"
import type { SubscriptionFlatModel } from "@renderer/store/subscription"
import type { FeedModel } from "src/hono"
import { beforeAll, describe, expect, test } from "vitest"
import { CleanerService } from "./cleaner"

View File

@ -2,8 +2,8 @@ import {
FEED_COLLECTION_LIST,
ROUTE_FEED_IN_FOLDER,
} from "@renderer/constants"
import type { EntryReadHistoriesModel } from "@renderer/hono"
import type { FeedViewType } from "@renderer/lib/enum"
import type { EntryReadHistoriesModel } from "src/hono"
import { useShallow } from "zustand/react/shallow"
import { useFeedIdByView, useFolderFeedsByFeedId } from "../subscription"

View File

@ -1,5 +1,4 @@
import { runTransactionInScope } from "@renderer/database"
import type { EntryReadHistoriesModel } from "@renderer/hono"
import { apiClient } from "@renderer/lib/api-fetch"
import {
getEntriesParams,
@ -10,6 +9,7 @@ import type { CombinedEntryModel, EntryModel, FeedModel, UserModel,
import { EntryService } from "@renderer/services"
import { produce } from "immer"
import { merge, omit } from "lodash-es"
import type { EntryReadHistoriesModel } from "src/hono"
import { feedActions } from "../feed"
import { feedUnreadActions } from "../unread"

View File

@ -1,6 +1,6 @@
import type { EntryReadHistoriesModel } from "@renderer/hono"
import type { FeedViewType } from "@renderer/lib/enum"
import type { CombinedEntryModel } from "@renderer/models"
import type { EntryReadHistoriesModel } from "src/hono"
type FeedId = string
type EntryId = string

View File

@ -1,10 +1,13 @@
import type { BrowserWindow } from "electron"
import type { toast } from "sonner"
import type { GeneralSettings, UISettings } from "./interface/settings"
const PREFIX = "__follow"
interface RenderGlobalContext {
showSetting: () => void
getGeneralSettings: () => GeneralSettings
getUISettings: () => UISettings
toast: typeof toast
}
@ -12,19 +15,19 @@ export const registerGlobalContext = (context: RenderGlobalContext) => {
globalThis[PREFIX] = context
}
export function callGlobalContextMethod(
window: BrowserWindow,
method: string,
args?: any[]
): void
export function callGlobalContextMethod<T extends keyof RenderGlobalContext>(
window: BrowserWindow,
method: T,
// @ts-expect-error
args: Parameters<RenderGlobalContext[T]> = [] as any
): Promise<ReturnType<RenderGlobalContext[T]>>
export function callGlobalContextMethod(
window: BrowserWindow,
method: string,
args?: any[]
): void
export function callGlobalContextMethod<T extends keyof RenderGlobalContext>(
window: BrowserWindow,
method: T,

View File

@ -0,0 +1,25 @@
export interface GeneralSettings {
appLaunchOnStartup: boolean
dataPersist: boolean
sendAnonymousData: boolean
unreadOnly: boolean
scrollMarkUnread: boolean
hoverMarkUnread: boolean
renderMarkUnread: boolean
}
export interface UISettings {
entryColWidth: number
feedColWidth: number
opaqueSidebar: boolean
sidebarShowUnreadCount: boolean
uiTextSize: number
showDockBadge: boolean
modalOverlay: boolean
modalDraggable: boolean
modalOpaque: boolean
reduceMotion: boolean
readerFontFamily: string
readerRenderInlineStyle: boolean
codeHighlightTheme: string
}

View File

@ -6,7 +6,8 @@
"src/preload/**/*",
"types/**/*.d.ts",
"src/shared/src/**/*",
"src/env.ts"
"src/env.ts",
"./src/hono.ts"
],
"compilerOptions": {
"composite": true,
@ -21,6 +22,9 @@
],
"@env": [
"./src/env.ts"
],
"@pkg": [
"./package.json"
]
}
}

View File

@ -7,7 +7,8 @@
"src/preload/*.d.ts",
"src/shared/src/**/*",
"types/**/*.d.ts",
"src/env.ts"
"src/env.ts",
"src/hono.ts"
],
"compilerOptions": {
"composite": true,
@ -27,8 +28,12 @@
"@shared/*": [
"src/shared/src/*"
],
"@pkg": ["./package.json"],
"@env": ["./src/env.ts"]
"@pkg": [
"./package.json"
],
"@env": [
"./src/env.ts"
]
}
}
}