Kgothatso Ngako 73cba2ae0e feat: say at the foot of the transcript what the group is still waiting for you to sign
The transcript carries a proposal past as it happens, and b977326 gave a member
owed two decisions a queue to find the second one in. Neither says anything
before it is tapped. A proposal announces itself as one line among everything
else the room said, the conversation carries it upward, and from then on the
only evidence that the group is waiting on this member is a line they would
have to scroll back to -- or the Proposals button on a screen behind a
kebab menu, which is a place to look rather than a thing that tells you to
look. The decision does not expire with the scroll, and a member who has not
answered is what the whole room is waiting on.

**Where it sits.** First item of the transcript's LazyColumn, which has
`reverseLayout = true`, so index 0 is at the bottom edge of the viewport --
under the newest message and directly above the composer. That is also where
the list is scrolled to when a room is opened, so a member who has just been
asked for something is told so without moving. `ChatMessageDao` orders
`createdAt DESC` and the reversal turns it back the right way up, which is why
"first item" and "under the newest message" are the same place.

**Not pinned above the composer.** It scrolls with the transcript and leaves
view when a reader goes back through history. Pinning it is not a move of the
composable: `RenderMessages` is a Column of `Spacer(weight(1f))` then the
messages column, and a Column measures its unweighted children first and in
order, each against what the previous ones left. The messages column holds a
LazyColumn with no height modifier, which takes the whole remaining height, so
a sibling card placed after it would be measured against nothing and would not
appear. Making that work wants `weight(1f)` moved onto the messages column,
which changes how every part of this screen is measured rather than where one
card goes.

**Guarded outside `item { }` rather than inside it.** The relay-list notice
beside it is written the other way round -- an item that always exists and
sometimes composes nothing -- and `verticalArrangement = Arrangement.spacedBy`
puts its gap between every pair of adjacent items whatever their height, so an
item composing nothing still costs 10.dp. This one is not added at all when
nothing is owed, so a room with no proposals carries no phantom gap at the foot
of its transcript.

**Read before the list builder.** `proposalsAwaitingYou` is pulled into a local
above the `LazyColumn` call rather than inside its scope, so the state read is
plainly a read of `RenderMessages` and the notice appearing or disappearing is
a recomposition of this function. Reading it inside the builder would work --
the item provider is snapshot-aware -- but it puts the difference between "no
proposals" and "one proposal" inside a lambda whose re-execution is the lazy
list's business rather than this function's.

**What the held state carries now.** `proposalsAwaitingYou` widens from
`Set<String>` to `List<AwaitingProposal>`: the session id it already had, plus
a `ProposedEvent.Summary` of what the batch is about and the batch's size. It
is still filled from the same `observeSessionsForChatRoom` collector asking
`FrostSigningManager.isAwaitingApproval` -- the point of b977326's version was
that the transcript and the proposal list ask one question, and that is
unchanged.

The summary is built in the collector rather than at render because it is
parsed out of stored JSON, once per item, and this is a scrolling list.
`ProposalListUIState.Proposal` derives its own for the same reason and says so;
doing it in the composable would re-parse every event in every open proposal on
every recomposition of the room.

**The first item that can be read, not the first item.** `firstNotNullOfOrNull`
over `Event.fromJsonOrNull`, matching `ProposalListScreen`, whose `lead` is the
first of the already-`mapNotNull`ed events. A batch is named after its first
item because the rest hang off it, but a batch whose first item this build
cannot parse is still about something, and falling back to "Proposal" when the
second item says "New chapter" would be throwing away the name for a reason the
reader cannot see.

**Named when there is one, counted when there are several.** One proposal gets
"Waiting for your signature" over what it signs -- "New chapter · Genesis 1 ·
797 words · 31 chunks" -- because "a proposal is waiting" is not something
anybody can decide about, and the whole value of the card over a dot on a menu
is that it says what the group wants. Several get the count and nothing else.
Naming the first of several would say the others were not there, which is
exactly the fault `ProposalListScreen` was built to fix; listing them all would
be building that screen a second time in the composer's space.

**The two ways to have nothing to name.** A session can exist before its
proposal has arrived, and a proposal can arrive holding events this build
cannot read. They are different situations and the card says which, in the same
words `ProposalCard` uses -- "Nothing has arrived to sign yet" against "None of
its events could be read" -- so a member who taps through finds the row saying
what the card said.

**It opens the queue, in every case.** Not the proposal it names, even when it
names exactly one. The transcript's own lines are the way to a single proposal
and keep their existing routing; this is the standing count of what is owed,
and the queue is the screen that answers the question it raises -- including
for a proposal it could not name, where opening one session would be opening
the one thing the card just admitted it could not describe.

**Its own callback rather than `onOpenSigning(null)`.** That would have worked:
`ChatRoomMessagingScreen` already sends a null session id to `ProposalListRoute`.
But null there is a claim -- "this line predates `ChatMessage.frostSigningSessionId`
and cannot say which proposal it meant" -- and the card knows precisely which
proposals it is about. Reusing the branch would make the null case mean two
unrelated things and leave the next reader unable to tell which callers
actually do not know their session. `onOpenProposals: () -> Unit` says the one
thing it does.

**`hidesOtherDecisions` is untouched in meaning.** It follows the new shape --
`size > 1 && any { it.sessionId == sessionId }`, with the cheap check first --
and still answers the same two questions about a tapped transcript line. No
line changes where it goes.

**Not covered, deliberately.** The empty-transcript branch gets no card. A
proposal writes its own lines into the room that signs, which is what b977326
established, so an awaiting proposal implies a transcript; the branch this
skips is the "Break the ice" case, which cannot coexist with one.

The card lives and dies with an open room. b977326 closed by noting that
nothing counts outstanding decisions where a reader can see them before
tapping, and that is still true one level up: the home chat list says nothing,
and a badge there wants this count somewhere it outlives one open room, which
is its own change.

`ChatRoomMessagingScreen` still calls `initiate()` inside `key(true) { }`
rather than a `LaunchedEffect`, so its collectors are re-launched on
recomposition. This adds no observer -- it reads the one b977326 added -- so
the exposure is unchanged rather than widened.

No tests. What changed is a composable and a navigation callback, and there is
no UI test harness here to press a card in. The seam that does have one,
`isAwaitingApproval`, is untouched and is already what the proposal list is
tested through; the summary this reuses, `ProposedEvent.summarize`, is likewise
already covered where it is defined.

Verified: :composeApp:compileDebugKotlinAndroid succeeds; 884 tests pass, 565
jvm and 319 android, unchanged from before the change since it adds none. That
the card appears exactly when a proposal is owed, and that it lands on the
queue, are read from the code rather than asserted -- both want the app on a
device in a group that has a key.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 21:20: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%