// Bridge to the native PollingPlugin (see // android/app/src/main/java/chat/vojo/app/PollingPlugin.java). // // Drives the WorkManager-based /notifications polling fallback used on // networks where FCM (mtalk.google.com:5228) is blocked. JS owns the // credential + room-name cache lifecycle; native owns the periodic fetch // and notification rendering. // // Web has no analogue: the Service Worker already wakes for push without // needing a periodic poll, and browsers don't expose a 15-minute periodic // background API anyway. The web fallback is a no-op. import { registerPlugin } from '@capacitor/core'; import { isAndroidPlatform } from '../utils/capacitor'; export type RoomMetadataMap = Record< string, | string | { name: string; isDirect: boolean; isEncrypted: boolean; // MXC URL for the room's avatar (or, for a DM with no explicit room // avatar, the other member's avatar). Optional — empty for rooms // without an avatar configured. Java side resolves via auth-media // v1.11 + LRU bitmap cache. avatarMxc?: string; } >; /** * user_id → MXC avatar URL. Bridged to the Java side so the FCM / * WorkManager renderers can attach IconCompat icons to per-sender Person * objects in MessagingStyle (and to the self-Person anchor). Senders not * in this map fall through to Android's default initials/blank circle. */ export type UserAvatarsMap = Record; interface PollingPluginIface { saveSession(opts: { accessToken: string; homeserverUrl: string; userId?: string }): Promise; clearSession(): Promise; // Tolerant of both legacy (`roomId: "Display"`) and new structured shape // (`roomId: { name, isDirect, isEncrypted, avatarMxc }`) — the Java side // decides the message channel (DM vs group) and resolves the room large // icon based on the structured value when present. saveRoomNames(opts: { names: RoomMetadataMap }): Promise; saveUserAvatars(opts: { avatars: UserAvatarsMap }): Promise; schedule(opts: { intervalMinutes: number }): Promise; cancel(): Promise; dismissRoom(opts: { roomId: string }): Promise; } const noopPlugin: PollingPluginIface = { saveSession: async () => undefined, clearSession: async () => undefined, saveRoomNames: async () => undefined, saveUserAvatars: async () => undefined, schedule: async () => undefined, cancel: async () => undefined, dismissRoom: async () => undefined, }; const plugin = registerPlugin('Polling', { web: noopPlugin, }); const guard = async (fn: () => Promise, fallback: T): Promise => { if (!isAndroidPlatform()) return fallback; try { return await fn(); } catch (err) { // Old APK installed before the plugin shipped, or transient bridge // error. Swallow — polling is a best-effort backup channel, not a // hard dependency on the foreground push lifecycle. // eslint-disable-next-line no-console console.warn('[polling] native call failed:', err); return fallback; } }; export const polling = { saveSession: (opts: { accessToken: string; homeserverUrl: string; userId?: string }) => guard(() => plugin.saveSession(opts), undefined), clearSession: () => guard(() => plugin.clearSession(), undefined), saveRoomNames: (names: RoomMetadataMap) => guard(() => plugin.saveRoomNames({ names }), undefined), saveUserAvatars: (avatars: UserAvatarsMap) => guard(() => plugin.saveUserAvatars({ avatars }), undefined), schedule: (intervalMinutes = 15) => guard(() => plugin.schedule({ intervalMinutes }), undefined), cancel: () => guard(() => plugin.cancel(), undefined), dismissRoom: (roomId: string) => guard(() => plugin.dismissRoom({ roomId }), undefined), };