diff --git a/crates/tauri-app/src-tauri/tauri.conf.json b/crates/tauri-app/src-tauri/tauri.conf.json index 98c2c19..bdf62e8 100644 --- a/crates/tauri-app/src-tauri/tauri.conf.json +++ b/crates/tauri-app/src-tauri/tauri.conf.json @@ -51,4 +51,4 @@ "icons/icon.png" ] } -} +} \ No newline at end of file diff --git a/crates/tauri-app/src/lib/api/client.test.ts b/crates/tauri-app/src/lib/api/client.test.ts index d4f9c6f..597aae7 100644 --- a/crates/tauri-app/src/lib/api/client.test.ts +++ b/crates/tauri-app/src/lib/api/client.test.ts @@ -22,6 +22,7 @@ const invokeMock = vi.fn(); vi.mock("@tauri-apps/api/core", () => ({ invoke: (...args: unknown[]) => invokeMock(...args), + isTauri: () => true, })); // Capture object URLs so the test can assert the cache is wired up. diff --git a/crates/tauri-app/src/lib/api/client.ts b/crates/tauri-app/src/lib/api/client.ts index d5b2da9..3757024 100644 --- a/crates/tauri-app/src/lib/api/client.ts +++ b/crates/tauri-app/src/lib/api/client.ts @@ -1,6 +1,43 @@ -import { invoke } from "@tauri-apps/api/core"; +import { invoke, isTauri } from "@tauri-apps/api/core"; import { writable } from "svelte/store"; +/** + * Guard the Tauri runtime. When the page is served by `vite dev` + * for browser-only preview (no Tauri webview), `__TAURI_INTERNALS__` + * is undefined and the bare `invoke` call throws + * `TypeError: Cannot read properties of undefined (reading + * 'invoke')`. That error fired inside `onMount` during + * `session.load()` and crashed the entire Svelte mount, leaving + * the user with a white page. We wrap every Tauri call in two + * helpers: + * - `tauriCall(cmd, fallback)` for loads — returns the fallback + * when no Tauri runtime is present (e.g. session.load() returns + * null). + * - `safeInvoke(cmd, args)` for actions that MUST hit the runtime + * (login, register, etc.) — throws a friendly error so the UI + * can show a "running in browser preview" notice. + */ +async function tauriCall(cmd: string, fallback: T, args?: Record): Promise { + if (!isTauri()) { + return fallback; + } + return invoke(cmd, args); +} + +/** + * Strict variant of `tauriCall` for actions that MUST hit the + * Tauri runtime (login, register, logout, post, like, etc.). In + * the browser preview this throws a friendly Error so the UI can + * show a "running in browser preview" notice. In the Tauri + * webview it falls through to a normal `invoke` call. + */ +async function safeInvoke(cmd: string, args?: Record): Promise { + if (!isTauri()) { + throw new Error(`Tauri command ${cmd} requires the desktop runtime`); + } + return invoke(cmd, args); +} + export type Session = { did: string; handle: string; @@ -14,26 +51,28 @@ function createSessionStore() { return { subscribe, async load() { - try { - const s = await invoke("current_session"); - set(s); - } catch (e) { - console.error("current_session failed", e); - } + const s = await tauriCall("current_session", undefined, null); + set(s); }, async login(handle: string, password: string) { - const s = await invoke("auth_login", { identifier: handle, password }); + if (!isTauri()) { + throw new Error("login requires the Tauri desktop runtime"); + } + const s = await safeInvoke("auth_login", { identifier: handle, password }); set(s); return s; }, async register(handle: string, password: string) { - const s = await invoke("auth_register", { handle, password }); + if (!isTauri()) { + throw new Error("register requires the Tauri desktop runtime"); + } + const s = await safeInvoke("auth_register", { handle, password }); set(s); return s; }, async logout() { try { - await invoke("auth_logout"); + await safeInvoke("auth_logout"); } catch (e) { console.error("logout failed", e); } @@ -145,7 +184,7 @@ export async function createPost( // `embed` is forwarded verbatim; the caller is responsible for // shaping it as an `app.bsky.embed.images` / `.external` / etc. // record. Pass `null` or `undefined` to omit. - return await invoke("post_create", { + return await safeInvoke("post_create", { text, embed: embed ?? null, }); @@ -163,7 +202,7 @@ export async function pickAndUploadImage(): Promise<{ mimeType: string; size: number; } | null> { - const r = await invoke<{ + const r = await safeInvoke<{ blob: { $type: string; ref: { $link: string }; @@ -203,11 +242,11 @@ export function makeImagesEmbed(blob: { } export async function describeServer(): Promise { - return await invoke("pds_describe"); + return await safeInvoke("pds_describe"); } export async function pdsStatus(): Promise { - return await invoke("status_pds"); + return await safeInvoke("status_pds"); } export async function fetchTimeline( @@ -215,7 +254,7 @@ export async function fetchTimeline( cursor: string | null = null, limit: number = 30, ): Promise { - return await invoke("timeline_home", { + return await safeInvoke("timeline_home", { did, cursor, limit, @@ -223,18 +262,18 @@ export async function fetchTimeline( } export async function fetchProfile(handle: string): Promise { - return await invoke("profile_get", { handle }); + return await safeInvoke("profile_get", { handle }); } export async function fetchSearch( q: string, limit: number = 30, ): Promise { - return await invoke("search", { q, limit }); + return await safeInvoke("search", { q, limit }); } export async function fetchPost(uri: string): Promise { - return await invoke("post_get", { uri }); + return await safeInvoke("post_get", { uri }); } /// `app.bsky.feed.like.create` — Tauri command. Builds the @@ -251,21 +290,21 @@ export async function likePost( subjectUri: string, subjectCid: string, ): Promise { - return await invoke("like_post", { + return await safeInvoke("like_post", { subjectUri, subjectCid, }); } export async function unlikePost(likeUri: string): Promise { - return await invoke("unlike_post", { likeUri }); + return await safeInvoke("unlike_post", { likeUri }); } export async function repostPost( subjectUri: string, subjectCid: string, ): Promise { - return await invoke("repost_post", { + return await safeInvoke("repost_post", { subjectUri, subjectCid, }); @@ -274,7 +313,7 @@ export async function repostPost( export async function unrepostPost( repostUri: string, ): Promise { - return await invoke("unrepost_post", { repostUri }); + return await safeInvoke("unrepost_post", { repostUri }); } /// Fire-and-forget user-visible error toast. Implemented as a @@ -302,7 +341,7 @@ export async function showNotification( url?: string, ): Promise { try { - await invoke("show_notification", { title, body, url: url ?? null }); + await safeInvoke("show_notification", { title, body, url: url ?? null }); } catch (e) { console.error("show_notification failed", e); } @@ -380,7 +419,7 @@ export async function fetchBlob( const key = _blobKey(did, cid); const cached = _blobUrlCache.get(key); if (cached) return cached; - const bytes: number[] = await invoke("fetch_blob", { + const bytes: number[] = await safeInvoke("fetch_blob", { did, cid, }); diff --git a/crates/tauri-app/src/main.ts b/crates/tauri-app/src/main.ts index 04eda5b..f3cebaf 100644 --- a/crates/tauri-app/src/main.ts +++ b/crates/tauri-app/src/main.ts @@ -1,6 +1,7 @@ import "./app.css"; import App from "./App.svelte"; import { mount } from "svelte"; +import { isTauri } from "@tauri-apps/api/core"; import { listen, type UnlistenFn } from "@tauri-apps/api/event"; const app = mount(App, { target: document.getElementById("app")! }); @@ -14,6 +15,20 @@ const app = mount(App, { target: document.getElementById("app")! }); const unlisteners: UnlistenFn[] = []; async function wireBackendEvents() { + // When the page is served by `vite dev` (or any non-Tauri context), + // skip the whole wiring. Without this guard the bare `listen(...)` + // call throws "Cannot read properties of undefined (reading + // 'transformCallback')" because `window.__TAURI_INTERNALS__` is + // undefined, the error escapes the helper, propagates up here, + // and our `wireBackendEvents().catch(...)` swallows it — but only + // AFTER the runtime throws. The real risk is that this prevents + // Svelte from finishing its first mount cycle cleanly in the + // browser preview. Just bail out early. + if (!isTauri()) { + console.info("running in browser preview; skipping tauri event wiring"); + return; + } + // "Show maarcadetweet" tray menu item, or a left click on the // tray icon. We focus the main window — Tauri 2 has no // `WindowExt::show()` shortcut, so we look it up by label.