M5: push (FCM + ntfy) + offline catch-up + background notifications

Server (gateway-plugin):
- push.py: FCM (HTTP v1 service-account / legacy key) + ntfy backends; NtfyBackend.server_url for app discovery
- adapter.py: push on offline broadcast + high-priority push when live; fcm.register; server_caps.push_ntfy_server; outbox now ALWAYS appends so a reconnecting app catches up on live-delivered frames (fixes empty chat after notification tap / activity recreation)
- protocol.py / ws_server.py / outbox.py / plugin.yaml: M5 frames + env vars

App (Kotlin CMP):
- Protocol.kt: notification / fcm.register / sync frames + push caps
- GatewayClient.kt: hello carries push creds; auto-sync on reconnect
- IrisController.kt: banners, deep-link, notifyMessageIfBackgrounded (system notification on a regular reply when backgrounded)
- Android: PlatformPush, AppBridge, IrisNotifications, NtfyListenerService, IrisFirebaseMessagingService, AndroidPush, AndroidSecureStore
- Desktop: DesktopPush, DesktopSecureStore
- build files + manifest (permissions, services, deep-links)

Tests: 35-test tests/gateway/test_android.py suite passes (incl. new regression test_live_delivered_frame_still_parked_for_sync). E2E verified on device: push fire, reconnect sync, background notification, and message replay after ChatStore reset.
This commit is contained in:
ARIA committed 2026-08-19 19:20:55 +02:00
1 parent 913ee91024
commit 2ecfe1c05c
28 files changed
+1739 -51

No files matched your search

@@ -6,6 +6,7 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.darkColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
@@ -13,6 +14,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import iris.data.SecureStore
import iris.net.GatewayClient
import iris.platform.setActiveController
import iris.state.IrisController
import iris.ui.screens.ChatScreen
import iris.ui.screens.ConnectScreen
@@ -33,10 +35,25 @@ private val IrisDark = darkColorScheme(
* settings, and media (docs/10-android-app.md).
*/
@Composable
fun IrisApp(store: SecureStore) {
fun IrisApp(
store: SecureStore,
deepLinkChatId: String? = null,
deepLinkThreadId: String? = null,
) {
val controller = remember(store) { IrisController(store) }
DisposableEffect(controller) {
onDispose { controller.dispose() }
setActiveController(controller)
onDispose {
setActiveController(null)
controller.dispose()
}
}
// M5: open the chat referenced by a push notification tap (applied now or
// on the next hello.ack, whichever is later).
LaunchedEffect(deepLinkChatId, deepLinkThreadId) {
if (!deepLinkChatId.isNullOrBlank()) {
controller.openDeepLink(deepLinkChatId, deepLinkThreadId)
}
}
val state by controller.client.state.collectAsState()