feat: implement useMarkAsRead hook for improved unread state management and update notification handling

This commit is contained in:
2026-02-28 00:10:35 +11:00
parent c890cce37a
commit 29c409fad8
16 changed files with 299 additions and 113 deletions

View File

@@ -4,29 +4,42 @@
*/
export const openExternalUrl = async (url: string): Promise<void> => {
console.log('[openExternalUrl] called with:', url);
console.log('[openExternalUrl] __TAURI__ exists:', !!(window as any).__TAURI__);
// Only run this in Tauri builds
if (typeof window !== 'undefined' && (window as any).__TAURI__) {
// Use Electron's shell.openExternal if in Electron
if (isElectron()) {
try {
const electron = (window as any).electron;
if (electron?.shell?.openExternal) {
await electron.shell.openExternal(url);
console.log('[openExternalUrl] Electron shell.openExternal succeeded');
return;
}
} catch (err) {
console.warn('[openExternalUrl] Electron shell.openExternal failed:', err);
}
}
// Use Tauri for actual Tauri builds
if (isTauri() && !isElectron()) {
try {
// First, try the plugin directly (works on both desktop and mobile)
console.log('[openExternalUrl] trying opener plugin...');
console.log('[openExternalUrl] trying Tauri opener plugin...');
const { openUrl } = await import('@tauri-apps/plugin-opener');
await openUrl(url);
console.log('[openExternalUrl] plugin succeeded');
console.log('[openExternalUrl] Tauri plugin succeeded');
return;
} catch (pluginErr) {
console.warn('[openExternalUrl] opener plugin failed:', pluginErr);
console.warn('[openExternalUrl] Tauri opener plugin failed:', pluginErr);
// Fallback: try the custom command (useful if plugin fails due to ACL)
try {
console.log('[openExternalUrl] trying invoke command fallback...');
console.log('[openExternalUrl] trying Tauri invoke command fallback...');
const { invoke } = await import('@tauri-apps/api/core');
await invoke('open_external_url', { url });
console.log('[openExternalUrl] command fallback succeeded');
console.log('[openExternalUrl] Tauri command fallback succeeded');
return;
} catch (invokeErr) {
console.error('[openExternalUrl] command fallback also failed:', invokeErr);
console.error('[openExternalUrl] Tauri command fallback also failed:', invokeErr);
}
}
}
@@ -79,15 +92,29 @@ let notificationTapCallback: ((path: string) => void) | null = null;
* Bring the Tauri window to the front and focus it
*/
export const focusWindow = async (): Promise<void> => {
if (!isTauri()) return;
// Use Electron's window focus if in Electron
if (isElectron()) {
try {
const electron = (window as any).electron;
if (electron?.window?.focus) {
await electron.window.focus();
}
return;
} catch (err) {
console.warn('Failed to focus Electron window:', err);
}
}
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);
// Use Tauri's window focus for actual Tauri builds
if (isTauri() && !isElectron()) {
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 Tauri window:', err);
}
}
};
@@ -96,28 +123,43 @@ export const focusWindow = async (): Promise<void> => {
* Call this once at app startup with a navigation callback
*/
export const setupNotificationTapListener = async (onTap: (path: string) => void): Promise<void> => {
if (!isTauri()) return;
notificationTapCallback = onTap;
try {
const { onAction } = await import('@tauri-apps/plugin-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) {
notificationTapCallback(path);
// Use Electron's native notification handler if in Electron
if (isElectron()) {
try {
const electron = (window as any).electron;
if (electron?.notification?.onNavigate) {
electron.notification.onNavigate((data: { path?: string }) => {
if (data.path && typeof data.path === 'string' && notificationTapCallback) {
notificationTapCallback(data.path);
}
});
return;
}
});
} catch (err) {
// Silently ignore errors in Electron compatibility layer
// Only warn if we're actually in a Tauri environment
if (typeof window !== 'undefined' && '__TAURI__' in window) {
console.warn('Failed to set up notification tap listener:', err);
} catch (err) {
console.warn('Failed to set up Electron notification listener:', err);
return;
}
}
// Use Tauri plugin for actual Tauri builds (not Electron)
if (isTauri() && !isElectron()) {
try {
const { onAction } = await import('@tauri-apps/plugin-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) {
notificationTapCallback(path);
}
});
} catch (err) {
console.warn('Failed to set up Tauri notification tap listener:', err);
}
}
};
@@ -128,11 +170,17 @@ export const setupNotificationTapListener = async (onTap: (path: string) => void
export const isTauri = (): boolean =>
'__TAURI__' in window || '__TAURI_INTERNALS__' in window;
/**
* Check if we're running inside Electron (not Tauri)
*/
export const isElectron = (): boolean =>
typeof window !== 'undefined' && 'electron' in window;
/**
* Check if we're running on a mobile platform (Android/iOS)
*/
export const isTauriMobile = (): boolean => {
if (!isTauri()) return false;
if (!isTauri() || isElectron()) return false;
const ua = navigator.userAgent.toLowerCase();
return ua.includes('android') || ua.includes('iphone') || ua.includes('ipad');
};
@@ -242,7 +290,26 @@ export const sendNotification = async (options: {
}): Promise<void> => {
const { title, body, icon, path, onClick } = options;
if (isTauri()) {
// Use Electron's native notification API if in Electron
if (isElectron()) {
try {
const electron = (window as any).electron;
if (electron?.notification?.show) {
await electron.notification.show({
title,
body,
icon,
path,
});
return;
}
} catch (err) {
console.warn('Electron notification failed:', err);
}
}
// Use Tauri plugin for actual Tauri builds (not Electron)
if (isTauri() && !isElectron()) {
try {
const {
sendNotification: tauriSendNotification,
@@ -269,14 +336,14 @@ export const sendNotification = async (options: {
extra: path ? { path } : undefined,
});
}
return;
} catch (err) {
console.warn('Tauri notification failed:', err);
// Fallback to browser notification on error
sendBrowserNotification({ title, body, icon, onClick });
}
} else {
sendBrowserNotification({ title, body, icon, onClick });
}
// Fallback to browser notification
sendBrowserNotification({ title, body, icon, onClick });
};
/**
@@ -292,22 +359,44 @@ export const isLinux = (): boolean => {
* Returns a data URL if an image is found, null otherwise
*/
export const readClipboardImage = async (): Promise<File | null> => {
if (!isTauri() || !isLinux()) return null;
try {
const { invoke } = await import('@tauri-apps/api/core');
const dataUrl = await invoke<string | null>('read_clipboard_image');
if (!dataUrl) return null;
// Convert data URL to File
const response = await fetch(dataUrl);
const blob = await response.blob();
return new File([blob], 'clipboard-image.png', { type: 'image/png' });
} catch (err) {
console.warn('Failed to read clipboard image:', err);
return null;
// Use Electron's clipboard API if in Electron
if (isElectron()) {
try {
const electron = (window as any).electron;
if (electron?.clipboard?.readImage) {
const dataUrl = await electron.clipboard.readImage();
if (!dataUrl) return null;
// Convert data URL to File
const response = await fetch(dataUrl);
const blob = await response.blob();
return new File([blob], 'clipboard-image.png', { type: 'image/png' });
}
} catch (err) {
console.warn('Failed to read Electron clipboard image:', err);
return null;
}
}
// Use Tauri's invoke for actual Tauri builds on Linux
if (isTauri() && !isElectron() && isLinux()) {
try {
const { invoke } = await import('@tauri-apps/api/core');
const dataUrl = await invoke<string | null>('read_clipboard_image');
if (!dataUrl) return null;
// Convert data URL to File
const response = await fetch(dataUrl);
const blob = await response.blob();
return new File([blob], 'clipboard-image.png', { type: 'image/png' });
} catch (err) {
console.warn('Failed to read Tauri clipboard image:', err);
return null;
}
}
return null;
};
/**