From 156dfcf3973aa95b80193382af1e7329d68cf6f8 Mon Sep 17 00:00:00 2001 From: Max Litruv Boonzaayer Date: Sun, 25 Jan 2026 13:58:44 +1100 Subject: [PATCH] fix: add Tauri opener plugin for external URL handling and improve notification focus behavior --- package.json | 1 + src/app/pages/client/ClientNonUIFeatures.tsx | 8 ++- src/app/utils/tauri.ts | 68 ++++++++++++++++++-- 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index d8cccdc..3cd852e 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@tauri-apps/api": "2.9.1", "@tauri-apps/plugin-fs": "2.4.5", "@tauri-apps/plugin-notification": "2.3.3", + "@tauri-apps/plugin-opener": "2", "@vanilla-extract/css": "1.9.3", "@vanilla-extract/recipes": "0.3.0", "@vanilla-extract/vite-plugin": "3.7.1", diff --git a/src/app/pages/client/ClientNonUIFeatures.tsx b/src/app/pages/client/ClientNonUIFeatures.tsx index 93af758..ee63c77 100644 --- a/src/app/pages/client/ClientNonUIFeatures.tsx +++ b/src/app/pages/client/ClientNonUIFeatures.tsx @@ -90,13 +90,15 @@ function InviteNotifications() { const notify = useCallback( (count: number) => { const body = `You have ${count} new invitation request.`; + const invitesPath = getInboxInvitesPath(); if (isTauri()) { sendNotification({ title: 'Invitation', body, + path: invitesPath, onClick: () => { - if (!window.closed) navigate(getInboxInvitesPath()); + if (!window.closed) navigate(invitesPath); }, }); } else { @@ -108,7 +110,8 @@ function InviteNotifications() { }); noti.onclick = () => { - if (!window.closed) navigate(getInboxInvitesPath()); + window.focus(); + if (!window.closed) navigate(invitesPath); noti.close(); }; } @@ -207,6 +210,7 @@ function MessageNotifications() { }); noti.onclick = () => { + window.focus(); if (!window.closed) navigate(roomPath); noti.close(); notifRef.current = undefined; diff --git a/src/app/utils/tauri.ts b/src/app/utils/tauri.ts index c186c2b..157d4f2 100644 --- a/src/app/utils/tauri.ts +++ b/src/app/utils/tauri.ts @@ -1,5 +1,5 @@ /** - * Open an external URL using Tauri shell on desktop/mobile, or window.open in browser + * Open an external URL using Tauri opener plugin on desktop/mobile, or window.open in browser * @param url The URL to open */ export const openExternalUrl = async (url: string): Promise => { @@ -9,11 +9,11 @@ export const openExternalUrl = async (url: string): Promise => { // Use new Function to avoid static analysis by Vite/Rollup await new Function( 'url', - "return import('@tauri-apps/plugin-shell').then(m => m.open(url));" + "return import('@tauri-apps/plugin-opener').then(m => m.openUrl(url));" )(url); return; } catch (err) { - console.warn('Tauri shell.open failed, falling back to window.open:', err); + console.warn('Tauri opener.openUrl failed, falling back to window.open:', err); } } window.open(url, '_blank'); @@ -26,7 +26,23 @@ export const openExternalUrl = async (url: string): Promise => { let notificationTapCallback: ((path: string) => void) | null = null; /** - * Set up listener for notification tap actions on mobile + * Bring the Tauri window to the front and focus it + */ +export const focusWindow = async (): Promise => { + if (!isTauri()) return; + + try { + const { getCurrentWindow } = await import('@tauri-apps/api/window'); + const window = getCurrentWindow(); + await window.unminimize(); + await window.setFocus(); + } catch (err) { + console.warn('Failed to focus window:', err); + } +}; + +/** + * Set up listener for notification tap/click actions * Call this once at app startup with a navigation callback */ export const setupNotificationTapListener = async (onTap: (path: string) => void): Promise => { @@ -37,7 +53,10 @@ export const setupNotificationTapListener = async (onTap: (path: string) => void try { const { onAction } = await import('@tauri-apps/plugin-notification'); - await onAction((notification) => { + await onAction(async (notification) => { + // Bring window to front first + await focusWindow(); + // Get the path from extra data and navigate const path = notification.notification?.extra?.path; if (path && typeof path === 'string' && notificationTapCallback) { @@ -115,7 +134,11 @@ const sendBrowserNotification = (options: { }); if (onClick) { - notification.onclick = () => { + notification.onclick = async () => { + // Bring Tauri window to front if in Tauri + await focusWindow(); + // Also use standard window.focus for browser + window.focus(); if (!window.closed) onClick(); notification.close(); }; @@ -124,6 +147,34 @@ const sendBrowserNotification = (options: { return notification; }; +/** Flag to track if notification channel has been created on Android */ +let notificationChannelCreated = false; + +/** + * Create notification channel for Android + * Required for Android 8.0+ to show notifications + */ +const ensureNotificationChannel = async (): Promise => { + if (notificationChannelCreated || !isAndroid()) return; + + try { + const { createChannel, Importance } = await import('@tauri-apps/plugin-notification'); + + await createChannel({ + id: 'messages', + name: 'Messages', + description: 'Message notifications from Matrix', + importance: Importance.High, + vibration: true, + sound: 'default', + }); + + notificationChannelCreated = true; + } catch (err) { + console.warn('Failed to create notification channel:', err); + } +}; + /** * Send a notification using Tauri's notification plugin * Falls back to browser Notification API if not in Tauri @@ -152,9 +203,14 @@ export const sendNotification = async (options: { } if (permissionGranted) { + // Ensure notification channel exists on Android + await ensureNotificationChannel(); + await tauriSendNotification({ title, body, + // Use the channel on Android + channelId: isAndroid() ? 'messages' : undefined, // Store path in extra data for notification tap handling extra: path ? { path } : undefined, });