61480ddgave the group three lines: a ceremony started, it finished, somebody abandoned it. Between the first and the second the ritual was a black box. A ceremony that fixes the group's signing quorum for good ran with nothing to watch, and a stalled one -- the common case, since it finishes only once every member's device has taken part -- gave no way to see which member it was waiting on. Every protocol message now gets a line naming the member whose device sent it: 30311 host key ...joined the shared key ceremony, publishing the key their device is identified by for the rest of it. 30312 round 1 ...sent their contribution to the key. Every member's is mixed in, so no single device ever holds the whole thing. 30313 coord r1 ...combined everyone's contributions and sent the result back to be checked. 30314 round 2 ...checked the combined result and signed to confirm it matches what they sent. 30315 certificate ...gathered everyone's confirmations into a certificate. A device that has it can finish and keep its share of the key. The wording says what the step accomplishes rather than what it is called. "sent pmsg1" is not a useful thing to read in a group chat, and the protocol names are already on the shared-key screen this line taps through to. 30310 and 30316 keep the announcers they have: both say more than the message carries -- the quorum in one case, who walked away in the other -- so folding them in here would have lost that. `complete` stays the one unauthored line, because a finished ceremony has no actor: the group ends up with a key, nobody hands it to them. This changes nothing about the protocol and adds no traffic. It extends the mechanism61480ddestablished rather than adding one: the rows are written locally from ritual messages this device already has, with no giftWrapPayloadId and no event behind them, so they cannot disagree with the ritual they describe and no new kind goes on the wire. Adding the five types to DKG_TYPES and DKG_AUTHORED_TYPES was enough to get RitualNotice's system-line treatment and its name prefix, which resolves from the joined profile and so follows a rename. The one genuinely new problem is idempotency, and it is the reason for six hooks rather than one. ChatMessage has no key to make a second insert a no-op -- its id is autogenerated -- while ritual messages arrive repeatedly: relays redeliver, and replayStoredMessages feeds the whole backlog through record() again on every resume. DkgParticipantMessage absorbs both, being keyed on (sessionId, participantPublicKey, kind); a chat row cannot. So each announce is guarded by reading the row it is about to write over, the same read-before-write fail() already documents: - record(), for the three per-member kinds, checks getMessage before the upsert - record(), for the two aggregates, reads the column before update() - publishOwn(), for this device's own messages, checks ownMessage - the two coordinator broadcast sites, already inside `== null` guards publishOwn's check is deliberately redundant with its callers', which all check before publishing. The chat row is the thing with no fallback, so the check that matters sits next to the write. An echo of this device's own broadcast finds the row already stored and stays silent. Worth knowing before this meets a real group: it is 3n + 4 lines per ritual -- 19 for five members, 34 for ten -- and a ritual is a burst, not a trickle. They are compact single lines, but they will dominate a transcript while a ceremony runs. If that reads as noise, the cheap fix is collapsing consecutive ritual lines into one expandable line in ChatMessageListViewModel; the types are distinct enough to group on, so no data change would be needed. That judgement wants a real ritual first, which is also the only thing that will exercise these paths -- they are the same paths the protocol messages take, and none of it has run on a device. 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…