Files
mantra-kmp/composeApp/src
Kgothatso Ngako b50b1762d4
Some checks failed
Material Design conformance / budgets (push) Has been cancelled
Material Design conformance / tests (push) Has been cancelled
feat: put a room's signing key first on its detail screen, and the signed event behind it
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>
2026-09-08 21:52:36 +02:00
..