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,
)
}
}