diff --git a/apps/desktop/layer/main/export.ts b/apps/desktop/layer/main/export.ts index 260b06619..567ca07bc 100644 --- a/apps/desktop/layer/main/export.ts +++ b/apps/desktop/layer/main/export.ts @@ -1,5 +1,2 @@ // Export types for renderer to use export type { IpcServices } from "./src/ipc" - -// Export services for potential main process use -export { services } from "./src/ipc" diff --git a/apps/desktop/layer/main/package.json b/apps/desktop/layer/main/package.json index 5c14baebe..3755d9136 100644 --- a/apps/desktop/layer/main/package.json +++ b/apps/desktop/layer/main/package.json @@ -30,7 +30,7 @@ "@sentry/electron": "6.8.0", "builder-util-runtime": "9.3.1", "electron-context-menu": "4.0.5", - "electron-ipc-decorator": "0.1.3", + "electron-ipc-decorator": "0.2.0", "electron-log": "5.4.1", "electron-squirrel-startup": "1.0.1", "electron-store": "10.1.0", diff --git a/apps/desktop/layer/main/src/ipc/index.ts b/apps/desktop/layer/main/src/ipc/index.ts index 3b17a4692..635c80e0c 100644 --- a/apps/desktop/layer/main/src/ipc/index.ts +++ b/apps/desktop/layer/main/src/ipc/index.ts @@ -1,4 +1,5 @@ import type { MergeIpcService } from "electron-ipc-decorator" +import { createServices } from "electron-ipc-decorator" import { AppService } from "./services/app" import { AuthService } from "./services/auth" @@ -10,17 +11,16 @@ import { ReaderService } from "./services/reader" import { SettingService } from "./services/setting" // Initialize all services -export const services = { - app: new AppService(), - auth: new AuthService(), - debug: new DebugService(), - dock: new DockService(), - menu: new MenuService(), - reader: new ReaderService(), - setting: new SettingService(), - integration: new IntegrationService(), -} as const - +const services = createServices([ + AppService, + AuthService, + DebugService, + DockService, + MenuService, + ReaderService, + SettingService, + IntegrationService, +]) // Extract method types automatically from services export type IpcServices = MergeIpcService @@ -28,4 +28,5 @@ export type IpcServices = MergeIpcService export function initializeIpcServices() { // Services are already initialized in the services constant above console.info("IPC services initialized") + void services } diff --git a/apps/desktop/layer/main/src/ipc/services/app.ts b/apps/desktop/layer/main/src/ipc/services/app.ts index 8966a632e..a9b654912 100644 --- a/apps/desktop/layer/main/src/ipc/services/app.ts +++ b/apps/desktop/layer/main/src/ipc/services/app.ts @@ -8,6 +8,7 @@ import type { IpcContext } from "electron-ipc-decorator" import { IpcMethod, IpcService } from "electron-ipc-decorator" import path from "pathe" +import { START_IN_TRAY_ARGS } from "~/constants/app" import { getCacheSize } from "~/lib/cleaner" import { i18n } from "~/lib/i18n" import { store, StoreKey } from "~/lib/store" @@ -34,9 +35,7 @@ interface Sender extends Electron.WebContents { } export class AppService extends IpcService { - constructor() { - super("app") - } + static override readonly groupName = "app" @IpcMethod() getAppVersion(): string { @@ -183,6 +182,16 @@ export class AppService extends IpcService { return path.join(app.getAppPath(), input) } + @IpcMethod() + readyToShowMainWindow(_context: IpcContext) { + const shouldShowWindow = + !app.getLoginItemSettings().wasOpenedAsHidden && !process.argv.includes(START_IN_TRAY_ARGS) + if (shouldShowWindow) { + const window = WindowManager.getMainWindow() + if (window) window.show() + } + } + @IpcMethod() openCacheFolder(_context: IpcContext): void { const dir = path.join(app.getPath("userData"), "cache") @@ -217,22 +226,6 @@ export class AppService extends IpcService { }) } - // getCacheLimit: t.procedure.action(async () => { - // return store.get(StoreKey.CacheSizeLimit) - // }), - - // clearCache: t.procedure.action(async () => { - - // }), - - // limitCacheSize: t.procedure.input().action(async ({ input }) => { - // logger.info("set limitCacheSize", input) - // if (input === 0) { - // store.delete(StoreKey.CacheSizeLimit) - // } else { - // store.set(StoreKey.CacheSizeLimit, input) - // } - // }), @IpcMethod() limitCacheSize(_context: IpcContext, input: number): void { if (input === 0) { diff --git a/apps/desktop/layer/main/src/ipc/services/auth.ts b/apps/desktop/layer/main/src/ipc/services/auth.ts index 4c5a57c59..ec4b27790 100644 --- a/apps/desktop/layer/main/src/ipc/services/auth.ts +++ b/apps/desktop/layer/main/src/ipc/services/auth.ts @@ -4,9 +4,7 @@ import { IpcMethod, IpcService } from "electron-ipc-decorator" import { deleteNotificationsToken, updateNotificationsToken } from "../../lib/user" export class AuthService extends IpcService { - constructor() { - super("auth") - } + static override readonly groupName = "auth" @IpcMethod() async sessionChanged(_context: IpcContext): Promise { diff --git a/apps/desktop/layer/main/src/ipc/services/debug.ts b/apps/desktop/layer/main/src/ipc/services/debug.ts index 7969d7136..028d61fc4 100644 --- a/apps/desktop/layer/main/src/ipc/services/debug.ts +++ b/apps/desktop/layer/main/src/ipc/services/debug.ts @@ -7,9 +7,7 @@ interface InspectElementInput { } export class DebugService extends IpcService { - constructor() { - super("debug") - } + static override readonly groupName = "debug" @IpcMethod() inspectElement(context: IpcContext, input: InspectElementInput): void { diff --git a/apps/desktop/layer/main/src/ipc/services/dock.ts b/apps/desktop/layer/main/src/ipc/services/dock.ts index 994a20fba..04f2885ab 100644 --- a/apps/desktop/layer/main/src/ipc/services/dock.ts +++ b/apps/desktop/layer/main/src/ipc/services/dock.ts @@ -59,9 +59,7 @@ class PollingManager { export class DockService extends IpcService { private unreadPollingManager = new PollingManager() - constructor() { - super("dock") - } + static override readonly groupName = "dock" @IpcMethod() async pollingUpdateUnreadCount(): Promise { diff --git a/apps/desktop/layer/main/src/ipc/services/integration.ts b/apps/desktop/layer/main/src/ipc/services/integration.ts index b2b6bd85f..035fb9035 100644 --- a/apps/desktop/layer/main/src/ipc/services/integration.ts +++ b/apps/desktop/layer/main/src/ipc/services/integration.ts @@ -54,9 +54,7 @@ interface CustomFetchInput { } export class IntegrationService extends IpcService { - constructor() { - super("integration") - } + static override readonly groupName = "integration" @IpcMethod() async saveToObsidian( diff --git a/apps/desktop/layer/main/src/ipc/services/menu.ts b/apps/desktop/layer/main/src/ipc/services/menu.ts index 8440323f0..9a01c7c2f 100644 --- a/apps/desktop/layer/main/src/ipc/services/menu.ts +++ b/apps/desktop/layer/main/src/ipc/services/menu.ts @@ -18,9 +18,7 @@ interface ShowConfirmDialogInput { } export class MenuService extends IpcService { - constructor() { - super("menu") - } + static override readonly groupName = "menu" private normalizeMenuItems( items: SerializableMenuItem[], diff --git a/apps/desktop/layer/main/src/ipc/services/reader.ts b/apps/desktop/layer/main/src/ipc/services/reader.ts index 5ea01cfc0..3c25268d8 100644 --- a/apps/desktop/layer/main/src/ipc/services/reader.ts +++ b/apps/desktop/layer/main/src/ipc/services/reader.ts @@ -29,9 +29,7 @@ interface DetectCodeStringLanguageInput { } export class ReaderService extends IpcService { - constructor() { - super("reader") - } + static override readonly groupName = "reader" @IpcMethod() async readability(_context: IpcContext, input: ReadabilityInput) { diff --git a/apps/desktop/layer/main/src/ipc/services/setting.ts b/apps/desktop/layer/main/src/ipc/services/setting.ts index daca45d87..1e943a7b1 100644 --- a/apps/desktop/layer/main/src/ipc/services/setting.ts +++ b/apps/desktop/layer/main/src/ipc/services/setting.ts @@ -20,9 +20,7 @@ interface SetLoginItemSettingsInput { } export class SettingService extends IpcService { - constructor() { - super("setting") - } + static override readonly groupName = "setting" @IpcMethod() getLoginItemSettings(_context: IpcContext): Electron.LoginItemSettings { diff --git a/apps/desktop/layer/main/src/manager/window.ts b/apps/desktop/layer/main/src/manager/window.ts index e214364e8..d5d1530a0 100644 --- a/apps/desktop/layer/main/src/manager/window.ts +++ b/apps/desktop/layer/main/src/manager/window.ts @@ -5,11 +5,10 @@ import { LEGACY_APP_PROTOCOL } from "@follow/shared" import { callWindowExpose, WindowState } from "@follow/shared/bridge" import { APP_PROTOCOL, DEV } from "@follow/shared/constants" import type { BrowserWindowConstructorOptions } from "electron" -import { app, BrowserWindow, screen, shell } from "electron" +import { BrowserWindow, screen, shell } from "electron" import type { Event } from "electron/main" import path from "pathe" -import { START_IN_TRAY_ARGS } from "~/constants/app" import { isMacOS, isWindows, isWindows11 } from "~/env" import { filePathToAppUrl, getIconPath } from "~/helper" import { t } from "~/lib/i18n" @@ -83,12 +82,6 @@ class WindowManagerStatic { refreshBound(window, this.config.refreshBoundDelay) }) - window.on("ready-to-show", () => { - const shouldShowWindow = - !app.getLoginItemSettings().wasOpenedAsHidden && !process.argv.includes(START_IN_TRAY_ARGS) - if (shouldShowWindow) window.show() - }) - window.webContents.setWindowOpenHandler((details) => { shell.openExternal(details.url) return { action: "deny" } diff --git a/apps/desktop/layer/renderer/package.json b/apps/desktop/layer/renderer/package.json index ed0fcdff5..5f91fcc9b 100644 --- a/apps/desktop/layer/renderer/package.json +++ b/apps/desktop/layer/renderer/package.json @@ -59,7 +59,7 @@ "cookie-es": "2.0.0", "dayjs": "1.11.13", "dnum": "2.15.0", - "electron-ipc-decorator": "0.1.3", + "electron-ipc-decorator": "0.2.0", "embla-carousel-react": "8.6.0", "embla-carousel-wheel-gestures": "8.0.2", "es-toolkit": "1.39.6", diff --git a/apps/desktop/layer/renderer/src/App.tsx b/apps/desktop/layer/renderer/src/App.tsx index 136a0308a..2f0d8b9b7 100644 --- a/apps/desktop/layer/renderer/src/App.tsx +++ b/apps/desktop/layer/renderer/src/App.tsx @@ -1,14 +1,16 @@ import { isMobile } from "@follow/components/hooks/useMobile.js" import { IN_ELECTRON } from "@follow/shared/constants" import { tracker } from "@follow/tracker" +import { nextFrame } from "@follow/utils" import { cn, getOS } from "@follow/utils/utils" -import { useEffect } from "react" +import { useEffect, useLayoutEffect, useRef } from "react" import { Outlet } from "react-router" import { useAppIsReady } from "./atoms/app" import { useUISettingKey } from "./atoms/settings/ui" import { applyAfterReadyCallbacks } from "./initialize/queue" import { removeAppSkeleton } from "./lib/app" +import { ipcServices } from "./lib/client" import { appLog } from "./lib/log" import { Titlebar } from "./modules/app/Titlebar" import { RootProviders } from "./providers/root-providers" @@ -37,13 +39,19 @@ function App() { const AppLayer = () => { const appIsReady = useAppIsReady() - useEffect(() => { - removeAppSkeleton() + const onceReady = useRef(false) + useLayoutEffect(() => { + if (appIsReady && !onceReady.current) { + onceReady.current = true + ipcServices?.app.readyToShowMainWindow() + nextFrame(removeAppSkeleton) + } + }, [appIsReady]) + useEffect(() => { const doneTime = Math.trunc(performance.now()) tracker.uiRenderInit(doneTime) appLog("App is ready", `${doneTime}ms`) - applyAfterReadyCallbacks() if (isMobile()) { diff --git a/apps/desktop/layer/renderer/src/lib/load-language.ts b/apps/desktop/layer/renderer/src/lib/load-language.ts index b572d0507..70a7e9836 100644 --- a/apps/desktop/layer/renderer/src/lib/load-language.ts +++ b/apps/desktop/layer/renderer/src/lib/load-language.ts @@ -79,8 +79,7 @@ export const loadLanguageAndApply = async (lang: string) => { if (IN_ELECTRON) { importFilePath = - (await (ipcServices as any)?.app.resolveAppAsarPath(`dist/renderer/locales/${lang}.js`)) || - "" + (await ipcServices?.app.resolveAppAsarPath(`dist/renderer/locales/${lang}.js`)) || "" } else { importFilePath = `/locales/${lang}.js` } diff --git a/apps/desktop/layer/renderer/src/main.tsx b/apps/desktop/layer/renderer/src/main.tsx index 20c75d171..a3f0884f1 100644 --- a/apps/desktop/layer/renderer/src/main.tsx +++ b/apps/desktop/layer/renderer/src/main.tsx @@ -11,6 +11,7 @@ import { } from "@follow/store/context" import { getOS } from "@follow/utils/utils" import * as React from "react" +import { flushSync } from "react-dom" import ReactDOM from "react-dom/client" import { RouterProvider } from "react-router/dom" @@ -37,7 +38,8 @@ initializeApp().finally(() => { } }) - setAppIsReady(true) + // eslint-disable-next-line @eslint-react/dom/no-flush-sync + flushSync(() => setAppIsReady(true)) }) const $container = document.querySelector("#root") as HTMLElement diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 536851861..66364124e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -373,8 +373,8 @@ importers: specifier: 4.0.5 version: 4.0.5(patch_hash=15ed04a0d246eb9e701f47314b91328c64a8a7374914ad58b4053a3f84075fab) electron-ipc-decorator: - specifier: 0.1.3 - version: 0.1.3(electron@37.2.0) + specifier: 0.2.0 + version: 0.2.0(electron@37.2.0) electron-log: specifier: 5.4.1 version: 5.4.1 @@ -596,8 +596,8 @@ importers: specifier: 2.15.0 version: 2.15.0 electron-ipc-decorator: - specifier: 0.1.3 - version: 0.1.3(electron@37.2.0) + specifier: 0.2.0 + version: 0.2.0(electron@37.2.0) embla-carousel-react: specifier: 8.6.0 version: 8.6.0(react@19.0.0) @@ -8757,8 +8757,8 @@ packages: engines: {node: '>= 16'} hasBin: true - electron-ipc-decorator@0.1.3: - resolution: {integrity: sha512-rMmWUc6OFuIXw+e5DEAIsgU0leC+kJ2ibVEwzRDEjxuQvAqsaZJztFrbtUS44kDdIQriRId0lFKYlDJAMm3yhQ==} + electron-ipc-decorator@0.2.0: + resolution: {integrity: sha512-7020pQ/8qdm+CkVQRQG1A0zhWERI4OFyCnULtcPFv4JJRE1rwfBKrKwWIxsoeFpjBEGEV1M7Ms5FGt2jLrMY5g==} engines: {node: '>=18.0.0'} peerDependencies: electron: '>=32.0.0' @@ -24728,7 +24728,7 @@ snapshots: - supports-color optional: true - electron-ipc-decorator@0.1.3(electron@37.2.0): + electron-ipc-decorator@0.2.0(electron@37.2.0): dependencies: electron: 37.2.0