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 <noreply@anthropic.com>
This commit is contained in:
@@ -390,4 +390,12 @@
|
||||
<string name="before_the_subgroup_exists_its_parent_signs">Before the subgroup exists, its parent signs for it. That signature is what lets anyone check where this group came from.</string>
|
||||
<string name="ask_the_parent_group_to_certify">Ask the parent group to certify this subgroup</string>
|
||||
<string name="create_the_subgroup">Create the subgroup</string>
|
||||
<string name="the_key_ceremony_needs_you">The key ceremony needs you</string>
|
||||
<string name="the_subgroups_key_ceremony_needs_you">The subgroup's key ceremony needs you</string>
|
||||
<string name="ceremonies_need_you">%1$s key ceremonies need you</string>
|
||||
<string name="join_the_ceremony_by_publishing_your_key">Join it by publishing your device's key</string>
|
||||
<string name="send_your_contribution_to_the_key">Send your contribution to the key</string>
|
||||
<string name="check_the_combined_result">Check the combined result and confirm it</string>
|
||||
<string name="the_group_has_its_key_finish_setting_it_up">The group has its key — finish setting it up</string>
|
||||
<string name="open">Open</string>
|
||||
</resources>
|
||||
|
||||
@@ -63,6 +63,16 @@ interface DkgSessionDao {
|
||||
)
|
||||
fun observeLatestSessionFor(chatRoomId: String, parentChatRoomId: String?): Flow<DkgSession?>
|
||||
|
||||
/**
|
||||
* 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<List<DkgSession>>
|
||||
|
||||
/**
|
||||
* Every ceremony this device came out of holding a share, newest first.
|
||||
*
|
||||
|
||||
@@ -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<DkgSession?> =
|
||||
database.dkgSessionDao().observeSessionById(sessionId)
|
||||
|
||||
override fun observeSessionsForChatRoom(chatRoomId: String): Flow<List<DkgSession>> =
|
||||
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?,
|
||||
|
||||
@@ -44,6 +44,26 @@ interface DkgRepository {
|
||||
*/
|
||||
fun observeSessionById(sessionId: String): Flow<DkgSession?>
|
||||
|
||||
/**
|
||||
* 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<List<DkgSession>>
|
||||
|
||||
/**
|
||||
* 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<DkgSession?> = flowOf(null)
|
||||
|
||||
override fun observeSessionsForChatRoom(chatRoomId: String): Flow<List<DkgSession>> =
|
||||
flowOf(emptyList())
|
||||
|
||||
override suspend fun roomAwaitingCreation(session: DkgSession): String? = null
|
||||
|
||||
override suspend fun getLatestSessionForChatRoom(chatRoomId: String): DkgSession? = null
|
||||
|
||||
override suspend fun proposeRitual(
|
||||
|
||||
@@ -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 = {}
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -734,6 +734,7 @@ fun MantraNavHost(
|
||||
nostrRepository = databaseNostrRepository,
|
||||
chatRepository = databaseChatRepository,
|
||||
frostSigningRepository = databaseFrostSigningRepository,
|
||||
dkgRepository = databaseDkgRepository,
|
||||
)
|
||||
}
|
||||
composable<BlankRoute> {
|
||||
@@ -834,6 +835,7 @@ fun MantraNavHost(
|
||||
nostrRepository = databaseNostrRepository,
|
||||
chatRepository = databaseChatRepository,
|
||||
frostSigningRepository = databaseFrostSigningRepository,
|
||||
dkgRepository = databaseDkgRepository,
|
||||
onNavigateToRouteAndPopUpInclusive = { chatRoomDetailRoute ->
|
||||
navController.navigate(
|
||||
route = chatRoomDetailRoute
|
||||
|
||||
@@ -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<ChatMessageListViewModel.AwaitingCeremony>,
|
||||
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<ChatMessageListViewModel.AwaitingProposal>,
|
||||
|
||||
@@ -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<AwaitingProposal> 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<AwaitingCeremony> 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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user