Kgothatso Ngako ba05ec704d feat(chat): keep a gift wrap subscription open for as long as the app is active
The first live subscription: kind 1059 p-tagged to us, on every DM relay, open
until the wallet changes or the process ends. That covers direct messages and
the Marmot Welcome events that make us a member of a group — so a DM now lands
whether or not anyone has the chat list open, and a group invite is noticed
without waiting for the next screen to schedule a sync.

Nothing is removed from the queue yet. ChatRoomListViewModel and
ChatMessageListViewModel still schedule what they always did; this runs beside
them and the duplicate events cost a no-op storeNostrEvent each. The screens
stop doing it in the last step, once groups are covered too.

**No `since`.** NIP-59 randomizes a wrap's created_at into the past, and our own
outbound path stamps them with TimeUtils.randomWithTwoDays() — so a wrap
published right now can carry a timestamp two days old, and a `since` anywhere
near the present would silently drop it. "Some messages just never arrive" is
the worst failure mode to debug, and re-receiving a wrap costs one no-op write.

**`limit` instead.** NIP-01 scopes limit to the initial query — the stored
events a relay sends before EOSE — and explicitly not to the stream that
follows, so limit=100 bounds what a reconnect costs without touching the live
tail. Filling in the rest of the history stays negentropy's job.

**What ends a subscription, and what doesn't.** CLOSED does, and is retried with
backoff from 5s to 5m, jumping straight to the maximum when the reason parses as
back-pressure — answering "too many subscriptions" by promptly opening another
is how one refusal becomes a flood. EOSE does not end anything: it is the
boundary between stored history and the live tail, and treating it as an end is
precisely what made every sync a poll. It does reset the backoff, since a
subscription that got that far was working and whatever ended it is a new
problem. A dropped socket needs nothing from this loop at all — the pool replays
the REQ on reconnect and the collector, attached to the socket client rather
than to a session, simply starts receiving again.

A NOTICE is logged and otherwise ignored. It carries no subscription id, so it
reaches every collector on the socket; acting on one here would let an unrelated
relay complaint tear this subscription down.

Events are saved inline rather than in a launched coroutine, so they land in the
order the relay sent them and the socket's buffer does the back-pressure,
instead of fanning a burst out into a coroutine per event.

Wiring: LiveSubscriptionManager is built by SynchronizationViewModel because it
has to share that class's RelaysSocketManager — a second one would mean a second
RelayPool and a duplicate socket per relay — and is launched inside the same
per-wallet supervisorScope as the three pumps, so a wallet switch tears it down.
It deliberately does not take from the pumps' Semaphore(4): a subscription that
never finishes would hold a permit forever and permanently halve backfill
throughput. The comment there now says so.

isBackPressure moves out of SynchronizationViewModel to
network/relays/RelayBackPressure.kt, unchanged, now that two callers need it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 16:09:08 +02:00
2026-07-05 23:21:02 +02:00
2026-07-28 09:10:20 +02:00
2026-03-24 04:47:19 +02:00
2026-03-23 01:41:39 +02:00
2026-03-23 01:41:39 +02:00
2026-03-23 01:41:39 +02:00

This is a Kotlin Multiplatform project targeting Android, iOS, Desktop (JVM).

  • /composeApp is for code that will be shared across your Compose Multiplatform applications. It contains several subfolders:

    • commonMain is for code thats common for all targets.
    • Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name. For example, if you want to use Apples CoreCrypto for the iOS part of your Kotlin app, the iosMain folder would be the right place for such calls. Similarly, if you want to edit the Desktop (JVM) specific part, the jvmMain folder is the appropriate location.
  • /iosApp contains iOS applications. Even if youre sharing your UI with Compose Multiplatform, you need this entry point for your iOS app. This is also where you should add SwiftUI code for your project.

Build and Run Android Application

To build and run the development version of the Android app, use the run configuration from the run widget in your IDEs toolbar or build it directly from the terminal:

  • on macOS/Linux
    ./gradlew :composeApp:assembleDebug
    
  • on Windows
    .\gradlew.bat :composeApp:assembleDebug
    

Build and Run Desktop (JVM) Application

To build and run the development version of the desktop app, use the run configuration from the run widget in your IDEs toolbar or run it directly from the terminal:

  • on macOS/Linux
    ./gradlew :composeApp:run
    
  • on Windows
    .\gradlew.bat :composeApp:run
    

Build and Run iOS Application

To build and run the development version of the iOS app, use the run configuration from the run widget in your IDEs toolbar or open the /iosApp directory in Xcode and run it from there.


Learn more about Kotlin Multiplatform

Description
Mantra as a kotlin multiplatform project
https://mantra.press
Readme 8.2 MiB
Languages
Kotlin 100%