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