The switch. A subgroup's ChillDKG and its key state now happen in the parent's Marmot room; the sibling NIP-17 room derived from the child's admins is gone, and so is the rule that one admin set could hold one subgroup forever. This is what `docs/subgroups.md` deferred in "Why not the parent's Marmot room" and said to revisit. The reason to wait was `docs/mls-skipped-keys.md` -- a DKG cannot finish until every participant takes part, so one group event lost to the skipped-keys bug stalls it for everybody -- and that is being fixed. The two transports and the two guards this needs landed in the three commits before it. **`SelectSubgroupAdminsViewModel` stops standing up a room.** It opens the ritual in `loaded.parentRoom`, p-tagged to the picked admins, and hands back to the parent's transcript. That transcript is where the other admins were going to answer from anyway -- `observeCeremoniesAwaitingYou` and `observeProposalsAwaitingYou` are room-scoped and were already looking there -- so the coordinator now lands in the same place as everybody else rather than in a room only they knew was coming. The name and the admin set ride on the proposal and land on the session, which is what `1e7ddd84`'s two columns are for. `MarmotGroupName.of` still puts the `#` on at the two sites that use the name, and is still idempotent, so the certificate the parent signs and the room `MarmotGroupCreation` creates are called the same thing. **`SubgroupManager.ceremonyRoomIdFor` is deleted and `refuseCeremonyRoom` becomes `refuseSubgroup`.** There is no ceremony room to derive or to name a refusal after. The refusal it made -- "this group already has a subgroup run by exactly these members", forever -- narrows to a ceremony over those admins under that parent that is still *running*, on `getLatestSubgroupSessionFor`. That refusal is worth keeping for a reason the old one did not have. It is not that two subgroups over the same people are forbidden; it is that nobody can answer for two live ceremonies at once. Every admin would be asked twice, on two ladders, for two keys, one of which nobody will make a room from. A *finished* one means that subgroup exists, and asking for another is a legitimate ask that the derived room made impossible. **Three reads move from the room to the ceremony**, all of them in the ritual screen, and each would have been silently wrong in the parent's room: - `proposeBirthCertificate`'s `adminPublicKeys` -- the room's roster would ask the parent to certify itself as its own child; - `createAdminGroup`'s `members` -- it would welcome the whole parent into the subgroup; - the name both of those carry -- it would be the parent's. `DkgRitualUIState.ritualMembers` follows, so the progress ladder draws the ceremony's participants rather than a row per parent member, and does not report a ceremony waiting on people it was never with. Profiles still come off the room, which for a subgroup is the parent and holds every admin the ceremony can have. **Nothing new is offered on a Marmot room's detail screen.** The shared-key button is still gated on `mlsGroupState == null`, and the comment there now says why that is still right: offering a ceremony to a room is offering it a key of its own, and a Marmot room's id is derived from a key it already has. Its subgroups' ceremonies are reached from the transcript line that names the one they are about, which is the only thing that can say which. `SubgroupManagerJvmTest` follows the refusal. The two tests built on `ceremonyRoomIdFor` become tests of what actually distinguishes ceremonies now -- another subgroup's and the room's own do not block one, a live one over the same admins does, a finished one does not -- and the ordering test moves to `DkgSession.formatParticipants`, which is where "the same set however it was assembled" now has to hold. 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…