feat: Implement Android share functionality and background sync

- Add ShareRoomPicker component for selecting rooms to share Android intents.
- Introduce AndroidShare utility for handling shared files and payloads from Android.
- Implement background synchronization with UnifiedPush for Android notifications.
- Enhance notification handling in Tauri and Capacitor environments.
- Update TypeScript configuration for better module resolution.
- Create Capacitor configuration file for Android integration.
- Add SVG icon for notifications.
- Refactor media URL authentication checks in matrix utility.
This commit is contained in:
2026-04-26 14:32:16 +10:00
parent 7b69ed6df8
commit ef0aa2a847
12 changed files with 1354 additions and 17 deletions

View File

@@ -116,6 +116,9 @@ export const isYouTubeStreamingAvailable = (): boolean => {
/** Callback for notification tap actions */
let notificationTapCallback: ((path: string) => void) | null = null;
const ANDROID_NOTIFICATION_SMALL_ICON = 'ic_stat_paarrot';
const ANDROID_NOTIFICATION_ICON_COLOR = '#FF8A00';
/**
* Bring the Tauri window to the front and focus it
*/
@@ -190,6 +193,21 @@ export const setupNotificationTapListener = async (onTap: (path: string) => void
console.warn('Failed to set up Tauri notification tap listener:', err);
}
}
if (isCapacitorNative()) {
try {
const { LocalNotifications } = await import('@capacitor/local-notifications');
await LocalNotifications.addListener('localNotificationActionPerformed', async (event: any) => {
await focusWindow();
const path = event?.notification?.extra?.path;
if (path && typeof path === 'string' && notificationTapCallback) {
notificationTapCallback(path);
}
});
} catch (err) {
console.warn('Failed to set up Capacitor notification tap listener:', err);
}
}
};
/**
@@ -204,6 +222,15 @@ export const isTauri = (): boolean =>
export const isElectron = (): boolean =>
typeof window !== 'undefined' && 'electron' in window;
/**
* Check if we're running inside a Capacitor native app
*/
export const isCapacitorNative = (): boolean => {
if (typeof window === 'undefined') return false;
const cap = (window as any).Capacitor;
return Boolean(cap?.isNativePlatform?.() || cap?.getPlatform?.() === 'android' || cap?.getPlatform?.() === 'ios');
};
/**
* Check if we're running on a mobile platform (Android/iOS)
*/
@@ -305,6 +332,78 @@ const ensureNotificationChannel = async (): Promise<void> => {
}
};
const ensureCapacitorNotificationChannel = async (): Promise<void> => {
if (notificationChannelCreated || !isAndroid()) return;
try {
const { LocalNotifications } = await import('@capacitor/local-notifications');
await LocalNotifications.createChannel({
id: 'messages',
name: 'Messages',
description: 'Message notifications from Matrix',
importance: 5,
sound: 'default',
visibility: 1,
vibration: true,
lights: true,
});
notificationChannelCreated = true;
} catch (err) {
console.warn('Failed to create Capacitor notification channel:', err);
}
};
/**
* Request notification permission across Electron, Tauri, Capacitor, and browser
*/
export const requestSystemNotificationPermission = async (): Promise<boolean> => {
if (isElectron() || (isTauri() && !isElectron())) {
if (!('Notification' in window)) return false;
const permission = await Notification.requestPermission();
return permission === 'granted';
}
if (isCapacitorNative()) {
try {
const { LocalNotifications } = await import('@capacitor/local-notifications');
let perm = await LocalNotifications.checkPermissions();
if (perm.display !== 'granted') {
perm = await LocalNotifications.requestPermissions();
}
return perm.display === 'granted';
} catch (err) {
console.warn('Capacitor notification permission request failed:', err);
return false;
}
}
if (!('Notification' in window)) return false;
const permission = await Notification.requestPermission();
return permission === 'granted';
};
export const getSystemNotificationPermissionState = async (): Promise<PermissionState> => {
if (isCapacitorNative()) {
try {
const { LocalNotifications } = await import('@capacitor/local-notifications');
const perm = await LocalNotifications.checkPermissions();
return perm.display === 'granted' ? 'granted' : 'prompt';
} catch (err) {
console.warn('Failed to check Capacitor notification permission:', err);
return 'denied';
}
}
if ('Notification' in window) {
if (window.Notification.permission === 'default') {
return 'prompt';
}
return window.Notification.permission;
}
return 'denied';
};
/**
* Send a notification using Tauri's notification plugin
* Falls back to browser Notification API if not in Tauri
@@ -369,6 +468,38 @@ export const sendNotification = async (options: {
console.warn('Tauri notification failed:', err);
}
}
if (isCapacitorNative()) {
try {
const { LocalNotifications } = await import('@capacitor/local-notifications');
let perm = await LocalNotifications.checkPermissions();
if (perm.display !== 'granted') {
perm = await LocalNotifications.requestPermissions();
}
if (perm.display === 'granted') {
await ensureCapacitorNotificationChannel();
const id = Math.floor(Date.now() % 2147483647);
await LocalNotifications.schedule({
notifications: [
{
id,
title,
body,
channelId: isAndroid() ? 'messages' : undefined,
smallIcon: isAndroid() ? ANDROID_NOTIFICATION_SMALL_ICON : undefined,
iconColor: isAndroid() ? ANDROID_NOTIFICATION_ICON_COLOR : undefined,
extra: path ? { path } : undefined,
},
],
});
}
return;
} catch (err) {
console.warn('Capacitor notification failed:', err);
}
}
// Fallback to browser notification
sendBrowserNotification({ title, body, icon, onClick });