feat: say who opened the ceremony on the shared key screen

The screen showed the quorum, the ladder and now the roster, but never named the
member whose ceremony it was. Any member can open one and it settles the group's
signing quorum for good, so who opened this one belongs on the screen that
describes it — the same reason the chat notice names them.

  Alice started this ceremony.
  2 of 3 members will be needed to sign with this key.

It sits above the failure branch so it holds in every state. A ceremony that was
abandoned or has already produced a key is still worth attributing: a member
arriving at a finished ceremony they do not remember agreeing to should be able
to see whose it was, and DkgSession.coordinatorPublicKey is kept for the life of
the row either way.

## One naming rule, in one place

HexKey.memberName() resolves a member's display name from the profiles joined
onto the room, falling back to a shortened key. The roster switched to it, so the
opener line and the rows below it cannot disagree about what to call somebody,
and the chat notice's copy of the fallback went with it.

This replaces a second private SHORTENED_PUBLIC_KEY_LENGTH I had added to
ChatMessageListViewModel. A third still lives in SelectChatRoomTypeViewModel at a
different value (12) and is deliberately untouched: that is a different choice
about a different surface, not a duplicate of this one, and folding them together
is a call about that screen rather than about this feature.

## Still reads a raw key on one surface

The abandoned card shows DkgSession.failureReason verbatim, which for a ceremony
ended by another member begins "Abandoned by 1a2b3c4d:" -- a truncated key where
the chat line now shows a name. Naming them there needs the culprit stored beside
the reason rather than inside it, which is a column on DkgSession and a schema
version, so it is left as it is rather than parsed back out of the string.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kgothatso Ngako
2026-09-05 00:52:35 +02:00
parent 3fad331969
commit 3f4f05162d
3 changed files with 50 additions and 9 deletions

View File

@@ -0,0 +1,30 @@
package press.mantra.compose.extensions
import press.mantra.compose.database.model.intermdiate.LocalParticipant
import com.vitorpamplona.quartz.nip01Core.core.HexKey
/** Enough of a public key to tell two members apart when neither has a profile yet. */
private const val SHORTENED_PUBLIC_KEY_LENGTH = 8
/** A public key cut to something readable, for where no profile has arrived yet. */
fun HexKey.shortened(): String = take(SHORTENED_PUBLIC_KEY_LENGTH)
/**
* What to call the member behind this public key on screen.
*
* Resolved from the profiles joined onto the room rather than stored anywhere, so
* it follows a rename and is never stuck on the "LOADING..." placeholder a member
* is given the moment they are first seen. Falls back to a short key, which is at
* least stable and distinguishing, when there is no profile at all yet.
*/
fun HexKey.memberName(
members: List<LocalParticipant>,
activeUserPublicKey: HexKey
): String = if (this == activeUserPublicKey) {
"You"
} else {
members.firstOrNull { it.participant.participantPublicKey == this }
?.profile
?.humanReadableNameOrPubkey()
?: shortened()
}

View File

@@ -37,8 +37,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -47,6 +50,7 @@ import press.mantra.compose.database.model.DkgSession
import press.mantra.compose.database.model.intermdiate.LocalChatRoom
import press.mantra.compose.database.model.intermdiate.LocalParticipant
import press.mantra.compose.database.model.types.DkgRitualStage
import press.mantra.compose.extensions.memberName
import press.mantra.compose.managers.ChillDkgRitualManager
import press.mantra.compose.repository.ChatRepository
import press.mantra.compose.repository.DkgRepository
@@ -237,6 +241,19 @@ private fun RitualProgress(
val round1Count = round1Participants.size
val round2Count = round2Participants.size
// Above the failure branch so it holds in every state. Any member can open a
// ceremony and it settles the group's signing quorum for good, so who opened
// this one is worth saying whether it is running, finished or abandoned.
Text(
text = buildAnnotatedString {
withStyle(SpanStyle(color = ProfileColor.fromPublicKey(session.coordinatorPublicKey))) {
append(session.coordinatorPublicKey.memberName(members, activeUserPublicKey))
}
append(" started this ceremony.")
},
style = MaterialTheme.typography.bodyMedium
)
if (stage == DkgRitualStage.FAILED) {
Card(
modifier = Modifier.fillMaxWidth(),
@@ -394,11 +411,7 @@ private fun RitualRoster(
Text(
modifier = Modifier.weight(1f),
text = if (publicKey == activeUserPublicKey) {
"You"
} else {
member.profile?.humanReadableNameOrPubkey() ?: publicKey.take(8)
},
text = publicKey.memberName(members, activeUserPublicKey),
maxLines = 1,
overflow = TextOverflow.MiddleEllipsis,
color = ProfileColor.fromPublicKey(publicKey),

View File

@@ -56,6 +56,7 @@ import press.mantra.compose.database.model.NegentropySynchronizeRequest
import press.mantra.compose.database.model.intermdiate.LocalChatMessage
import press.mantra.compose.database.model.intermdiate.LocalChatRoom
import press.mantra.compose.database.model.types.SynchronizationFilter
import press.mantra.compose.extensions.shortened
import press.mantra.compose.extensions.toFormattedTimeAndDateString
import press.mantra.compose.nostr.Relays
import press.mantra.compose.repository.ChatRepository
@@ -530,7 +531,7 @@ private fun RitualNotice(
"You"
} else {
localChatMessage.profile?.humanReadableNameOrPubkey()
?: chatMessage.senderPublicKey.take(SHORTENED_PUBLIC_KEY_LENGTH)
?: chatMessage.senderPublicKey.shortened()
}
)
}
@@ -557,6 +558,3 @@ private fun RitualNotice(
)
}
}
/** Enough of a public key to tell two members apart when neither has a profile yet. */
private const val SHORTENED_PUBLIC_KEY_LENGTH = 8