diff --git a/src/app/pages/client/ClientRoot.tsx b/src/app/pages/client/ClientRoot.tsx index 96d97e3..eb0d5ee 100644 --- a/src/app/pages/client/ClientRoot.tsx +++ b/src/app/pages/client/ClientRoot.tsx @@ -13,7 +13,7 @@ import { Spinner, Text, } from 'folds'; -import { HttpApiEvent, HttpApiEventHandlerMap, MatrixClient } from 'matrix-js-sdk'; +import { HttpApiEvent, HttpApiEventHandlerMap, MatrixClient, SyncState } from 'matrix-js-sdk'; import FocusTrap from 'focus-trap-react'; import React, { MouseEventHandler, ReactNode, useCallback, useEffect, useState } from 'react'; import { @@ -36,6 +36,7 @@ import { SyncStatus } from './SyncStatus'; import { AuthMetadataProvider } from '../../hooks/useAuthMetadata'; import { getFallbackSession } from '../../state/sessions'; import { CallProviderWrapper } from '../../features/call/CallProviderWrapper'; +import { isTauriMobile, startBackgroundSync, stopBackgroundSync } from '../../utils/tauri'; function ClientRootLoading() { return ( @@ -139,6 +140,90 @@ const useLogoutListener = (mx?: MatrixClient) => { }, [mx]); }; +/** + * Hook to handle visibility changes and restart sync on mobile + * When the app goes to background and comes back, sync may be stale/stopped + */ +const useVisibilitySync = (mx?: MatrixClient) => { + useEffect(() => { + if (!mx || !isTauriMobile()) return; + + const handleVisibilityChange = () => { + if (document.visibilityState === 'visible') { + console.log('[useVisibilitySync] App became visible, checking sync state'); + const syncState = mx.getSyncState(); + console.log('[useVisibilitySync] Current sync state:', syncState); + + // If sync is not running or errored, restart it + if (!mx.clientRunning || syncState === SyncState.Error || syncState === SyncState.Stopped || syncState === null) { + console.log('[useVisibilitySync] Restarting sync...'); + startClient(mx).catch((err) => { + console.error('[useVisibilitySync] Failed to restart sync:', err); + }); + } else { + // Force a sync even if client is running (to catch up quickly) + console.log('[useVisibilitySync] Triggering immediate sync'); + mx.retryImmediately(); + } + } + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); + + // Also handle resume event for mobile (if available) + window.addEventListener('resume', handleVisibilityChange); + + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange); + window.removeEventListener('resume', handleVisibilityChange); + }; + }, [mx]); +}; + +/** + * Hook to start/stop native Rust background sync on mobile + * This allows notifications to work even when the app is backgrounded + */ +const useBackgroundSync = (mx?: MatrixClient) => { + useEffect(() => { + if (!mx || !isTauriMobile()) return; + + // Start background sync with credentials from the Matrix client + const startSync = async () => { + try { + const homeserverUrl = mx.getHomeserverUrl(); + const userId = mx.getUserId(); + const accessToken = mx.getAccessToken(); + const deviceId = mx.getDeviceId(); + + if (!homeserverUrl || !userId || !accessToken || !deviceId) { + console.warn('[useBackgroundSync] Missing credentials, cannot start background sync'); + return; + } + + await startBackgroundSync({ + homeserverUrl, + userId, + accessToken, + deviceId, + }); + console.log('[useBackgroundSync] Background sync started'); + } catch (err) { + console.error('[useBackgroundSync] Failed to start background sync:', err); + } + }; + + startSync(); + + // Stop background sync on cleanup + return () => { + stopBackgroundSync().catch((err) => { + console.error('[useBackgroundSync] Failed to stop background sync:', err); + }); + }; + }, [mx]); +}; + type ClientRootProps = { children: ReactNode; }; @@ -161,6 +246,8 @@ export function ClientRoot({ children }: ClientRootProps) { ); useLogoutListener(mx); + useVisibilitySync(mx); + useBackgroundSync(mx); useEffect(() => { if (loadState.status === AsyncStatus.Idle) { diff --git a/src/app/plugins/react-custom-html-parser.tsx b/src/app/plugins/react-custom-html-parser.tsx index 9b433c7..bad01d5 100644 --- a/src/app/plugins/react-custom-html-parser.tsx +++ b/src/app/plugins/react-custom-html-parser.tsx @@ -506,7 +506,7 @@ export const getReactCustomHtmlParser = ( const htmlSrc = mxcUrlToHttp(mx, props.src, params.useAuthentication); if (htmlSrc && props.src.startsWith('mxc://') === false) { return ( - + {props.alt || props.title || htmlSrc} ); diff --git a/src/app/utils/tauri.ts b/src/app/utils/tauri.ts index b43e879..02c0d78 100644 --- a/src/app/utils/tauri.ts +++ b/src/app/utils/tauri.ts @@ -9,26 +9,28 @@ export const openExternalUrl = async (url: string): Promise => { // Only run this in Tauri builds if (typeof window !== 'undefined' && (window as any).__TAURI__) { try { - console.log('[openExternalUrl] invoking open_external_url command...'); - const { invoke } = await import('@tauri-apps/api/core'); - await invoke('open_external_url', { url }); - console.log('[openExternalUrl] command succeeded'); + // First, try the plugin directly (works on both desktop and mobile) + console.log('[openExternalUrl] trying opener plugin...'); + const { openUrl } = await import('@tauri-apps/plugin-opener'); + await openUrl(url); + console.log('[openExternalUrl] plugin succeeded'); return; - } catch (err) { - console.error('[openExternalUrl] Tauri command failed:', err); - // Try the plugin as fallback (for mobile) + } catch (pluginErr) { + console.warn('[openExternalUrl] opener plugin failed:', pluginErr); + + // Fallback: try the custom command (useful if plugin fails due to ACL) try { - console.log('[openExternalUrl] trying opener plugin fallback...'); - const { openUrl } = await import('@tauri-apps/plugin-opener'); - await openUrl(url); - console.log('[openExternalUrl] plugin fallback succeeded'); + console.log('[openExternalUrl] trying invoke command fallback...'); + const { invoke } = await import('@tauri-apps/api/core'); + await invoke('open_external_url', { url }); + console.log('[openExternalUrl] command fallback succeeded'); return; - } catch (pluginErr) { - console.error('[openExternalUrl] plugin fallback also failed:', pluginErr); + } catch (invokeErr) { + console.error('[openExternalUrl] command fallback also failed:', invokeErr); } - return; } } + console.log('[openExternalUrl] falling back to window.open'); window.open(url, '_blank'); }; @@ -269,3 +271,69 @@ export const readClipboardImage = async (): Promise => { return null; } }; + +/** + * Background Sync API for mobile platforms + * Starts a native Rust-based Matrix sync that runs even when the app is backgrounded + */ +export interface BackgroundSyncCredentials { + homeserverUrl: string; + userId: string; + accessToken: string; + deviceId: string; +} + +/** + * Start background Matrix sync on mobile + * This runs the sync in native Rust code, allowing notifications even when the app is backgrounded + */ +export const startBackgroundSync = async (credentials: BackgroundSyncCredentials): Promise => { + if (!isTauriMobile()) { + console.log('[BackgroundSync] Not on mobile, skipping'); + return; + } + + try { + const { invoke } = await import('@tauri-apps/api/core'); + await invoke('start_background_sync', { + homeserverUrl: credentials.homeserverUrl, + userId: credentials.userId, + accessToken: credentials.accessToken, + deviceId: credentials.deviceId, + }); + console.log('[BackgroundSync] Started successfully'); + } catch (err) { + console.error('[BackgroundSync] Failed to start:', err); + throw err; + } +}; + +/** + * Stop background Matrix sync on mobile + */ +export const stopBackgroundSync = async (): Promise => { + if (!isTauriMobile()) return; + + try { + const { invoke } = await import('@tauri-apps/api/core'); + await invoke('stop_background_sync'); + console.log('[BackgroundSync] Stopped'); + } catch (err) { + console.error('[BackgroundSync] Failed to stop:', err); + } +}; + +/** + * Get the current background sync state + */ +export const getBackgroundSyncState = async (): Promise => { + if (!isTauriMobile()) return 'NotApplicable'; + + try { + const { invoke } = await import('@tauri-apps/api/core'); + return await invoke('get_background_sync_state'); + } catch (err) { + console.error('[BackgroundSync] Failed to get state:', err); + return 'Error'; + } +};