From 5c893ab1081d7f92b1358fa266deadbeb0e1fb3f Mon Sep 17 00:00:00 2001 From: Kgothatso Ngako Date: Wed, 9 Sep 2026 02:14:11 +0200 Subject: [PATCH] feat(subgroups): put the ceremony that needs you at the bottom of the chat A NIP-17 room already showed the standing "waiting for your signature" notice under the newest message -- `ProposalsAwaitingYouNotice` is transport-agnostic and reads `FrostSigningSession` by room. What it never covered is the other thing a room can owe somebody, which in a NIP-17 room is the main thing: a ceremony. That gap matters more here than the signing one does. A ChillDKG cannot finish until **every** member has taken part, so one member not finding their request stalls everyone indefinitely -- and the only way to find it was to scroll the transcript to its request line, past whatever else the room has been used for. Three subgroups on the connected devices sat at 1 of 3 host keys for exactly that reason. `CeremoniesAwaitingYouNotice` sits beside the signing one, first in the reversed layout so a room owing both puts the ceremony nearest the composer -- until a ceremony finishes there is no key to sign anything with. **It covers two different kinds of owing, and the second has no gate behind it.** A participant is owed an approval, read through the same `pendingApproval` the ritual screen uses so the two cannot disagree. The member who *opened* it is owed something the protocol has no gate for: a ceremony reaching COMPLETE finishes nothing on its own -- the group has a key and somebody still has to get its state signed and create the room -- and that somebody is whoever opened it. Nothing else in the app would ever say so, which is what the coordinator was missing. `roomAwaitingCreation` is how it knows when to stop: the room a finished ceremony's key derives either exists or does not. Asking that rather than keeping a flag means the notice cannot become permanent furniture in every room that has ever held a ceremony. **It reads every ceremony in the room, not the newest.** A room holds more than one the moment a subgroup's admins are the whole group, and the one that wants you is routinely not the one that happened last -- that is what buried 2.0 and 2.1. The notice opens the ceremony it names, by session id, through the same `onOpenSharedKey` the transcript's own lines use since `0b65d702`. Unlike the signing notice it opens the ceremony rather than a queue: there is no queue of ceremonies, and with several the count is shown and the newest opened. Eight strings in the catalogue in sentence case, each step worded the way its approval screen words it so a member is not asked twice in two vocabularies. `dkgRepository` is threaded to `ChatMessageListViewModel` through the messaging screen, the home pane and the nav host. 397 common tests, 718 jvm tests, `m3Audit` meets every budget with 0 title-case strings and 0 dp literals. Co-Authored-By: Claude Opus 5 --- .../composeResources/values/strings.xml | 8 ++ .../compose/database/dao/DkgSessionDao.kt | 10 ++ .../repository/DatabaseDkgRepository.kt | 14 +++ .../compose/repository/DkgRepository.kt | 25 ++++ .../ui/composable/ChatRoomMessagingScreen.kt | 6 +- .../compose/ui/composable/HomeScreen.kt | 4 + .../ui/composable/navigation/MantraNavHost.kt | 2 + .../composable/widgets/chat/ChatTranscript.kt | 113 ++++++++++++++++++ .../ui/view/model/ChatMessageListViewModel.kt | 93 +++++++++++++- .../ui/composable/ChatPaneLayoutJvmTest.kt | 2 + 10 files changed, 273 insertions(+), 4 deletions(-) diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index a65a7a5d..59f14509 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -390,4 +390,12 @@ Before the subgroup exists, its parent signs for it. That signature is what lets anyone check where this group came from. Ask the parent group to certify this subgroup Create the subgroup + The key ceremony needs you + The subgroup's key ceremony needs you + %1$s key ceremonies need you + Join it by publishing your device's key + Send your contribution to the key + Check the combined result and confirm it + The group has its key — finish setting it up + Open diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/DkgSessionDao.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/DkgSessionDao.kt index 7ebe56e4..fcaffae4 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/DkgSessionDao.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/DkgSessionDao.kt @@ -63,6 +63,16 @@ interface DkgSessionDao { ) fun observeLatestSessionFor(chatRoomId: String, parentChatRoomId: String?): Flow + /** + * Every ceremony this room holds, newest first. + * + * A room holds more than one the moment a subgroup's admins are the whole + * group, so the standing "this needs you" notice in the transcript cannot ask + * about "the room's ceremony" -- it has to look at all of them and say which. + */ + @Query("SELECT * FROM DkgSession WHERE chatRoomId = :chatRoomId ORDER BY createdAt DESC") + fun observeSessionsForChatRoom(chatRoomId: String): Flow> + /** * Every ceremony this device came out of holding a share, newest first. * diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/repository/DatabaseDkgRepository.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/repository/DatabaseDkgRepository.kt index da023200..d436d3ef 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/repository/DatabaseDkgRepository.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/repository/DatabaseDkgRepository.kt @@ -3,6 +3,7 @@ package press.mantra.compose.database.repository import press.mantra.compose.database.MantraDatabase import press.mantra.compose.database.model.DkgParticipantMessage import press.mantra.compose.database.model.DkgSession +import press.mantra.compose.database.model.types.DkgRitualStage import press.mantra.compose.database.model.FrostSigningSession import press.mantra.compose.database.model.GroupKeyState import press.mantra.compose.database.model.GroupSignedEvent @@ -31,6 +32,19 @@ class DatabaseDkgRepository( override fun observeSessionById(sessionId: String): Flow = database.dkgSessionDao().observeSessionById(sessionId) + override fun observeSessionsForChatRoom(chatRoomId: String): Flow> = + database.dkgSessionDao().observeSessionsForChatRoom(chatRoomId) + + override suspend fun roomAwaitingCreation(session: DkgSession): String? { + if (session.stage != DkgRitualStage.COMPLETE) return null + + val roomId = session.thresholdPublicKey + ?.let { runCatching { SharedKeyDerivation.marmotGroupId(it) }.getOrNull() } + ?: return null + + return roomId.takeIf { database.chatRoomDao().findChatRoomById(it) == null } + } + override fun observeLatestSessionForChatRoom( chatRoomId: String, parentChatRoomId: String?, diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/repository/DkgRepository.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/repository/DkgRepository.kt index 01e55111..991436f2 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/repository/DkgRepository.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/repository/DkgRepository.kt @@ -44,6 +44,26 @@ interface DkgRepository { */ fun observeSessionById(sessionId: String): Flow + /** + * Every ceremony a room holds, newest first. + * + * For the transcript's standing notice, which has to say what a room owes + * this member without knowing in advance how many ceremonies are in it. + */ + fun observeSessionsForChatRoom(chatRoomId: String): Flow> + + /** + * The room a finished ceremony's key derives, if this device does not hold it + * yet -- or null once it does, or while there is no key. + * + * What tells the coordinator they still have work. A ceremony reaching + * COMPLETE is not the end of anything on its own: the group has a key and + * somebody still has to get its state signed and make the room. Once that + * room exists there is nothing left to do, and asking whether it exists is + * how this knows without keeping a flag that could be wrong. + */ + suspend fun roomAwaitingCreation(session: DkgSession): String? + suspend fun getLatestSessionForChatRoom(chatRoomId: String): DkgSession? /** @@ -178,6 +198,11 @@ interface DkgRepository { override fun observeSessionById(sessionId: String): Flow = flowOf(null) + override fun observeSessionsForChatRoom(chatRoomId: String): Flow> = + flowOf(emptyList()) + + override suspend fun roomAwaitingCreation(session: DkgSession): String? = null + override suspend fun getLatestSessionForChatRoom(chatRoomId: String): DkgSession? = null override suspend fun proposeRitual( 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 07cba250..657dfab7 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 @@ -43,6 +43,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import press.mantra.compose.database.model.ChatRoom import press.mantra.compose.database.model.intermdiate.LocalChatRoom import press.mantra.compose.repository.ChatRepository +import press.mantra.compose.repository.DkgRepository import press.mantra.compose.repository.FrostSigningRepository import press.mantra.compose.repository.NostrRepository import press.mantra.compose.ui.composable.navigation.routes.ChatRoomDetailRoute @@ -82,6 +83,7 @@ fun ChatRoomMessagingScreen( nostrRepository: NostrRepository, chatRepository: ChatRepository, frostSigningRepository: FrostSigningRepository, + dkgRepository: DkgRepository, onNavigateToRouteAndPopUpInclusive: (Route) -> Unit, onNavigateToRoute: (Route) -> Unit, ) { @@ -119,7 +121,8 @@ fun ChatRoomMessagingScreen( localChatRoom = chatRoomDetailUIState.localChatRoom, nostrRepository = nostrRepository, chatRepository = chatRepository, - frostSigningRepository = frostSigningRepository + frostSigningRepository = frostSigningRepository, + dkgRepository = dkgRepository ) ) @@ -486,6 +489,7 @@ private fun ChatRoomMessagingScreenPreview() { nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY, chatRepository = ChatRepository.NO_OP_CHAT_REPOSITORY, frostSigningRepository = FrostSigningRepository.NO_OP_FROST_SIGNING_REPOSITORY, + dkgRepository = DkgRepository.NO_OP_DKG_REPOSITORY, onNavigateToRouteAndPopUpInclusive = {}, onNavigateToRoute = {} ) diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/HomeScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/HomeScreen.kt index 8c885c84..cd40abc7 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/HomeScreen.kt @@ -65,6 +65,7 @@ import press.mantra.compose.ui.composable.widgets.ErrorState import androidx.compose.material3.SnackbarHost import press.mantra.compose.ui.composable.widgets.LocalSnackbarHostState import androidx.compose.ui.Alignment +import press.mantra.compose.repository.DkgRepository import press.mantra.compose.repository.FrostSigningRepository import press.mantra.compose.ui.composable.navigation.routes.ChatRoomMessagingRoute import press.mantra.compose.ui.composable.widgets.Decorative @@ -90,6 +91,7 @@ fun HomeScreen( // required parameter rather than a nullable one because a home screen that silently // loses its detail pane on a desktop is a worse failure than a compile error. frostSigningRepository: FrostSigningRepository, + dkgRepository: DkgRepository, ) { val sheetState = rememberModalBottomSheetState() val scope = rememberCoroutineScope() @@ -291,6 +293,7 @@ fun HomeScreen( nostrRepository = nostrRepository, chatRepository = chatRepository, frostSigningRepository = frostSigningRepository, + dkgRepository = dkgRepository, // "Replace what is on screen" is a route push on a // phone and a change of selection here. It fires when // a conversation that did not exist yet has just been @@ -381,6 +384,7 @@ It has survived not only five centuries, but also the leap into electronic types nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY, chatRepository = ChatRepository.NO_OP_CHAT_REPOSITORY, frostSigningRepository = FrostSigningRepository.NO_OP_FROST_SIGNING_REPOSITORY, + dkgRepository = DkgRepository.NO_OP_DKG_REPOSITORY, ) } } diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/navigation/MantraNavHost.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/navigation/MantraNavHost.kt index 6ffc270a..2cdc39de 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/navigation/MantraNavHost.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/navigation/MantraNavHost.kt @@ -734,6 +734,7 @@ fun MantraNavHost( nostrRepository = databaseNostrRepository, chatRepository = databaseChatRepository, frostSigningRepository = databaseFrostSigningRepository, + dkgRepository = databaseDkgRepository, ) } composable { @@ -834,6 +835,7 @@ fun MantraNavHost( nostrRepository = databaseNostrRepository, chatRepository = databaseChatRepository, frostSigningRepository = databaseFrostSigningRepository, + dkgRepository = databaseDkgRepository, onNavigateToRouteAndPopUpInclusive = { chatRoomDetailRoute -> navController.navigate( route = chatRoomDetailRoute diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/widgets/chat/ChatTranscript.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/widgets/chat/ChatTranscript.kt index 5fc7cc1d..75db2d21 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/widgets/chat/ChatTranscript.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/widgets/chat/ChatTranscript.kt @@ -17,6 +17,15 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountTree +import mantra.composeapp.generated.resources.open +import mantra.composeapp.generated.resources.the_group_has_its_key_finish_setting_it_up +import mantra.composeapp.generated.resources.check_the_combined_result +import mantra.composeapp.generated.resources.send_your_contribution_to_the_key +import mantra.composeapp.generated.resources.join_the_ceremony_by_publishing_your_key +import mantra.composeapp.generated.resources.the_subgroups_key_ceremony_needs_you +import mantra.composeapp.generated.resources.the_key_ceremony_needs_you +import mantra.composeapp.generated.resources.ceremonies_need_you +import press.mantra.compose.database.model.types.DkgApprovalStep import androidx.compose.material.icons.filled.AccessTime import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.CheckCircle @@ -161,6 +170,7 @@ fun ChatTranscript( // of this function and not of a lazy item that may not // be composed at the time. val awaitingYou = viewModel.proposalsAwaitingYou + val ceremoniesAwaitingYou = viewModel.ceremoniesAwaitingYou LazyColumn( modifier = Modifier.fillMaxWidth().padding(MaterialTheme.spacing.space50), @@ -179,6 +189,23 @@ fun ChatTranscript( } } + // Beside the signing notice and above it in the + // reversed layout, so a room owing both puts the + // ceremony nearest the composer. A ceremony is the + // thing everything else in the room is waiting on: + // until it finishes there is no key to sign anything + // with. + if (ceremoniesAwaitingYou.isNotEmpty()) { + item { + CeremoniesAwaitingYouNotice( + ceremonies = ceremoniesAwaitingYou, + onClick = { dkgSessionId -> + onOpenSharedKey(dkgSessionId) + } + ) + } + } + item { if (viewModel.isReceiverChatMessageRelayListMissing.value) { Row( @@ -526,6 +553,92 @@ fun ChatTranscript( * of what is owed, and the queue is the screen that answers the question it * raises -- including for the one it could not name. */ +/** + * What a ceremony in this room still wants from this member, under the newest + * message. + * + * The counterpart to [ProposalsAwaitingYouNotice], for the other thing a room can + * be owed. It matters more in a NIP-17 room than a signing notice does, because a + * NIP-17 room is where ceremonies happen and a ceremony cannot finish until every + * member has taken part -- so one member not finding their request stalls everyone + * indefinitely, with nothing on any screen saying why. + * + * Unlike the signing notice this opens the ceremony it names rather than a queue. + * There is no queue of ceremonies to open, and naming one is honest: with several, + * the count is shown and the newest is opened, which is the one a member is most + * likely to have just been asked about. + * + * The coordinator's case has no approval gate behind it at all. A ceremony that + * has produced a key still needs its state signed and its room created, and the + * member who opened it is the one who does that -- so `roomAwaitingCreation` + * earns a place here beside the gates, and stops being asked for the moment the + * room exists. + */ +@Composable +private fun CeremoniesAwaitingYouNotice( + ceremonies: List, + onClick: (dkgSessionId: String) -> Unit, +) { + val single = ceremonies.singleOrNull() + + val headline = when { + single == null -> stringResource(Res.string.ceremonies_need_you, ceremonies.size) + single.isSubgroup -> stringResource(Res.string.the_subgroups_key_ceremony_needs_you) + else -> stringResource(Res.string.the_key_ceremony_needs_you) + } + + // What it wants, in the words the approval screens use for the same step, so + // a member is not asked twice in two vocabularies. The gate comes first: an + // approval is owed by this member and cannot be done by anybody else, while + // finishing the setup is work the coordinator can come back to. + val detail = single?.let { + when (it.step) { + DkgApprovalStep.HOST_KEY -> stringResource(Res.string.join_the_ceremony_by_publishing_your_key) + DkgApprovalStep.ROUND_1 -> stringResource(Res.string.send_your_contribution_to_the_key) + DkgApprovalStep.ROUND_2 -> stringResource(Res.string.check_the_combined_result) + null -> stringResource(Res.string.the_group_has_its_key_finish_setting_it_up) + } + } + + Card( + onClick = { onClick(ceremonies.first().sessionId) }, + modifier = Modifier.fillMaxWidth().padding(horizontal = MaterialTheme.spacing.space50), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer + ) + ) { + Row( + modifier = Modifier.fillMaxWidth().padding(MaterialTheme.spacing.space150), + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.space125), + verticalAlignment = Alignment.CenterVertically + ) { + Icon(Icons.Default.Key, contentDescription = Decorative) + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.space25) + ) { + Text(text = headline, style = MaterialTheme.typography.labelMedium) + + if (detail != null) { + Text( + text = detail, + style = MaterialTheme.typography.bodySmall, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + } + + Text( + text = stringResource(Res.string.open), + style = MaterialTheme.typography.labelLarge + ) + } + } +} + @Composable private fun ProposalsAwaitingYouNotice( proposals: List, 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 3b036c8a..7ef1bdb0 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 @@ -23,6 +23,9 @@ import press.mantra.compose.nostr.MemberProfileSync import press.mantra.compose.nostr.Nip17Filters import press.mantra.compose.nostr.Relays import press.mantra.compose.repository.ChatRepository +import press.mantra.compose.database.model.types.DkgRitualStage +import press.mantra.compose.database.model.types.DkgApprovalStep +import press.mantra.compose.repository.DkgRepository import press.mantra.compose.repository.FrostSigningRepository import press.mantra.compose.repository.NostrRepository import press.mantra.compose.text.ProposedEvent @@ -41,7 +44,8 @@ class ChatMessageListViewModel( val localChatRoom: LocalChatRoom, val nostrRepository: NostrRepository, val chatRepository: ChatRepository, - val frostSigningRepository: FrostSigningRepository + val frostSigningRepository: FrostSigningRepository, + val dkgRepository: DkgRepository ): ViewModel() { val logger = Logger.withTag(TAG) var chatMessageListUIState: ChatMessageListUIState by mutableStateOf(initialChatMessageListUIState) @@ -94,6 +98,46 @@ class ChatMessageListViewModel( var proposalsAwaitingYou: List by mutableStateOf(emptyList()) private set + /** + * One ceremony in this room that still wants something from this member. + * + * The counterpart to [AwaitingProposal], for the other thing a room can be + * waiting on somebody for. A NIP-17 room is where ceremonies happen, and + * before this the only way to find one that wanted you was to scroll the + * transcript to its request line -- which for a subgroup means scrolling past + * whatever else the room has been used for. + */ + data class AwaitingCeremony( + val sessionId: String, + + /** What it wants, or null when what it wants is the next step rather than a signature. */ + val step: DkgApprovalStep?, + + /** + * The room this ceremony's key derives, when that room does not exist + * yet and this member is the one who would make it. + * + * A ceremony reaching COMPLETE finishes nothing on its own: the group has + * a key and somebody still has to get its state signed and create the + * room. That somebody is whoever opened it, and nothing else in the app + * would ever tell them so. + */ + val roomAwaitingCreation: String?, + + /** Whether this ceremony is making a subgroup, for what the notice says. */ + val isSubgroup: Boolean, + ) + + /** + * The ceremonies in this room that still want something from this member. + * + * Every one of them, not the room's newest: a room holds more than one the + * moment a subgroup's admins are the whole group, and the one that wants you + * is routinely not the one that happened last. + */ + var ceremoniesAwaitingYou: List by mutableStateOf(emptyList()) + private set + /** * Whether opening [sessionId] by itself would hide other decisions the room * is waiting on this member for. @@ -156,6 +200,7 @@ class ChatMessageListViewModel( scheduleSynchronization() observeChatRoomFeed() observeProposalsAwaitingYou() + observeCeremoniesAwaitingYou() askForGroupHistory() } @@ -192,6 +237,46 @@ class ChatMessageListViewModel( } } + /** + * Watches which of the room's ceremonies are waiting on this member. + * + * Two different kinds of waiting, and both belong here. A participant is owed + * an approval gate -- `pendingApproval` is the same reading the ritual screen + * makes, so the two cannot disagree. The member who *opened* it is owed + * something the protocol has no gate for: a finished ceremony still needs its + * key state signed and its room created, and nothing else in the app would + * ever say so. + * + * A ceremony whose room exists wants nothing from anybody, which is what + * `roomAwaitingCreation` is asking. That keeps the notice from becoming + * permanent furniture in every room that has ever held a ceremony. + */ + private fun observeCeremoniesAwaitingYou() { + viewModelScope.launch(Dispatchers.IO) { + dkgRepository + .observeSessionsForChatRoom(localChatRoom.chatRoom.id) + .collect { sessions -> + ceremoniesAwaitingYou = sessions + .filter { it.stage != DkgRitualStage.FAILED } + .mapNotNull { session -> + val step = dkgRepository.pendingApproval(session) + val roomAwaitingCreation = session + .takeIf { it.coordinatorPublicKey == it.userPublicKey } + ?.let { dkgRepository.roomAwaitingCreation(it) } + + if (step == null && roomAwaitingCreation == null) return@mapNotNull null + + AwaitingCeremony( + sessionId = session.id, + step = step, + roomAwaitingCreation = roomAwaitingCreation, + isSubgroup = session.parentChatRoomId != null, + ) + } + } + } + } + /** * Ask the group for its signed record, if this device holds none of it. * @@ -340,7 +425,8 @@ class ChatMessageListViewModel( localChatRoom: LocalChatRoom, nostrRepository: NostrRepository, chatRepository: ChatRepository, - frostSigningRepository: FrostSigningRepository + frostSigningRepository: FrostSigningRepository, + dkgRepository: DkgRepository ): ViewModelProvider.Factory = viewModelFactory { initializer { ChatMessageListViewModel( @@ -348,7 +434,8 @@ class ChatMessageListViewModel( localChatRoom = localChatRoom, nostrRepository = nostrRepository, chatRepository = chatRepository, - frostSigningRepository = frostSigningRepository + frostSigningRepository = frostSigningRepository, + dkgRepository = dkgRepository ) } } diff --git a/composeApp/src/jvmTest/kotlin/press/mantra/compose/ui/composable/ChatPaneLayoutJvmTest.kt b/composeApp/src/jvmTest/kotlin/press/mantra/compose/ui/composable/ChatPaneLayoutJvmTest.kt index fa4a4c38..b4443882 100644 --- a/composeApp/src/jvmTest/kotlin/press/mantra/compose/ui/composable/ChatPaneLayoutJvmTest.kt +++ b/composeApp/src/jvmTest/kotlin/press/mantra/compose/ui/composable/ChatPaneLayoutJvmTest.kt @@ -14,6 +14,7 @@ import press.mantra.compose.database.model.Profile import press.mantra.compose.database.model.intermdiate.LocalChatRoom import press.mantra.compose.database.model.intermdiate.LocalProfileWithFollowing import press.mantra.compose.repository.ChatRepository +import press.mantra.compose.repository.DkgRepository import press.mantra.compose.repository.FrostSigningRepository import press.mantra.compose.repository.NostrRepository import press.mantra.compose.ui.composable.widgets.ProvideSnackbarHost @@ -96,6 +97,7 @@ class ChatPaneLayoutJvmTest { nostrRepository = FixedProfile, chatRepository = NoRooms, frostSigningRepository = FrostSigningRepository.NO_OP_FROST_SIGNING_REPOSITORY, + dkgRepository = DkgRepository.NO_OP_DKG_REPOSITORY, ) } }