From bed4554364028af9ff3ade91af552c799253f9e0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Apr 2026 21:47:36 +0000 Subject: [PATCH] feat(marmot): show member pictures and names when group has no name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Marmot groups without a configured display name now fall back to the NIP-17 strategy: a stacked avatar of the members and a comma-separated list of their first names. Applied to both the group list row and the chat top app bar. NonClickableUserPictures and DisplayUserSetAsSubject get List overloads so the renderers can be reused without dragging Marmot through ChatroomKey (which is also a NIP-17 dedup key — different Marmot groups can share the same member set). --- .../amethyst/ui/note/UserProfilePicture.kt | 19 +++++--- .../marmotGroup/MarmotGroupChatScreen.kt | 40 ++++++++++++++--- .../marmotGroup/MarmotGroupListScreen.kt | 45 +++++++++++++++---- .../privateDM/header/RoomNameOnlyDisplay.kt | 8 ++++ 4 files changed, 93 insertions(+), 19 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index 79a211901..2fe382482 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -231,19 +231,28 @@ fun NonClickableUserPictures( room: ChatroomKey, size: Dp, accountViewModel: AccountViewModel, +) { + NonClickableUserPictures(room.users.toList(), size, accountViewModel) +} + +@Composable +fun NonClickableUserPictures( + userHexList: List, + size: Dp, + accountViewModel: AccountViewModel, ) { Box(Modifier.size(size), contentAlignment = Alignment.TopEnd) { - when (room.users.size) { + when (userHexList.size) { 0 -> {} 1 -> { - LoadUser(baseUserHex = room.users.first(), accountViewModel) { + LoadUser(baseUserHex = userHexList.first(), accountViewModel) { it?.let { BaseUserPicture(it, size, accountViewModel, outerModifier = Modifier) } } } 2 -> { - val userList = room.users.toList() + val userList = userHexList LoadUser(baseUserHex = userList[0], accountViewModel) { it?.let { @@ -268,7 +277,7 @@ fun NonClickableUserPictures( } 3 -> { - val userList = room.users.toList() + val userList = userHexList LoadUser(baseUserHex = userList[0], accountViewModel) { it?.let { @@ -303,7 +312,7 @@ fun NonClickableUserPictures( } else -> { - val userList = room.users.toList() + val userList = userHexList LoadUser(baseUserHex = userList[0], accountViewModel) { it?.let { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatScreen.kt index 334cd7a35..2cbd57c40 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatScreen.kt @@ -21,7 +21,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack @@ -35,12 +37,16 @@ import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.DisplayUserSetAsSubject import com.vitorpamplona.quartz.nip01Core.core.HexKey @OptIn(ExperimentalMaterial3Api::class) @@ -56,6 +62,8 @@ fun MarmotGroupChatScreen( } val displayName by chatroom.displayName.collectAsStateWithLifecycle() val memberCount by chatroom.memberCount.collectAsStateWithLifecycle() + val members by chatroom.members.collectAsStateWithLifecycle() + val memberPubkeys = remember(members) { members.map { it.pubkey } } DisappearingScaffold( isInvertedLayout = true, @@ -70,19 +78,39 @@ fun MarmotGroupChatScreen( } }, title = { - Column( + Row( modifier = Modifier.clickable { nav.nav(Route.MarmotGroupInfo(nostrGroupId)) }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - Text(displayName ?: "Marmot Group") - if (memberCount > 0) { - Text( - text = "$memberCount members", - style = MaterialTheme.typography.bodySmall, + if (memberPubkeys.isNotEmpty()) { + NonClickableUserPictures( + userHexList = memberPubkeys, + size = 36.dp, + accountViewModel = accountViewModel, ) } + Column { + if (!displayName.isNullOrBlank()) { + Text(displayName!!) + } else if (memberPubkeys.isNotEmpty()) { + DisplayUserSetAsSubject( + userList = memberPubkeys, + accountViewModel = accountViewModel, + ) + } else { + Text("Marmot Group") + } + if (memberCount > 0) { + Text( + text = "$memberCount members", + style = MaterialTheme.typography.bodySmall, + ) + } + } } }, actions = { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt index cbcd0d9ba..433f3225b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt @@ -65,7 +65,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.model.marmotGroups.MarmotGroupChatroom import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.DisplayUserSetAsSubject import com.vitorpamplona.quartz.nip01Core.core.HexKey @OptIn(ExperimentalMaterial3Api::class) @@ -179,6 +181,7 @@ fun MarmotGroupListScreen( MarmotGroupListItem( groupId = groupId, chatroom = chatroom, + accountViewModel = accountViewModel, onClick = { nav.nav(Route.MarmotGroupChat(groupId)) }, @@ -213,10 +216,13 @@ private fun loadGroupList( fun MarmotGroupListItem( groupId: HexKey, chatroom: MarmotGroupChatroom, + accountViewModel: AccountViewModel, onClick: () -> Unit, ) { val displayName by chatroom.displayName.collectAsStateWithLifecycle() val unread by chatroom.unreadCount.collectAsStateWithLifecycle() + val members by chatroom.members.collectAsStateWithLifecycle() + val memberPubkeys = remember(members) { members.map { it.pubkey } } val newestMessage = chatroom.newestMessage Row( @@ -225,17 +231,40 @@ fun MarmotGroupListItem( .fillMaxWidth() .clickable(onClick = onClick) .padding(horizontal = 16.dp, vertical = 12.dp), - horizontalArrangement = Arrangement.SpaceBetween, + horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically, ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = displayName ?: "Group ${groupId.take(8)}...", - style = MaterialTheme.typography.titleSmall, - fontWeight = if (unread > 0) FontWeight.Bold else FontWeight.Normal, - maxLines = 1, - overflow = TextOverflow.Ellipsis, + if (memberPubkeys.isNotEmpty()) { + NonClickableUserPictures( + userHexList = memberPubkeys, + size = 55.dp, + accountViewModel = accountViewModel, ) + } + Column(modifier = Modifier.weight(1f)) { + if (!displayName.isNullOrBlank()) { + Text( + text = displayName!!, + style = MaterialTheme.typography.titleSmall, + fontWeight = if (unread > 0) FontWeight.Bold else FontWeight.Normal, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } else if (memberPubkeys.isNotEmpty()) { + DisplayUserSetAsSubject( + userList = memberPubkeys, + accountViewModel = accountViewModel, + fontWeight = if (unread > 0) FontWeight.Bold else FontWeight.Normal, + ) + } else { + Text( + text = "Group ${groupId.take(8)}...", + style = MaterialTheme.typography.titleSmall, + fontWeight = if (unread > 0) FontWeight.Bold else FontWeight.Normal, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } if (newestMessage != null) { Text( text = newestMessage.event?.content ?: "", diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt index 3985297e9..c8e7d4b6f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt @@ -69,7 +69,15 @@ fun DisplayUserSetAsSubject( fontWeight: FontWeight = FontWeight.Bold, ) { val userList = remember(room) { room.users.toList() } + DisplayUserSetAsSubject(userList, accountViewModel, fontWeight) +} +@Composable +fun DisplayUserSetAsSubject( + userList: List, + accountViewModel: AccountViewModel, + fontWeight: FontWeight = FontWeight.Bold, +) { if (userList.size == 1) { // Regular Design Row {