diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/repository/DatabaseFrostSigningRepository.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/repository/DatabaseFrostSigningRepository.kt index 4a0c514c..951bd62b 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/repository/DatabaseFrostSigningRepository.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/repository/DatabaseFrostSigningRepository.kt @@ -13,7 +13,6 @@ import press.mantra.compose.database.model.FrostSigningItem import press.mantra.compose.database.model.FrostSigningSession import press.mantra.compose.database.model.intermdiate.LocalChatRoom import press.mantra.compose.database.model.intermdiate.LocalFrostSigningSession -import press.mantra.compose.database.model.types.FrostSigningStage import press.mantra.compose.managers.FrostSigningManager import press.mantra.compose.repository.FrostSigningRepository @@ -41,14 +40,6 @@ class DatabaseFrostSigningRepository( override suspend fun getSessionById(sessionId: String): FrostSigningSession? = database.frostSigningSessionDao().getSessionById(sessionId) - override suspend fun liveSessionForChatRoom(chatRoomId: String): FrostSigningSession? { - val sessions = database.frostSigningSessionDao().getSessionsForChatRoom(chatRoomId) - - return sessions.firstOrNull { - it.stage != FrostSigningStage.COMPLETE && it.stage != FrostSigningStage.FAILED - } ?: sessions.firstOrNull() - } - override suspend fun canSign(chatRoomId: String): Boolean = FrostSigningManager.canSign(database, chatRoomId) diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/repository/FrostSigningRepository.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/repository/FrostSigningRepository.kt index 7d13801c..fbaaffbf 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/repository/FrostSigningRepository.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/repository/FrostSigningRepository.kt @@ -45,16 +45,6 @@ interface FrostSigningRepository { suspend fun getSessionById(sessionId: String): FrostSigningSession? - /** - * The session a room is currently running, or its most recent one if none is. - * - * For callers that mean "the signing going on here" without holding an id -- - * a transcript line, mostly. Prefers a live session because that is the one - * anybody tapping through wants to act on; a finished one is only what is - * left to show when there is nothing live. - */ - suspend fun liveSessionForChatRoom(chatRoomId: String): FrostSigningSession? - /** Whether this room holds a key it can sign with, so the UI offers nothing that would fail. */ suspend fun canSign(chatRoomId: String): Boolean @@ -156,8 +146,6 @@ interface FrostSigningRepository { override suspend fun getSessionById(sessionId: String): FrostSigningSession? = null - override suspend fun liveSessionForChatRoom(chatRoomId: String): FrostSigningSession? = null - override suspend fun canSign(chatRoomId: String): Boolean = false override suspend fun proposeSigning( diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ChatRoomMessagingScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ChatRoomMessagingScreen.kt index e909ab6c..b023c243 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ChatRoomMessagingScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ChatRoomMessagingScreen.kt @@ -48,6 +48,7 @@ import press.mantra.compose.repository.NostrRepository import press.mantra.compose.ui.composable.navigation.routes.ChatRoomDetailRoute import press.mantra.compose.ui.composable.navigation.routes.DkgRitualRoute import press.mantra.compose.ui.composable.navigation.routes.FrostSigningRoute +import press.mantra.compose.ui.composable.navigation.routes.ProposalListRoute import press.mantra.compose.ui.composable.navigation.routes.Route import press.mantra.compose.ui.composable.widgets.LoadingDataIndicator import press.mantra.compose.ui.theme.TorchTheme @@ -147,14 +148,25 @@ fun ChatRoomMessagingScreen( ) ) }, - onOpenSigning = { - // No session id: a chat row carries none, and the - // screen resolves the room's live one. + onOpenSigning = { sessionId -> + // A line written before chat rows named their + // session cannot say which proposal it is about, + // and the room may have several. The list is the + // honest answer: it shows all of them with their + // own state, rather than guessing at one. onNavigateToRoute.invoke( - FrostSigningRoute( - activeUserPublicKey = activeUserPublicKey, - chatRoomId = chatRoomId - ) + if (sessionId == null) { + ProposalListRoute( + activeUserPublicKey = activeUserPublicKey, + chatRoomId = chatRoomId + ) + } else { + FrostSigningRoute( + activeUserPublicKey = activeUserPublicKey, + chatRoomId = chatRoomId, + sessionId = sessionId + ) + } ) } ) diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/FrostSigningScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/FrostSigningScreen.kt index a62b92d1..8032eabb 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/FrostSigningScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/FrostSigningScreen.kt @@ -74,7 +74,7 @@ import press.mantra.compose.ui.view.state.FrostSigningUIState fun FrostSigningScreen( activeUserPublicKey: HexKey, chatRoomId: String, - sessionId: String?, + sessionId: String, initialFrostSigningUIState: FrostSigningUIState = FrostSigningUIState.Loading, chatRepository: ChatRepository, frostSigningRepository: FrostSigningRepository, diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/navigation/routes/FrostSigningRoute.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/navigation/routes/FrostSigningRoute.kt index 3d7575f7..7f8e7637 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/navigation/routes/FrostSigningRoute.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/navigation/routes/FrostSigningRoute.kt @@ -8,20 +8,17 @@ import kotlinx.serialization.Serializable * Carries a session id rather than only a room, because a group signs * repeatedly and can have more than one session open at a time -- unlike a * ceremony, where "the room's ritual" identifies it. + * + * The id is required. It used to be optional, resolved to "the room's live + * session" for callers that did not know which one they meant -- a transcript + * line, mostly, since chat rows carried no session. Two proposals open at once + * made that guess wrong, and every one of those lines now names its session; a + * line too old to have one goes to [ProposalListRoute] instead, where the + * reader picks from the whole list rather than being sent to a guess. */ @Serializable data class FrostSigningRoute( val activeUserPublicKey: String, val chatRoomId: String, - - /** - * Null when the caller does not know which session it means. - * - * A transcript line is the main case: chat rows carry no session, and adding - * a column for one feature to a table every message uses is a poor trade for - * a lookup the screen can do. It resolves to the room's live session, which - * is the one a line is talking about in every case but a group running two - * at once. - */ - val sessionId: String? = null + val sessionId: String ): Route() diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/ChatMessageListViewModel.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/ChatMessageListViewModel.kt index 4ba70a91..e40b70a2 100755 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/ChatMessageListViewModel.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/ChatMessageListViewModel.kt @@ -237,7 +237,15 @@ class ChatMessageListViewModel( @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable - fun RenderMessages(onOpenSharedKey: () -> Unit, onOpenSigning: () -> Unit) { + fun RenderMessages( + onOpenSharedKey: () -> Unit, + /** + * Opens the signing this line is about, by session id -- or the room's + * whole list of proposals when the line predates [ChatMessage.frostSigningSessionId] + * and cannot say which one it meant. + */ + onOpenSigning: (sessionId: String?) -> Unit, + ) { Column( modifier = Modifier.fillMaxWidth(), @@ -383,7 +391,11 @@ class ChatMessageListViewModel( localChatMessage = localChatMessage, isAnswered = localChatMessage.chatMessage.id in answeredRequests, isSettled = localChatMessage.chatMessage.id in settledRequests, - onClick = onOpenSigning + onClick = { + onOpenSigning( + localChatMessage.chatMessage.frostSigningSessionId + ) + } ) return@items } diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/FrostSigningViewModel.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/FrostSigningViewModel.kt index 6a1d0afa..3dc63459 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/FrostSigningViewModel.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/view/model/FrostSigningViewModel.kt @@ -25,7 +25,7 @@ import press.mantra.compose.ui.view.state.FrostSigningUIState class FrostSigningViewModel( val chatRoomId: String, - val sessionId: String?, + val sessionId: String, val activeUserPublicKey: HexKey, initialFrostSigningUIState: FrostSigningUIState, val chatRepository: ChatRepository, @@ -53,22 +53,12 @@ class FrostSigningViewModel( return@launch } - // A transcript line knows its room but not its session, so resolve one - // before watching anything. - val id = sessionId - ?: frostSigningRepository.liveSessionForChatRoom(chatRoomId)?.id - if (id == null) { - frostSigningUIState = - FrostSigningUIState.Error("This group is not signing anything right now.") - return@launch - } - frostSigningUIState = FrostSigningUIState.Loaded(localChatRoom = localChatRoom) combine( - frostSigningRepository.observeSessionById(id), - frostSigningRepository.observeMessages(id), - frostSigningRepository.observeItems(id) + frostSigningRepository.observeSessionById(sessionId), + frostSigningRepository.observeMessages(sessionId), + frostSigningRepository.observeItems(sessionId) ) { session, messages, items -> Triple(session, messages, items) } .collect { (session, messages, items) -> frostSigningUIState = FrostSigningUIState.Loaded( @@ -138,7 +128,7 @@ class FrostSigningViewModel( fun factory( chatRoomId: String, - sessionId: String?, + sessionId: String, activeUserPublicKey: HexKey, initialFrostSigningUIState: FrostSigningUIState = FrostSigningUIState.Loading, chatRepository: ChatRepository, diff --git a/docs/frost-batch-signing.md b/docs/frost-batch-signing.md index ab051935..a9c2e838 100644 --- a/docs/frost-batch-signing.md +++ b/docs/frost-batch-signing.md @@ -426,6 +426,16 @@ what a line older than the column has to be read by, and is right for those rooms: nothing that predates batch proposals ran two sessions at once. A ceremony line still uses the clock always, since a room runs one ritual at a time. +The transcript answers one question about a proposal — is this still asking +something of you — and answers it in the middle of everything else the room said. +Two open proposals need a place that shows both, so `ProposalListScreen` lists a +room's sessions newest first with the ones waiting on the reader gathered at the +top, and the room's history under them. `FrostSigningRoute.sessionId` stopped +being optional at the same time: it existed for transcript lines that could not +say which session they meant, resolved to "the room's live session", and that +guess is exactly what two proposals make wrong. A line too old to name its +session opens the list instead. + --- ## Phase 6 — the test that actually proves it