Every `add*` on MantraDao does two things in one transaction: writes the entity and queues a SubmissionEvent carrying the same nip30303 event for the group. The part worth asserting is the one `rumorOf` exists for. An entity's id is computed by its `Mantra*.from*EventTemplate` factory. The payload's id is computed separately, in `rumorOf`, from the same template. The two are meant to produce the *same event* -- the row on disk and the payload on the wire, not two copies of one. Nothing enforces that: the factories live in different files, both compile independently, and both produce a plausible 64-character id. A divergence would surface only as a group that receives a submission whose payload matches nothing it can find, which is a long way from the two hash calls that disagreed. Covered, through the seam rather than by recomputing the hash: the submission records `payloadEventId`, and that value has to equal the id of the entity the same call returned. Asserted for a dialect and again for an artifact version, because store-and-submit is the convention every `add*` follows rather than something addDialect does on its own -- and the second one goes through the full foreign key chain, dialect then artifact then version. Also covered: The envelope is not the payload. A submission's own id is the SubmissionEvent's and must differ from the payload's, which is exactly why `deleteByPayloadEventId` exists -- a superseded nip30303 event cannot be un-queued by its own id, and if the two ever collapsed to one value that method would start deleting envelopes by accident. The submission is queued unprocessed, `marmotGroupEventId == null`. That null is what the outbound pipeline selects on to encrypt the row into a kind:445. Filed as processed it would be stored and never sent, and the group would simply never learn about the dialect while the local device showed it as added. A ChatMessage line is written, since the room's feed reads ChatMessage and an added entity that leaves no line is invisible to everyone including its author. Verified by mutation rather than assumed: making `rumorOf` hash a createdAt one second off the template's fails both invariant tests, with the ids compared in the failure output. The mutation was reverted; no production source is touched by this commit. 6 tests. 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…