From 4899dcbca7fe9f0974243b4bcab3c050b256e85b Mon Sep 17 00:00:00 2001 From: Kgothatso Ngako Date: Sun, 13 Sep 2026 14:44:57 +0200 Subject: [PATCH] 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 Pulled-From: curated/curated@272b6316337dd2137b767de00ea9c958b781f371 --- .../ui/composable/ChatRoomDetailScreen.kt | 13 +- .../ui/composable/ChatRoomMemberRowJvmTest.kt | 127 ++++++++++++++++++ .../ui/composable/GroupSignedWorkFixtures.kt | 31 +++++ 3 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 composeApp/src/jvmTest/kotlin/press/mantra/compose/ui/composable/ChatRoomMemberRowJvmTest.kt create mode 100644 composeApp/src/jvmTest/kotlin/press/mantra/compose/ui/composable/GroupSignedWorkFixtures.kt diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ChatRoomDetailScreen.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ChatRoomDetailScreen.kt index 37a3ae27..0689fb34 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ChatRoomDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/ui/composable/ChatRoomDetailScreen.kt @@ -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 ) ) } diff --git a/composeApp/src/jvmTest/kotlin/press/mantra/compose/ui/composable/ChatRoomMemberRowJvmTest.kt b/composeApp/src/jvmTest/kotlin/press/mantra/compose/ui/composable/ChatRoomMemberRowJvmTest.kt new file mode 100644 index 00000000..26fdfdad --- /dev/null +++ b/composeApp/src/jvmTest/kotlin/press/mantra/compose/ui/composable/ChatRoomMemberRowJvmTest.kt @@ -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() + } +} diff --git a/composeApp/src/jvmTest/kotlin/press/mantra/compose/ui/composable/GroupSignedWorkFixtures.kt b/composeApp/src/jvmTest/kotlin/press/mantra/compose/ui/composable/GroupSignedWorkFixtures.kt new file mode 100644 index 00000000..c4b22221 --- /dev/null +++ b/composeApp/src/jvmTest/kotlin/press/mantra/compose/ui/composable/GroupSignedWorkFixtures.kt @@ -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 + ) + ) +}