A NIP-17 room has no MLS group, no key packages and no invites -- membership *is* the p-tag set on each message. Two things follow, and both are load-bearing. The room id is deriveChatRoomId over the member set, the same aggregate the inbound path derives from an arriving gift wrap. That is what makes creation idempotent, and idempotence here is not a nicety: two people starting the same conversation have to land on one room, or the thread exists twice with each side writing into its own copy and neither seeing the other. Covered from three angles -- the order members are named in does not change the id, creating the same conversation twice reuses the room as it stands rather than rewriting it, and a different member set derives a different room. The order-independence test is guarding `deriveChatRoomId`'s own `.sorted()`, not the DAO's `.toSet()`, and its comment now says so. That was established by mutation rather than assumed: rebuilding the member set as an order-preserving LinkedHashSet in the DAO changes nothing, because the derivation sorts anyway, while removing the sort fails the test. The distinction matters for anyone reading the DAO and concluding the set is what does the work. And `mlsGroupState = null` is what marks the room NIP-17. sendChatMessage reads exactly that field to choose between a kind:445 group event and per-recipient gift wraps, so a room that acquired MLS state would have its messages routed down a path no recipient is running. Covered alongside: the creator is a participant of their own conversation even when not listed among the participants -- sealGiftWrapPayload walks that list to decide who to wrap for, so omitting the creator would send messages every other member could read and the sender could not -- and naming the creator among the participants does not produce a second row for them. One test records a precondition and an asymmetry. Participant.participantPublicKey is a foreign key onto Profile, so createNip17ChatRoom raises a SQLite constraint failure for a member this device has no profile for, while getOrCreateChatRoom, one method down, answers the same "never seen this user" situation by returning null. A caller treating the two alike gets an unhandled exception out of the first. That was found by writing the tests -- seven of them failed with SQLite 787 before every member was seeded -- and is pinned rather than seeded around silently. The remaining getOrCreate coverage: it returns the room already stored rather than overwriting it with the defaults passed in, stands one up for a user it has a profile for, and writes nothing at all when it does not. Real secp256k1 keys throughout, because deriveChatRoomId does point arithmetic and treats an off-curve value differently from a valid one -- hex filler would exercise a path users never reach. 11 tests. composeApp jvmTest is 309 tests, 0 failures. 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…