Completes the outbound half: a message with a recipient is queued as the rumor a gift wrap will carry, and the notary wraps it on its way into MLS. Everything downstream -- MLS encrypt, outer ChaCha20, kind:445, persistence, broadcast -- is untouched and does not know the difference. sendChatMessage resolves the recipient against the room before it writes anything. A recipient outside the room cannot be sent to: the wrap would be undecryptable by every member including them, while the group still saw that a private message had gone somewhere. Sending to yourself is refused for a different reason -- the wrap's key is discarded, so it could never be read back. A direct message keeps kind:14 rather than being mapped down to the group's kind:9 the way an ordinary message is. It is a NIP-17 chat message that happens to travel inside a group, and the kind is what tells the two apart on the way back in. Two things here are less arbitrary than they look: The queued row IS the rumor -- same kind, same tags, same content, same timestamp -- so its id is the one the recipient computes after unwrapping. That is the identity of the message on both sides. Which means the wire event's id is NOT the row's, and one existing lookup assumed it was. `getChatMessagesByMarmotInnerEventId(innerEvent.id)` now keys on the queued row instead. Left alone, a direct message's wrap id would match no ChatMessage, the lookup would come back null, and no BroadcastNostrEventRequest would ever be inserted -- encrypted, persisted, and silently never sent, with no error anywhere. The two ids are the same value for every other kind of message, so nothing else changes behaviour. The plaintext is scrubbed from the queued row once it has been sent. It is already on the ChatMessage row, which is the sender's only copy; a second one would be cleartext left behind in a table that otherwise holds nothing but wire events. sealGiftWrapPayload now refuses any payload belonging to an MLS room. That path is the one way a gift wrap reaches a relay -- the notary watches for payloads with no seal, seals them, and broadcasts -- and a Marmot direct message is a real, correctly signed NIP-59 wrap, indistinguishable from something this path would be right to publish. Keeping direct messages out of GiftWrapPayload is what makes them unbroadcastable; this refuses at the other end too, rather than trusting every future caller to know that. 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…