Android's shouldInterceptRequest blocks a pool of ~6 IO threads, each
waiting for JS to respond via the Capacitor bridge. With 200+ files
each requiring a network round-trip to Blossom, loading is painfully
slow. iOS doesn't have this problem — WKURLSchemeHandler is async.
Split the native plugin lifecycle into create() and navigate():
- create() adds the WebView container with spinner overlay (visible)
- navigate() loads the entry URL (triggers fetch interception)
On Android, onReady downloads all manifest blobs in parallel (12
concurrent fetches) into an in-memory cache while the native
ProgressBar spinner animates. Once navigate() fires, every resolveFile
call is an instant cache hit.
On iOS/web, onReady is a no-op and navigate() fires immediately.
iOS: load inline spinner HTML (centered spinning ring on dark background)
before navigating to the real content URL. Supports light/dark mode via
prefers-color-scheme. The spinner is replaced when the real page loads.
Android: use a native ProgressBar overlay instead of HTML — the HTML
spinner froze because constant Capacitor bridge calls saturated the
main thread, starving the WebView compositor. The native ProgressBar
animates on the render thread independently. Wrapped in a FrameLayout
with a dark overlay behind the spinner.
Both platforms: set WebView background to #14161f (app dark theme)
instead of white. Increased Android shouldInterceptRequest timeout
from 10s to 60s to prevent premature timeouts on large nsites.
Default to push mode (no foreground service). Persistent mode with
the always-on background polling service is opt-in via the new
Delivery Method section in notification settings.
- Add notificationStyle ('push' | 'persistent') to EncryptedSettings
- Show radio group in NotificationSettings on native platforms
- Pass notificationStyle through Capacitor plugin to SharedPreferences
- DittoNotificationPlugin starts/stops foreground service on style change
- MainActivity only starts service on launch when style is persistent
- Re-enable unread polling on native when push mode is active
Pass followed pubkeys through the Capacitor plugin to the native
polling service. When onlyFollowing is enabled, the relay query
includes an authors filter so only events from followed accounts
trigger native Android notifications.
Disabled notification types (e.g. reactions) still triggered push
notifications and showed the unread dot indicator, even though the
notification tab correctly filtered them out.
Three root causes fixed:
- useHasUnreadNotifications now uses getEnabledNotificationKinds to
only query for enabled types, preventing phantom unread dots
- NotificationSettings now syncs type preference changes to the
nostr-push server via updateSubscription (is_active toggle)
- Native Android poller now receives enabled kinds from the JS layer
and uses them in the relay filter instead of hardcoded kinds
Letters were completely absent from the notification pipeline — users had
to visit the Letters page to discover incoming letters. This integrates
kind 8211 into every layer of the notification system:
- useNotifications: query, grouping, and referenced-event exclusion
- useHasUnreadNotifications: unread dot indicator
- NotificationsPage: LetterNotification component with link to /letters
- NotificationSettings: toggleable Letters row
- notificationTemplates: web push template
- Android NotificationRelayService + NostrPoller: native push support
- EncryptedSettings + schema: letters preference field
Closes#188
When onCreate() catches ForegroundServiceStartNotAllowedException and calls
stopSelf() early, httpClient is never initialized. onDestroy() still runs and
was crashing with a NullPointerException on httpClient.dispatcher().
Android 16 (targetSdk 36) enforces strict time limits on dataSync
foreground services. When the limit is exhausted, calling startForeground()
or startForegroundService() throws ForegroundServiceStartNotAllowedException
and kills the app. Catch the exception in both NotificationRelayService.onCreate()
and MainActivity.onCreate() so the app continues running; the AlarmManager
will reschedule the next fetch cycle at the normal interval.
- Add kind 1111 to the Nostr REQ filter in NotificationRelayService
- Add case 1111 to kindToAction() in NostrPoller with the same context-aware
label logic: 'replied to your comment' when k=1111, 'commented on your post' otherwise