feat: add Marmot group info screen, member list, and leave group
Based on MIP spec review, add missing UI screens: MarmotManager additions (commons): - memberPubkeys(): extract Nostr pubkeys from MLS ratchet tree leaf nodes via Credential.Basic identity - memberCount(): get group member count - groupEpoch(): expose current MLS epoch - GroupMemberInfo data class for leaf index + pubkey pairs Account & AccountViewModel: - leaveMarmotGroup(): publish SelfRemove proposal and clean up - marmotGroupMembers(): expose member list to UI MarmotGroupInfoScreen (new): - Group metadata header (name, ID, epoch, member count) - Member list with leaf indices, clickable to view profiles - "Leave Group" with confirmation dialog - "Add Member" access from group info Navigation: - Route.MarmotGroupInfo added and wired into nav graph - Chat screen title now clickable to navigate to group info https://claude.ai/code/session_0194SxKfAU61PY92eqP4cCXM
This commit is contained in:
@@ -1804,6 +1804,21 @@ class Account(
|
|||||||
manager.createGroup(nostrGroupId)
|
manager.createGroup(nostrGroupId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leave a Marmot MLS group.
|
||||||
|
* Publishes the SelfRemove proposal and removes local state.
|
||||||
|
*/
|
||||||
|
suspend fun leaveMarmotGroup(
|
||||||
|
nostrGroupId: HexKey,
|
||||||
|
groupRelays: Set<NormalizedRelayUrl>,
|
||||||
|
) {
|
||||||
|
val manager = marmotManager ?: return
|
||||||
|
if (!isWriteable()) return
|
||||||
|
|
||||||
|
val outbound = manager.leaveGroup(nostrGroupId)
|
||||||
|
client.publish(outbound.signedEvent, groupRelays)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun createStatus(newStatus: String) = sendMyPublicAndPrivateOutbox(UserStatusAction.create(newStatus, signer))
|
suspend fun createStatus(newStatus: String) = sendMyPublicAndPrivateOutbox(UserStatusAction.create(newStatus, signer))
|
||||||
|
|
||||||
suspend fun publishCallSignaling(wrap: EphemeralGiftWrapEvent) {
|
suspend fun publishCallSignaling(wrap: EphemeralGiftWrapEvent) {
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.membershipMa
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.membershipManagement.PostBookmarkListManagementScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.membershipManagement.PostBookmarkListManagementScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.old.OldBookmarkListScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.old.OldBookmarkListScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.MarmotGroupChatScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.MarmotGroupChatScreen
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.MarmotGroupInfoScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.MarmotGroupListScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.MarmotGroupListScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomByAuthorScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomByAuthorScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomScreen
|
||||||
@@ -286,6 +287,7 @@ fun BuildNavigation(
|
|||||||
|
|
||||||
composableFromEnd<Route.MarmotGroupList> { MarmotGroupListScreen(accountViewModel, nav) }
|
composableFromEnd<Route.MarmotGroupList> { MarmotGroupListScreen(accountViewModel, nav) }
|
||||||
composableFromEndArgs<Route.MarmotGroupChat> { MarmotGroupChatScreen(it.nostrGroupId, accountViewModel, nav) }
|
composableFromEndArgs<Route.MarmotGroupChat> { MarmotGroupChatScreen(it.nostrGroupId, accountViewModel, nav) }
|
||||||
|
composableFromEndArgs<Route.MarmotGroupInfo> { MarmotGroupInfoScreen(it.nostrGroupId, accountViewModel, nav) }
|
||||||
|
|
||||||
composableFromEndArgs<Route.PublicChatChannel> {
|
composableFromEndArgs<Route.PublicChatChannel> {
|
||||||
PublicChatChannelScreen(it.id, it.draftId, it.replyTo, accountViewModel, nav)
|
PublicChatChannelScreen(it.id, it.draftId, it.replyTo, accountViewModel, nav)
|
||||||
|
|||||||
@@ -269,6 +269,10 @@ sealed class Route {
|
|||||||
val nostrGroupId: String,
|
val nostrGroupId: String,
|
||||||
) : Route()
|
) : Route()
|
||||||
|
|
||||||
|
@Serializable data class MarmotGroupInfo(
|
||||||
|
val nostrGroupId: String,
|
||||||
|
) : Route()
|
||||||
|
|
||||||
@Serializable data class NewGroupDM(
|
@Serializable data class NewGroupDM(
|
||||||
val message: String? = null,
|
val message: String? = null,
|
||||||
val attachment: String? = null,
|
val attachment: String? = null,
|
||||||
|
|||||||
+7
@@ -1464,6 +1464,13 @@ class AccountViewModel(
|
|||||||
account.publishMarmotKeyPackage()
|
account.publishMarmotKeyPackage()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun leaveMarmotGroup(nostrGroupId: String) {
|
||||||
|
val relays = account.outboxRelays.flow.value
|
||||||
|
account.leaveMarmotGroup(nostrGroupId, relays)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun marmotGroupMembers(nostrGroupId: String): List<com.vitorpamplona.amethyst.commons.marmot.GroupMemberInfo> = account.marmotManager?.memberPubkeys(nostrGroupId) ?: emptyList()
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
Log.d("AccountViewModel", "onCleared")
|
Log.d("AccountViewModel", "onCleared")
|
||||||
callController?.cleanup()
|
callController?.cleanup()
|
||||||
|
|||||||
+8
-1
@@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -41,6 +42,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
|
||||||
@@ -72,7 +74,12 @@ fun MarmotGroupChatScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
title = {
|
title = {
|
||||||
Column {
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier.clickable {
|
||||||
|
nav.nav(Route.MarmotGroupInfo(nostrGroupId))
|
||||||
|
},
|
||||||
|
) {
|
||||||
Text(displayName ?: "Marmot Group")
|
Text(displayName ?: "Marmot Group")
|
||||||
if (memberCount > 0) {
|
if (memberCount > 0) {
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
+297
@@ -0,0 +1,297 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
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.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ExitToApp
|
||||||
|
import androidx.compose.material.icons.filled.GroupAdd
|
||||||
|
import androidx.compose.material.icons.filled.Person
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.vitorpamplona.amethyst.commons.marmot.GroupMemberInfo
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun MarmotGroupInfoScreen(
|
||||||
|
nostrGroupId: HexKey,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
val chatroom =
|
||||||
|
remember(nostrGroupId) {
|
||||||
|
accountViewModel.account.marmotGroupList.getOrCreateGroup(nostrGroupId)
|
||||||
|
}
|
||||||
|
val displayName by chatroom.displayName.collectAsStateWithLifecycle()
|
||||||
|
var members by remember { mutableStateOf(emptyList<GroupMemberInfo>()) }
|
||||||
|
var showLeaveDialog by remember { mutableStateOf(false) }
|
||||||
|
var showAddMemberDialog by remember { mutableStateOf(false) }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val myPubkey = accountViewModel.account.signer.pubKey
|
||||||
|
|
||||||
|
LaunchedEffect(nostrGroupId) {
|
||||||
|
members = accountViewModel.marmotGroupMembers(nostrGroupId)
|
||||||
|
chatroom.memberCount.value = members.size
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { nav.popBack() }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = "Back",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = { Text("Group Info") },
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = { showAddMemberDialog = true }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.GroupAdd,
|
||||||
|
contentDescription = "Add Member",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { padding ->
|
||||||
|
LazyColumn(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding),
|
||||||
|
) {
|
||||||
|
// Group header section
|
||||||
|
item {
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = displayName ?: "Group ${nostrGroupId.take(8)}...",
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "${members.size} members",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(top = 4.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Group ID: ${nostrGroupId.take(16)}...",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(top = 2.dp),
|
||||||
|
)
|
||||||
|
val epoch = accountViewModel.account.marmotManager?.groupEpoch(nostrGroupId)
|
||||||
|
if (epoch != null) {
|
||||||
|
Text(
|
||||||
|
text = "Epoch: $epoch",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(top = 2.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Members section header
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = "Members",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Member list
|
||||||
|
items(members, key = { it.leafIndex }) { member ->
|
||||||
|
MemberRow(
|
||||||
|
member = member,
|
||||||
|
isMe = member.pubkey == myPubkey,
|
||||||
|
onClick = {
|
||||||
|
nav.nav(Route.Profile(member.pubkey))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leave group section
|
||||||
|
item {
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(top = 8.dp))
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { showLeaveDialog = true }
|
||||||
|
.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ExitToApp,
|
||||||
|
contentDescription = "Leave Group",
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Leave Group",
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showLeaveDialog) {
|
||||||
|
LeaveGroupDialog(
|
||||||
|
groupName = displayName ?: "this group",
|
||||||
|
onConfirm = {
|
||||||
|
showLeaveDialog = false
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
accountViewModel.leaveMarmotGroup(nostrGroupId)
|
||||||
|
}
|
||||||
|
nav.nav(Route.MarmotGroupList)
|
||||||
|
},
|
||||||
|
onDismiss = { showLeaveDialog = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showAddMemberDialog) {
|
||||||
|
AddMemberDialog(
|
||||||
|
nostrGroupId = nostrGroupId,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
onDismiss = { showAddMemberDialog = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MemberRow(
|
||||||
|
member: GroupMemberInfo,
|
||||||
|
isMe: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Person,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(36.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text =
|
||||||
|
if (isMe) {
|
||||||
|
"${member.pubkey.take(16)}... (you)"
|
||||||
|
} else {
|
||||||
|
"${member.pubkey.take(16)}..."
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
fontWeight = if (isMe) FontWeight.Bold else FontWeight.Normal,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Leaf #${member.leafIndex}",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LeaveGroupDialog(
|
||||||
|
groupName: String,
|
||||||
|
onConfirm: () -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text("Leave Group") },
|
||||||
|
text = {
|
||||||
|
Text("Are you sure you want to leave \"$groupName\"? You will no longer receive messages from this group.")
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = onConfirm) {
|
||||||
|
Text("Leave", color = MaterialTheme.colorScheme.error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
+41
@@ -35,6 +35,7 @@ import com.vitorpamplona.quartz.marmot.mip02Welcome.WelcomeEvent
|
|||||||
import com.vitorpamplona.quartz.marmot.mip03GroupMessages.GroupEvent
|
import com.vitorpamplona.quartz.marmot.mip03GroupMessages.GroupEvent
|
||||||
import com.vitorpamplona.quartz.marmot.mls.group.MlsGroupManager
|
import com.vitorpamplona.quartz.marmot.mls.group.MlsGroupManager
|
||||||
import com.vitorpamplona.quartz.marmot.mls.group.MlsGroupStateStore
|
import com.vitorpamplona.quartz.marmot.mls.group.MlsGroupStateStore
|
||||||
|
import com.vitorpamplona.quartz.marmot.mls.tree.Credential
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||||
@@ -254,4 +255,44 @@ class MarmotManager(
|
|||||||
* Get all active group IDs.
|
* Get all active group IDs.
|
||||||
*/
|
*/
|
||||||
fun activeGroupIds(): Set<HexKey> = groupManager.activeGroupIds()
|
fun activeGroupIds(): Set<HexKey> = groupManager.activeGroupIds()
|
||||||
|
|
||||||
|
// --- Group Info ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the member count for a group.
|
||||||
|
*/
|
||||||
|
fun memberCount(nostrGroupId: HexKey): Int = groupManager.getGroup(nostrGroupId)?.memberCount ?: 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the member list for a group.
|
||||||
|
* Returns leaf index and Nostr pubkey (extracted from BasicCredential) for each member.
|
||||||
|
*/
|
||||||
|
fun memberPubkeys(nostrGroupId: HexKey): List<GroupMemberInfo> {
|
||||||
|
val group = groupManager.getGroup(nostrGroupId) ?: return emptyList()
|
||||||
|
return group.members().mapNotNull { (leafIndex, leafNode) ->
|
||||||
|
val pubkey =
|
||||||
|
when (val cred = leafNode.credential) {
|
||||||
|
is Credential.Basic -> cred.identity.toHexKey()
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (pubkey != null) {
|
||||||
|
GroupMemberInfo(leafIndex = leafIndex, pubkey = pubkey)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current epoch for a group.
|
||||||
|
*/
|
||||||
|
fun groupEpoch(nostrGroupId: HexKey): Long? = groupManager.getGroup(nostrGroupId)?.epoch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about a member in a Marmot MLS group.
|
||||||
|
*/
|
||||||
|
data class GroupMemberInfo(
|
||||||
|
val leafIndex: Int,
|
||||||
|
val pubkey: HexKey,
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user