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:
parent
cdc8703869
commit
917f3a3370
|
|
@ -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"),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: {}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,2 @@
|
|||
// 5min
|
||||
export const UNREAD_BACKGROUND_POLLING_INTERVAL = 1000 * 60 * 5
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}` :
|
||||
"",
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 })
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
../../hono.ts
|
||||
|
|
@ -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()
|
||||
}, [])
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue