The transcript is where a proposal is met, and it is a bad place to keep one. It answers one question -- is this still asking something of you -- in the middle of everything else the room said that day, and then it scrolls. Until now the only other way in was a signing screen that resolved "the room's live session", so a room with two proposals open had one of them reachable and the group's history had none of it. So: one row per session, newest first, live. The ones still waiting on the reader are gathered under "Waiting for you" and the rest follow, because they are two different kinds of thing to read -- the rest is what the group has done, those are what it is waiting on this member for -- and because burying the second open proposal under the first one's history is the whole failure this screen exists to answer. **Each row carries its own state, from the same rule the signing screen uses.** Whether a proposal still has a decision in it is asked of `FrostSigningManager.isAwaitingApproval` rather than worked out again here, so the list and the screen behind it cannot come to different answers about the same session. The rest of the status is the session's own stage said briefly: you agreed and it is waiting on n of m, you are one of the signers, enough members took part without you, signed, or -- for an abandoned one -- its failure reason, because "declined on this device" and "the group could not agree" are different things to have happened and the reason is the whole content of the ending. **Named after the lead item.** A batch's first item is the one the rest hang off -- a chapter's chunks carry the chapter's id -- so the chapter names the row and the count says the rest of it. An item that could not be read is said on the row rather than left for the screen behind it: a batch is all-or-nothing, so an unreadable item is a reason to refuse the whole proposal. **One query, not one per row.** `LocalFrostSigningSession` embeds the session and relates its items and its proposer, and `observeSessionsForChatRoom` becomes a `@Transaction` query over it -- the repository already declared that method and nothing called it, so this is the shape it should have had rather than a second query beside it. The model sorts the items rather than the query: Room does not order a relation, and item order is protocol rather than presentation, since two devices reading a batch in different orders aggregate against different messages. The proposer is joined for the reason the transcript joins a sender -- so a rename follows, and a member seen only as a pubkey is not stuck on the placeholder their profile was created with. **Derived once per emission.** Every row's events come out of stored JSON. That is not work to repeat on each recomposition of a scrolling list, so the view model does it when the flow emits and the screen renders what it is handed. Reachable from the group's detail screen, beside Shared Key and gated the same way: a shared threshold key only means anything in a room where every member is an equal admin, and a room with no key to sign with has nothing to propose. **Tests.** FrostSigningSessionDaoJvmTest covers what the list reads -- two sessions in a room come back newest first, each with its items in `itemIndex` order despite the relation's own order, and with the proposer resolved to a name. 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…