feat(marmot): show member pictures and names when group has no name

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<HexKey>
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).
This commit is contained in:
Claude
2026-04-23 21:47:36 +00:00
parent 647d6f9841
commit bed4554364
4 changed files with 93 additions and 19 deletions
@@ -231,19 +231,28 @@ fun NonClickableUserPictures(
room: ChatroomKey,
size: Dp,
accountViewModel: AccountViewModel,
) {
NonClickableUserPictures(room.users.toList(), size, accountViewModel)
}
@Composable
fun NonClickableUserPictures(
userHexList: List<HexKey>,
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 {
@@ -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 = {
@@ -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 ?: "",
@@ -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<HexKey>,
accountViewModel: AccountViewModel,
fontWeight: FontWeight = FontWeight.Bold,
) {
if (userList.size == 1) {
// Regular Design
Row {