diff --git a/apps/web/src/desktop-cache.ts b/apps/web/src/desktop-cache.ts index 3aec002..b1e091a 100644 --- a/apps/web/src/desktop-cache.ts +++ b/apps/web/src/desktop-cache.ts @@ -1,4 +1,3 @@ -import { invoke } from "@tauri-apps/api/core"; import { isDesktopRuntime } from "./desktop-runtime"; export type DesktopCacheKind = "navigation" | "inbox" | "search" | "conversation"; @@ -9,6 +8,7 @@ const desktopOfflineKey = "mailflow.desktop.offline"; async function invokeDesktop(command: string, arguments_: Record = {}) { if (!isDesktopRuntime()) return undefined; try { + const { invoke } = await import("@tauri-apps/api/core"); return await invoke(command, arguments_); } catch { return undefined; @@ -68,6 +68,7 @@ export async function writeDesktopCache( export async function removeDesktopAccount(accountId: string) { if (!isDesktopRuntime()) return; + const { invoke } = await import("@tauri-apps/api/core"); await invoke("offline_cache_remove_account", { accountId }); } diff --git a/apps/web/src/desktop-session.ts b/apps/web/src/desktop-session.ts index d062c18..05899dd 100644 --- a/apps/web/src/desktop-session.ts +++ b/apps/web/src/desktop-session.ts @@ -1,4 +1,3 @@ -import { invoke } from "@tauri-apps/api/core"; import { isDesktopRuntime } from "./desktop-runtime"; import type { Locale } from "./i18n"; @@ -9,6 +8,11 @@ type NativeSession = { let cachedAccessToken: { token: string; expiresAt: number } | null = null; +async function invokeDesktop(command: string, arguments_?: Record) { + const { invoke } = await import("@tauri-apps/api/core"); + return arguments_ ? invoke(command, arguments_) : invoke(command); +} + function cacheToken(session: NativeSession) { const encoded = session.accessToken.split(".")[1]; if (!encoded) return; @@ -28,7 +32,7 @@ function cacheToken(session: NativeSession) { export async function nativeRequestHeaders(): Promise> { if (!isDesktopRuntime()) return {}; - const installation = await invoke("native_session_identity"); + const installation = await invokeDesktop("native_session_identity"); return { "x-mailflow-native": "1", "x-mailflow-installation": installation }; } @@ -36,14 +40,14 @@ export async function activateDesktopSession(response: Response): Promise("native_session_activate", { sessionToken }); + const session = await invokeDesktop("native_session_activate", { sessionToken }); cacheToken(session); return session; } export async function currentDesktopSession(): Promise { if (!isDesktopRuntime()) return null; - const session = await invoke("native_session_current"); + const session = await invokeDesktop("native_session_current"); if (session) cacheToken(session); else cachedAccessToken = null; return session; @@ -60,7 +64,7 @@ export async function desktopAccessToken(): Promise { export async function logoutDesktopSession() { if (!isDesktopRuntime()) return false; try { - await invoke("native_session_logout"); + await invokeDesktop("native_session_logout"); } finally { cachedAccessToken = null; } @@ -70,7 +74,7 @@ export async function logoutDesktopSession() { export async function forgetDesktopSession() { if (!isDesktopRuntime()) return; try { - await invoke("native_session_forget_local"); + await invokeDesktop("native_session_forget_local"); } finally { cachedAccessToken = null; } @@ -78,11 +82,11 @@ export async function forgetDesktopSession() { export async function beginDesktopPasskey(name: string): Promise { if (!isDesktopRuntime()) return null; - return invoke("native_passkey_begin", { name }); + return invokeDesktop("native_passkey_begin", { name }); } export async function finishDesktopPasskey(name: string, response: unknown): Promise { if (!isDesktopRuntime()) return false; - await invoke("native_passkey_finish", { name, response }); + await invokeDesktop("native_passkey_finish", { name, response }); return true; } diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index fb8afaf..4a70267 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -1,15 +1,32 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { StrictMode } from "react"; +import { lazy, StrictMode, Suspense } from "react"; import { createRoot } from "react-dom/client"; import { createBrowserRouter, RouterProvider } from "react-router"; -import { AdminPage } from "./admin-page"; import { AuthGate } from "./auth-gate"; import { registerDesktopOfflineShell } from "./desktop-cache"; import { initializeDesktopRuntime } from "./desktop-runtime"; +import { type Locale, translate } from "./i18n"; import { AccountsPage, MailPage } from "./pages"; import { initializeTelemetry } from "./telemetry"; import "./styles.css"; +const AdminPage = lazy(async () => ({ default: (await import("./admin-page")).AdminPage })); + +function AdminRoute({ locale }: { locale: Locale }) { + return ( + + + {translate(locale, "admin.loading")} + + } + > + + + ); +} + const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 30_000, retry: 1 }, @@ -22,10 +39,10 @@ initializeTelemetry(); const router = createBrowserRouter([ { path: "/", element: } /> }, - { path: "/admin", element: } /> }, + { path: "/admin", element: } /> }, { path: "/admin/:section", - element: } />, + element: } />, }, { path: "/settings/accounts", diff --git a/apps/web/src/pages.tsx b/apps/web/src/pages.tsx index 44e6a30..422de33 100644 --- a/apps/web/src/pages.tsx +++ b/apps/web/src/pages.tsx @@ -31,13 +31,21 @@ import { Trash2, Users, } from "lucide-react"; -import { type CSSProperties, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + type CSSProperties, + lazy, + Suspense, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { createPortal } from "react-dom"; import { useNavigate } from "react-router"; import { registerPasskey, signOut } from "./auth-client"; import { Button } from "./components/ui/button"; -import { type ComposeContext, ComposePanel } from "./composer"; -import { ConversationView } from "./conversation"; +import type { ComposeContext } from "./composer"; import { desktopConnectivityEvent, isDesktopCacheFallback } from "./desktop-cache"; import { listenForNativeExperience, @@ -86,6 +94,11 @@ import { type SearchSyntaxError, searchSuggestions, validateSearchSyntax } from type Translator = (key: TranslationKey) => string; +const ComposePanel = lazy(async () => ({ default: (await import("./composer")).ComposePanel })); +const ConversationView = lazy(async () => ({ + default: (await import("./conversation")).ConversationView, +})); + const inboxCategories: Array<{ id: MailCategory; labelKey: TranslationKey; @@ -1531,25 +1544,34 @@ export function MailPage({ initialLocale = "en" }: { initialLocale?: Locale }) { />
{activeThreadId && activeAccountId ? ( - setActiveThreadId(null)} - onCompose={(context) => { - setComposeContext(context); - setComposeOpen(true); - setSidebarCollapsed(true); - }} - onAction={(kind, labelId) => { - void runAction(kind, [activeThreadId], labelId); - if (kind === "archive" || kind === "move_to_trash") setActiveThreadId(null); - }} - canActions={canActions} - canCompose={canCompose} - canAttachments={canAttachments} - labelId={actionLabelId} - /> + + + {t("loadingConversation")} + + } + > + setActiveThreadId(null)} + onCompose={(context) => { + setComposeContext(context); + setComposeOpen(true); + setSidebarCollapsed(true); + }} + onAction={(kind, labelId) => { + void runAction(kind, [activeThreadId], labelId); + if (kind === "archive" || kind === "move_to_trash") setActiveThreadId(null); + }} + canActions={canActions} + canCompose={canCompose} + canAttachments={canAttachments} + labelId={actionLabelId} + /> + ) : ( <> {composeOpen && activeAccountId && canCompose && ( - setComposeMaximized((value) => !value)} - onClose={() => { - setComposeOpen(false); - setComposeMaximized(false); - }} - /> + + + {t("loadingComposer")} + + } + > + setComposeMaximized((value) => !value)} + onClose={() => { + setComposeOpen(false); + setComposeMaximized(false); + }} + /> + )} ); diff --git a/services/api/internal/modules/translations/catalogs.json b/services/api/internal/modules/translations/catalogs.json index e7719cd..6fe13c9 100644 --- a/services/api/internal/modules/translations/catalogs.json +++ b/services/api/internal/modules/translations/catalogs.json @@ -95,6 +95,7 @@ "attachmentFailed": "The attachment could not be downloaded.", "displayRemoteImages": "Display remote images", "loadingConversation": "Loading conversation…", + "loadingComposer": "Loading composer…", "conversationFailed": "The conversation could not be loaded.", "emptyConversation": "This conversation has no messages.", "loadMoreMessages": "Load more messages", @@ -464,6 +465,7 @@ "attachmentFailed": "No se ha podido descargar el adjunto.", "displayRemoteImages": "Mostrar imágenes remotas", "loadingConversation": "Cargando conversación…", + "loadingComposer": "Cargando redactor…", "conversationFailed": "No se ha podido cargar la conversación.", "emptyConversation": "Esta conversación no tiene mensajes.", "loadMoreMessages": "Cargar más mensajes",