The note said "add a parent group opens a picker of candidate rooms" and left the list unspecified, which is the easy place to design the wrong screen. It is not the groups that exist, and not the groups C's admins can name. **A certificate is proposed inside the adopting group, so the list is the selector's own rooms.** `FrostSigningManager.proposeSigning` runs in P2, so a candidate has to be a Marmot room this device already administers and holds a share of -- which is `SubgroupManager.refuseCeremonyRoom`'s first three checks with the target moved, plus C itself, C's existing parents and C's descendants removed. The admin test reads `MarmotGroupData.adminPubkeys` rather than `Participant.adminAt`, for the reason that function already gives: the flag is written only by `processGroupMembershipChanges` on an arriving commit, so on a room this device created it is null for everybody. So a group cannot be offered as a parent by somebody who is not in it. The overlap requirement the note already states turns up here as a list length, and an empty picker is a fact about the member rather than a failure -- the remedy is a person, not a button. **The filter costs one MLS decode per room and two indexed reads.** `toMlsGroup()` deserialises the whole group state out of hex to reach `adminPubkeys`. `canSign` is cheap for exactly the rooms that pass, because `completedKey`'s first branch is a key-state lookup and a session lookup by id; its `getKeyHoldingSessions` fallback, with a `marmotGroupId` derivation per session, fires only for rooms with no key state, which fail the filter anyway. Built the way `SelectChatRoomMembersViewModel.scheduleKeyPackageEventSynchronization` builds its prefetch -- off the main thread, filling in as it resolves. **Picking a parent is making an offer, not taking a decision.** The ordering is forced -- the key state carries the certificate -- so P2's quorum commits first and C's quorum answers second. Whoever picks P2 is not choosing on C's behalf; C's admins decide at step 2 by signing the re-announced key state or not. That is why the `ProposedEvent` 30326 arm is the most important small change in the design, and the note now says so from this direction too. **And it exposes a gap the founding flow does not have.** Founding has the same ordering -- `proposeBirthCertificate` runs before the child's quorum has signed anything -- but a subgroup's admins have at least *seen* the intent, because `DkgRitualEvents` puts `parent_group` on the ceremony proposal and the ritual screen names the group. Adoption has no ceremony and so no hint, which means the first thing an admin of C would hear is a request for their signature. `ChatMessage.applyInnerEvent`'s 30329 arm should grow a second branch for it. Today it calls `SubgroupManager.record`, which asks `certifies(event, subgroup, chatRoomId)` -- the certificate's parent must be the room it arrived in -- and drops everything else. A certificate that instead names *this* room as its subject, and verifies against the parent it names, is the arriving offer and gets its own line: "#Ekklesia has offered to adopt this group", beside today's "The group made #Translators a subgroup". Both branches are fully verified and neither needs a lookup, since `certifies` checks a signature against a room id and a room id is a public key. Nothing is stored -- `GroupSignedEvent`'s foreign key onto `ChatRoom` forbids it and nothing needs it, because the certificate reaches C's devices for real inside the key state at step 2. The line is notice, not evidence, and must read as an offer rather than as a fact about C's lineage. **Two entry points are defensible and they produce an identical certificate.** C's detail screen beside the parent card, mirroring the parent screen's "add subgroup"; or P2's beside that button, where the proposal actually lands. C's is the better default, because the intent originates with the group looking for a parent and its quorum is the one that decides. P2's is worth adding later, with the same guards read in the other direction and a candidate list of "Marmot rooms I hold a key state for", since that is where `subgroup_key` comes from. Two tests follow: the candidate filter in `SubgroupManagerJvmTest` -- a room this device is in but does not administer is not offered, and neither is one it administers but holds no share of -- and the new arm's branch in `ChatMessageJvmTest`. Sizing moves from six tests to eight and from one text file to two. Docs only -- no code changed, so there are no test counts to report. 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…