A group's `GroupKeyState` had no surface anywhere in the app. It decides which share a member signs with and which identity a reader will see on everything the group signs, and the only way to learn either was to read the logs. The group detail screen now opens with it, and tapping it shows the event a quorum actually put its signature to, with a button to take that event somewhere it can be checked. **First on the screen, above the description.** Which key a room signs as is the fact the rest of the room's signed work stands on -- a dialect, an artifact and a chapter are all worth exactly what the identity behind them is worth -- so it goes before the library and the dialects rather than into the settings-ish tail of the screen with reindexing and leaving. It is absent rather than empty on a room the group has said nothing about: there is no half state to report, since a room either has one a quorum signed or has none, and the shared key entry further down is already where somebody goes to make one. **The row's subtitle is the identity, not the threshold key.** Those are different values -- the group's root ChillDKG key, and that key walked to the room's path -- and only the second one appears on anything. It is what a reader checks a signature against and it is the room's own id, so it is the value a member is most likely to want to compare against something. The whole of it, along with the root key it came from, is one tap away in the sheet. **The state and the event are read separately, and neither is derived from the other.** A `GroupSignedEvent` carries every field the `GroupKeyState` row does, so one read would have done -- but the two mean different things when they are missing. The row is this device's reading, which is what the app resolves a signing request against; the event is the group's statement, with the signature on it, which is the only part that can be checked. A device holding the reading and not the statement should not be shown fields as though they were signed, and the sheet says so instead. It never happens the other way round: a state is only ever written from an event that passed both checks. **`signedEventFor` looks wherever the event is filed, which is not this room.** Since the previous commit a group agrees its key state before the room exists, so the event lives under the NIP-17 room its ceremony ran in and is authored by the Marmot room it is about. Finding it by room would find nothing. It is found by its `d` tag instead, through the same `stateFrom` that lets one be believed at all, so nothing is shown that this device would not have acted on. `stateAmong` and the new `signedEventFor` are now one walk returning both halves, because an event that produces no state must not be shown as though the group had settled anything. **The sheet shows and copies the canonical compact event JSON.** Pretty-printing it would read better in the block and was rejected: the point of copying it is to hand somebody something they can verify, and the moment the display and the copy diverge the button stops being "copy what you are looking at". What is shown is `Event.toJson()`, byte for byte, which is what a nostr tool expects to be given. **The hex is grouped in eights, which the render caught and reading did not.** Captured off a real desktop composition at 360dp, the JSON wrapped fine -- it has quotes and commas to break on -- and every key ran off the side of the sheet with its last characters unreadable. A 64-character key has no space in it, so Compose lays the whole run on one line and lets it overflow. The spaces are the break opportunities. They are also how a value meant to be compared character by character against another member's screen should have been shown in the first place, for the same reason a fingerprint or an account number is grouped. Nothing is copied from those fields, so shaping them for reading costs a paste nothing. **The value colour is stated rather than inherited.** The labels are deliberately quieter at `onSurfaceVariant` and the values are what a member came for, so they name `onSurface` instead of taking whatever `LocalContentColor` happens to be. The JSON block sits on `surfaceVariant`/`onSurfaceVariant`, which `ColorSchemeContrastTest` already measures in all six schemes. **The sheet's body is a composable of its own, and that is what makes it testable.** A `ModalBottomSheet` is a popup in its own window, which a layout test cannot reach into, so `GroupKeyStateSheetContent` is separated from the sheet that contains it. `GroupKeyStateSheetLayoutJvmTest` then renders it at phone width and asserts the *height* of the JSON: a parent that narrow caps the text's layout width whether it wraps or clips, so width would pass either way. The threshold is calibrated against the real measurement rather than guessed -- it renders 224dp wrapped, against roughly 16dp for a single clipped line, so 60dp separates them with room to spare. A second case renders the sheet for a device holding no signed event, since that branch returns early and would otherwise never be laid out. The screen's `@ConformancePreviews` gains a key state, so the row renders under all five conditions rather than only in a group that has held a ceremony. 651 jvm tests and 373 common tests pass; `m3Audit` meets every budget, with the string count unchanged at 39 -- the eleven new pieces of UI text are in the catalogue in sentence case. 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…