A profile created in the app could never start a chat with another profile created in the app: the peer always looked like it had no metadata, no DM relay list and no MLS key package. Root cause was in neither the chat code nor the relay list — nothing a new profile signed ever reached a relay. Three queue observers used `createdAt > :createdAt` with a `Clock.System.now()` default argument. Kotlin evaluates that default once, at the call site, and Room binds it for the life of the Flow; Instants persist at second resolution, so every request enqueued in the observer's own start second (the whole profile-creation burst) and everything left pending by a previous session was permanently invisible. Nothing else drains those tables. The failure was silent because `publishNostrEvent` stamps `signedAt` and indexes the Profile in one transaction, satisfying the ProfileLoaded branch before the UnannouncedProfile gate could be reached — so a device-only profile looked fully announced. Dropping the cutoff needs no schema change, so existing installs self-heal on next launch: the stranded rows are still pending. Also fixed, since they gate the same flow once events start moving: - Broadcasts now always reach a terminal status (outer timeout plus try/catch — `.catch` cannot see the suspend call that builds the flow), interrupted ones are requeued once at startup, `OK: false` is a failure rather than a recorded success, fan-out is bounded, and an uncorrelated NOTICE no longer fails whatever publish shares the socket. `take(1)` keeps the publish timeout from firing after a success on a SharedFlow that never completes. - CLOSED is parsed and handled, so a relay refusing a NEG subscription falls back to REQ instead of waiting forever; NOTICE is parsed as the two-element frame it is; negentropy timestamps use seconds, the unit relays use. - Both chat gates observe the peer's key package instead of reading it once and latching a terminal error, and queue the sync they claimed to be doing. Same-minute retries are no longer swallowed by IGNORE. - Group rooms were keyed by the MLS group id instead of the Marmot nostrGroupId (unrelated randoms, so neither side saw the other's events); inviting a member wrote no Participant row, so the Welcome produced no gift wraps, and discarded the post-addMember group state; the invite reported success unconditionally. - An inverted `containsKey` made the "missing peer DM relay list" recovery a no-op, and the wrong RelayTag class wrote "r" tags where NIP-51 relay lists expect "relay". Verified with `:composeApp:compileDebugKotlinAndroid`, including that Room's KSP regenerated the DAO impls without the frozen cutoff. Not yet exercised against live relays. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 that’s 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 Apple’s 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 you’re 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 IDE’s 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 IDE’s 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 IDE’s toolbar or open the /iosApp directory in Xcode and run it from there.
Learn more about Kotlin Multiplatform…