feat(ui): a member on the room's screen opens the profile preview

The member rows on the chat room detail were the last "Profile detail" stub
in the app: each pushed ImplementationPendingRoute. They now push
ProfilePreviewRoute by the member's public key, which the nav host already
serves, so a member arrives with the cached profile at once and the relays
refreshing it, and the bar's action opens the chat that exists with them or
starts one.

The preview rather than the profile detail, because the detail is keyed by a
kind 0's event id and a member whose profile has not arrived yet -- the
"LOADING..." row MemberProfileSync exists for -- has none. The public key is
the one thing every row has. A test pins both members opening the same
screen, the named one and the one still named by its key.

Replayed onto Mantra by docs/curated-to-mantra.md: ChatRoomDetailScreen.kt: the ImplementationPendingRoute import this commit removes is kept -- upstream's file had no other use for it once 808a3459 dropped the library, dialects and projects sections, and Mantra's projects section, which it kept, still routes "Add new project" through it. A semantic conflict rather than a textual one: the pick applied cleanly and the compiler found it; GroupSignedWorkFixtures.kt: the test this commit adds composes the screen against the fork's fixture of that name, which went with lines B, D and H -- it is recreated here reduced to the room alone, under the same names, so the test lands byte for byte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pulled-From: curated/curated@272b631633
This commit is contained in:
Kgothatso Ngako
2026-09-13 14:44:57 +02:00
parent e66c174b4f
commit 4899dcbca7
3 changed files with 169 additions and 2 deletions

View File

@@ -67,6 +67,7 @@ import press.mantra.compose.repository.ChatRepository
import press.mantra.compose.repository.MantraRepository
import press.mantra.compose.repository.NostrRepository
import press.mantra.compose.ui.composable.navigation.routes.ImplementationPendingRoute
import press.mantra.compose.ui.composable.navigation.routes.ProfilePreviewRoute
import press.mantra.compose.ui.composable.navigation.routes.Route
import press.mantra.compose.ui.composable.navigation.routes.DkgRitualRoute
import press.mantra.compose.ui.composable.navigation.routes.ProposalListRoute
@@ -693,11 +694,19 @@ fun ChatRoomDetailScreen(
key = { localParticipant -> localParticipant.participant.participantPublicKey}
) { localParticipant ->
// The preview, not the profile detail: the detail is
// keyed by a kind 0's event id, and a member whose
// profile has not arrived yet -- the "LOADING..." row
// the sync exists for -- has none. The preview is
// keyed by the public key, which is the one thing
// every row here has, and it shows the cached
// profile at once while the relays refresh it.
Card(
onClick = {
onNavigateToRoute.invoke(
ImplementationPendingRoute(
"Profile detail"
ProfilePreviewRoute(
activeUserPublicKey = activeUserPublicKey,
profilePublicKey = localParticipant.participant.participantPublicKey
)
)
}

View File

@@ -0,0 +1,127 @@
package press.mantra.compose.ui.composable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.ComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.runDesktopComposeUiTest
import press.mantra.compose.database.model.Participant
import press.mantra.compose.database.model.Profile
import press.mantra.compose.database.model.intermdiate.LocalParticipant
import press.mantra.compose.repository.ChatRepository
import press.mantra.compose.repository.MantraRepository
import press.mantra.compose.repository.NostrRepository
import press.mantra.compose.ui.composable.GroupSignedWorkFixtures.activeUserPublicKey
import press.mantra.compose.ui.composable.GroupSignedWorkFixtures.chatRoomId
import press.mantra.compose.ui.composable.navigation.routes.ProfilePreviewRoute
import press.mantra.compose.ui.composable.navigation.routes.Route
import press.mantra.compose.ui.composable.widgets.ProvideSnackbarHost
import press.mantra.compose.ui.theme.MantraTheme
import press.mantra.compose.ui.view.state.ChatRoomDetailUIState
import kotlin.test.Test
import kotlin.test.assertEquals
/**
* The member rows on the room's screen, and what one opens.
*
* A row is a promise about the screen behind it, and this one was a stub for long
* enough that the promise is worth pinning: a member opens the profile preview,
* keyed by the member's public key, with the active identity beside it so the
* preview can say whether a chat with them already exists.
*
* Two members, because the screen names them differently and the row has to open
* the same screen either way. One has a profile and is named by it. The other has
* none yet -- the "LOADING..." row `MemberProfileSync` exists for -- and is named
* by the key; that member is the reason the row opens the preview rather than the
* profile detail, which is keyed by a kind 0's event id and has nothing to open for
* a member whose kind 0 has not arrived.
*/
@OptIn(ExperimentalTestApi::class)
class ChatRoomMemberRowJvmTest {
private val named = "b".repeat(64)
private val stillLoading = "c".repeat(64)
@Test
fun `a member with a profile opens the preview by their key`() {
var opened: Route? = null
render(onNavigateToRoute = { opened = it }) {
onNodeWithText("Nomvula").performClick()
}
assertEquals(ProfilePreviewRoute(activeUserPublicKey, named), opened)
}
@Test
fun `a member whose profile has not arrived opens the preview by their key too`() {
var opened: Route? = null
render(onNavigateToRoute = { opened = it }) {
onNodeWithText(stillLoading).performClick()
}
assertEquals(ProfilePreviewRoute(activeUserPublicKey, stillLoading), opened)
}
private fun member(publicKey: String, profile: Profile?) = LocalParticipant(
participant = Participant(
participantPublicKey = publicKey,
chatRoomId = chatRoomId,
relayHint = null,
),
profile = profile,
)
private fun state() = ChatRoomDetailUIState.Loaded(
localChatRoom = GroupSignedWorkFixtures.localChatRoom().copy(
localParticipants = listOf(
member(
named,
Profile(
publicKey = named,
displayName = "Nomvula",
nostrEventId = "e".repeat(64),
),
),
member(stillLoading, profile = null),
),
),
)
/**
* The screen at a phone's width and a window tall enough to compose the whole
* of it, as `GroupSignedWorkRowsJvmTest` renders it: the members are near the
* bottom, and a `LazyColumn` does not lay out what it would not show.
*/
private fun render(
onNavigateToRoute: (Route) -> Unit,
assertions: ComposeUiTest.() -> Unit,
) = runDesktopComposeUiTest(width = 400, height = 4000) {
setContent {
MantraTheme {
ProvideSnackbarHost {
Box(modifier = Modifier.fillMaxSize()) {
ChatRoomDetailScreen(
activeUserPublicKey = activeUserPublicKey,
chatRoomId = chatRoomId,
relayHint = null,
initialChatRoomDetailUIState = state(),
nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY,
chatRepository = ChatRepository.NO_OP_CHAT_REPOSITORY,
mantraRepository = MantraRepository.NO_OP_MANTRA_REPOSITORY,
onNavigateToRoute = onNavigateToRoute,
onPopBackToRoute = {},
onNavigateBack = {},
)
}
}
}
}
assertions()
}
}

View File

@@ -0,0 +1,31 @@
package press.mantra.compose.ui.composable
import press.mantra.compose.database.model.ChatRoom
import press.mantra.compose.database.model.intermdiate.LocalChatRoom
/**
* A group room to compose the group's screen against.
*
* The fork's fixture of this name also carries the signed work the group's screen
* showed there -- a profile, relay lists, posts, curated schemas and entries -- which
* Mantra took out with lines B, D and H (docs/curated-to-mantra.md). What is left is
* the room itself, under the same names, so that a test written upstream against the
* room alone compiles here unchanged; `ChatRoomMemberRowJvmTest` is the first.
*/
internal object GroupSignedWorkFixtures {
val chatRoomId = "d4c3b2a1".repeat(8)
val activeUserPublicKey = "a".repeat(64)
fun localChatRoom(mlsGroupState: String? = "state") = LocalChatRoom(
chatRoom = ChatRoom(
id = chatRoomId,
userPublicKey = activeUserPublicKey,
subject = "Translation room",
description = "A group translating hard books.",
initialGiftWrapPayloadId = "sdfaer",
mlsGroupState = mlsGroupState
)
)
}