Kgothatso Ngako ccaef5d36a fix(subgroups): read the admin list off the group's data, not off a flag no creator ever sets
"Only an admin of this group can make a subgroup", said to the admin who had just
made the group. The guard was reading the wrong thing.

**`Participant.adminAt` is written in exactly one place**:
`MarmotInboundManager.processGroupMembershipChanges`, reached only from `NostrDao`
on a `GroupEventResult.CommitProcessed` -- an *arriving* commit. The rows a
creator makes come from `getOrCreateChatRoom` and `addMembers`, neither of which
sets it. So on the device that created a room, every participant reads as a
non-admin, including the creator, until some other member sends a commit it
processes. A freshly made `#admins` room has had no commits at all, so a group
whose epoch-0 context names three admins has none of them flagged on the one
device certain to be one.

**The guard now reads `MarmotGroupData.adminPubkeys`** off the room's own MLS
state. That is the thing the column is a cache of: MIP-01 stamps it into the
epoch-0 group context, every member gets it in their Welcome, and
`processGroupMembershipChanges` reads exactly this when it writes the flag.
Reading it directly cannot drift from what the group agreed and is right on both
sides from the first moment. A room whose group data cannot be read refuses rather
than falling back to the column.

The compiler caught a second bug while this was being written: the parent's admin
list was named `adminPublicKeys`, shadowing the parameter of the same name that
holds the *subgroup's* picked admins -- and the ceremony room is derived from that
parameter. It is now `parentAdminPublicKeys`, with a comment saying why the two
must never be confused.

**The column is caught up as well, because the UI still labels members with it.**
`MarmotGroupCreation` stamps `adminAt` on the rows of the members the room was
created with as admins -- the same list baked into `MarmotGroupData` a few lines
above, so nothing new is asserted. Flags only: `processGroupMembershipChanges`
also removes participants missing from the MLS tree, and running the whole
reconciliation would soft-delete the rows of anyone `addMembers` could not reach,
turning a partial invite into a partial membership. Widening a flag is safe;
narrowing membership on a best-effort step is not. Swallowed on failure like
`adopt` beside it: a missing flag costs a button not being offered, not
correctness.

**The old fixture was testing nothing.** It built the parent as a NIP-17 room with
MLS state bolted on, and `ChatRoom.deriveChatRoomId` returns a 33-byte compressed
key (66 hex) while `MarmotGroupData.nostrGroupId` takes 32 -- so `toExtension()`
produced an extension `currentMarmotData()` read back as **null**, silently,
taking the admin list with it. Every guard test was passing on that null. The
parent is now built as what a real one is: the `#admins` room derived from the
group's key, with `adminAt` deliberately left null on every row, which is the
state the guard has to work in.

Four new tests. Two in `SubgroupManagerJvmTest`: an admin is admitted with every
`adminAt` still null, and a room with no MLS state has no admin list to consult.
Two in a new `MarmotGroupCreationJvmTest`: the creator is flagged an admin of the
room they just made, and the room can read its own group data back -- the second
asserting `nostrGroupId` is the 64-hex derived id, since the wrong length there
fails by returning null rather than by throwing.

397 common tests, 712 jvm tests, `m3Audit` meets every budget.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 00:30:53 +02:00
2026-07-05 23:21:02 +02:00
2026-07-28 09:10:20 +02:00
2026-03-24 04:47:19 +02:00
2026-03-23 01:41:39 +02:00
2026-03-23 01:41:39 +02:00
2026-03-23 01:41:39 +02:00

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 thats 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 Apples 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 youre 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 IDEs 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 IDEs 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 IDEs toolbar or open the /iosApp directory in Xcode and run it from there.


Learn more about Kotlin Multiplatform

Description
Mantra as a kotlin multiplatform project
https://mantra.press
Readme 8.2 MiB
Languages
Kotlin 100%