docs(android): document MessagingStyle pipeline, channel split, callId-session dedup and edit-collapse defer rationale
This commit is contained in:
parent
b197e354f5
commit
347241264b
1 changed files with 66 additions and 2 deletions
|
|
@ -71,7 +71,7 @@ Components:
|
|||
|---|---|---|
|
||||
| Worker | [`VojoPollWorker.java`](../../android/app/src/main/java/chat/vojo/app/VojoPollWorker.java) | Periodic fetch of `/notifications`, flattens response into Sygnal-shape `Map<String,String>`, routes message/invite → `renderMessageNotification`, RTC ring → `renderMissedCallNotification`. Skips events that are `read=true`, push-rule-suppressed (`actions` lacks `notify`), in NotificationDedup, or with `ts < watermark`. Foreground-gated: doesn't render system notifications while `MainActivity.isInForeground` (still consumes state). Saves a drain cursor when capped at `MAX_PAGES_PER_RUN`. |
|
||||
| Bridge | [`PollingPlugin.java`](../../android/app/src/main/java/chat/vojo/app/PollingPlugin.java) | Capacitor plugin. JS calls `saveSession` (token + homeserver, seeds watermark on first use to skip historical backlog), `schedule(15)` (unique periodic worker), `saveRoomNames` (room-id → name cache), `cancel` (awaits WorkManager Operation completion) + `clearSession` on disable/logout. |
|
||||
| Renderers | [`VojoFirebaseMessagingService.java::renderMessageNotification`, `::renderMissedCallNotification`](../../android/app/src/main/java/chat/vojo/app/VojoFirebaseMessagingService.java) | Static, Context-parameterised so the Worker can post into the same notification id space as FCM. `eventId.hashCode()` slot — Android replaces in place when both paths deliver the same event AND both surfaces are still visible. After successful `nm.notify`, mark the event in NotificationDedup so the polling Worker doesn't re-surface it after the user dismisses an FCM-delivered one. |
|
||||
| Renderers | [`VojoFirebaseMessagingService.java::renderMessageNotification`, `::renderMissedCallNotification`](../../android/app/src/main/java/chat/vojo/app/VojoFirebaseMessagingService.java) | Static, Context-parameterised so the Worker can post into the same notification id space as FCM. Message path uses a **per-room** `roomId.hashCode()` slot — every new event in a room appends to a MessagingStyle conversation rather than stacking as a separate card (see [MessagingStyle pipeline](#messagingstyle-pipeline) below). Missed-call path uses per-event slots so multiple missed rings stack. After successful `nm.notify`, mark the event in NotificationDedup so the polling Worker doesn't re-surface it after the user dismisses an FCM-delivered one. |
|
||||
| Dedup | [`NotificationDedup.java`](../../android/app/src/main/java/chat/vojo/app/NotificationDedup.java) | Thread-safe shared LRU set of rendered event_ids. Written by both FCM service (background renders AND foreground-skipped events) and Worker (after successful render or foreground-skip). Bounded at 500 entries to comfortably exceed a single Worker run's worst case (`MAX_PAGES_PER_RUN × PAGE_LIMIT = 250`), persisted in `vojo_poll_state` SharedPreferences. |
|
||||
| JS plugin | [`src/app/plugins/polling.ts`](../../src/app/plugins/polling.ts) | `registerPlugin<PollingPluginIface>('Polling', { web: noop })`. Web has no analogue (SW already wakes for push) — fallback is a no-op. |
|
||||
| Lifecycle | [`src/app/hooks/usePushNotifications.ts::usePushNotificationsLifecycle`](../../src/app/hooks/usePushNotifications.ts) | Reactive to `usePushEnabled()`. On mount with push enabled: `saveSession` + `schedule` + initial room-name dump. On `visibilitychange → visible`: re-`saveSession` (recovers a 401-cleared credentials slot without remount) + re-dump room names. On unmount or push disable: `cancel` + `clearSession`. |
|
||||
|
|
@ -171,7 +171,71 @@ Cleanups invoked symmetrically across every logout path:
|
|||
the `SessionLoggedOut` listener, and the lifecycle effect's unmount all
|
||||
call `polling.cancel()` + `polling.clearSession()`.
|
||||
|
||||
## ADB wireless workflow
|
||||
## MessagingStyle pipeline
|
||||
|
||||
Background-rendered message notifications use
|
||||
`NotificationCompat.MessagingStyle` so multiple events in one room collapse
|
||||
into an expandable conversation card (WhatsApp / Telegram convention)
|
||||
rather than each event posting a separate banner. Notification id is
|
||||
**per-room** (`roomId.hashCode()`), not per-event.
|
||||
|
||||
Components:
|
||||
|
||||
| Layer | File | Role |
|
||||
|---|---|---|
|
||||
| Cache | [`RoomMessageCache.java`](../../android/app/src/main/java/chat/vojo/app/RoomMessageCache.java) | Thread-safe `ConcurrentHashMap<String, ArrayDeque<Entry>>` bounded at 20 messages × 200 rooms. Snapshot is taken INSIDE `compute()` so a concurrent FCM + Worker append on the same room can't race the copy. Mutated by both `VojoFirebaseMessagingService.renderMessageNotification` (FCM service path AND Worker path through the same static helper) and `appendOutgoingMessage` (ReplyReceiver echo). |
|
||||
| Channels | `vojo_messages_dm_v1` (IMPORTANCE_HIGH) + `vojo_messages_group_v1` (IMPORTANCE_DEFAULT) under `NotificationChannelGroup("vojo_messages_v1")`. Legacy `vojo_messages` is deleted on first creation of v1. Channel split lets users mute group-room noise in OS settings without losing DM alerts. |
|
||||
| Metadata snapshot | JS bridges `{roomId: {name, isDirect, isEncrypted}}` via `polling.saveRoomNames` → `KEY_ROOM_NAMES` in `vojo_poll_state`. `loadRoomMetadata` parses tolerantly (legacy `roomId: "name"` falls back to `isDirect=true, isEncrypted=true` for safety). Re-dump triggers: mount, visibility-change, `ClientEvent.AccountData` for `m.direct`, `RoomEvent.Timeline` filtered to `m.room.encryption`. |
|
||||
| Process-kill recovery | On cache miss, `seedCacheFromActiveNotification` calls `NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification` on the on-shade `StatusBarNotification` to rebuild prior history. Survives process kill; fails gracefully to single-message conversation if the notification was also dismissed. |
|
||||
| Receivers | [`MarkAsReadReceiver`](../../android/app/src/main/java/chat/vojo/app/MarkAsReadReceiver.java) (POST `/_matrix/client/v3/rooms/{roomId}/receipt/m.read/{eventId}` + dismiss), [`NotificationDismissReceiver`](../../android/app/src/main/java/chat/vojo/app/NotificationDismissReceiver.java) (swipe → clear cache so the next push starts fresh), [`ReplyReceiver`](../../android/app/src/main/java/chat/vojo/app/ReplyReceiver.java) (RemoteInput → PUT `m.room.message` with `m.text` body + optimistic local echo). All read credentials from `vojo_poll_state` SharedPreferences (same lifecycle as `VojoPollWorker`). |
|
||||
| Receipt-driven dismiss | JS `mx.on(RoomEvent.Receipt)` filters own-user receipts, checks `room.getUnreadNotificationCount(Total) === 0`, calls `polling.dismissRoom(roomId)` → native `nm.cancel + RoomMessageCache.clear`. Mirrors element-web's `Notifier.onRoomReceipt`. Killed-process dismiss is not covered (no JS context to observe the receipt) — acceptable: the next FCM push to that room renders a fresh conversation from cache-empty state. |
|
||||
|
||||
Why MessagingStyle vs the old per-event flow: 5 messages in one DM previously
|
||||
produced 5 separate cards in the shade with redundant title/avatar. The
|
||||
MessagingStyle conversation matches WhatsApp/Telegram UX and is the documented
|
||||
Android pattern for messaging apps. See element-android's
|
||||
`RoomGroupMessageCreator` for the canonical reference.
|
||||
|
||||
Why two channels (DM + group) and not per-conversation channels (the
|
||||
fluffychat approach): per-conversation works for low-room-count clients but
|
||||
proliferates user-visible settings entries on a Matrix client with dozens of
|
||||
active rooms. Element-android sidesteps the question with a NOISY/SILENT
|
||||
split based on push rules; we picked a middle ground — bucketed by DM vs
|
||||
group room — which mirrors fluffychat's `directChats`/`groupChats`
|
||||
NotificationChannelGroup setup.
|
||||
|
||||
Why reply action is gated on `!isEncrypted`: the Java path has no key
|
||||
material to sign + encrypt outgoing replies with, so an inline reply in an
|
||||
E2EE room would send cleartext (Synapse does not enforce the
|
||||
"encrypted-only" rule, so the leak is real). The snapshot defaults to
|
||||
`isEncrypted=true` on cache miss and the JS side re-dumps on
|
||||
`m.room.encryption` state events so the action is dropped within seconds of
|
||||
a room being switched to E2EE.
|
||||
|
||||
Why call-session composite dedup
|
||||
(`compositeCallDedupKey(roomId, sessionId)`): the legacy per-eventId dedup
|
||||
misses re-rings of the same call session because each ring is a fresh
|
||||
`m.rtc.notification` event with a new event_id. We extract the parent call
|
||||
event_id from `content.m.relates_to.event_id` (Worker JSON parse) /
|
||||
`content_m.relates_to_event_id` (FCM Sygnal-flatten) and mark the composite
|
||||
in NotificationDedup the moment we post the first CallStyle. Subsequent
|
||||
ring events for the same session see the mark and skip silently. Mirrors
|
||||
element-web's `getIncomingCallToastKey` pattern.
|
||||
|
||||
Why edit-collapse (`m.replace`) is **NOT implemented**: requires parsing
|
||||
`content.m.relates_to.rel_type == "m.replace"` + finding the original event
|
||||
in the per-room cache and replacing in place. The complication: FCM
|
||||
payloads (Sygnal-flattened) encode nested keys inconsistently across
|
||||
deployments (`content_m.relates_to_rel_type` vs
|
||||
`content_m_relates_to_rel_type` vs dot-preserved variants), and the Worker
|
||||
parses raw JSON cleanly while FCM hits one of the flattened shapes.
|
||||
Asymmetric handling (Worker only) creates user-visible drift between
|
||||
delivery paths. Real-world impact is low — users rarely edit
|
||||
notification-flagged messages in the seconds-long window before they're
|
||||
read — so the feature is deferred until we have a uniform key shape from
|
||||
Sygnal config or until a real-world report justifies the parser complexity.
|
||||
|
||||
|
||||
|
||||
1. On the phone, enable Wireless debugging, tap "Pair device with pairing code" — note IP, port, 6-digit code.
|
||||
2. `adb pair <ip>:<pair-port> <code>`
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue