mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 08:47:33 +00:00
feat: complete AddMember KeyPackage fetch and resolve member display names
AddMember flow (was TODO, now functional): - Account.fetchKeyPackageAndAddMember(): queries outbox relays for the target user's KeyPackageEvent (kind:30443) using client.fetchFirst(), decodes the base64 KeyPackage content, and calls addMarmotGroupMember() which publishes the commit then sends the Welcome gift wrap - AddMemberScreen now calls the real implementation and navigates back on success Member display names: - MemberRow now uses LoadUser to resolve Nostr pubkeys to User profiles from LocalCache, showing bestDisplayName() instead of raw hex - UserPicture composable shows profile pictures next to member names - "(you)" indicator for the current user's entry https://claude.ai/code/session_0194SxKfAU61PY92eqP4cCXM
This commit is contained in:
@@ -1731,6 +1731,60 @@ class Account(
|
||||
client.publish(outbound.signedEvent, groupRelays)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a user's KeyPackage from relays and add them to a Marmot group.
|
||||
* Returns a status message describing the outcome.
|
||||
*/
|
||||
@OptIn(kotlin.io.encoding.ExperimentalEncodingApi::class)
|
||||
suspend fun fetchKeyPackageAndAddMember(
|
||||
nostrGroupId: HexKey,
|
||||
memberPubKey: HexKey,
|
||||
): String {
|
||||
val manager = marmotManager ?: return "Error: Marmot not initialized"
|
||||
if (!isWriteable()) return "Error: Account is read-only"
|
||||
|
||||
// Build filter for the member's KeyPackages
|
||||
val filter = manager.subscriptionManager.keyPackageFilter(memberPubKey)
|
||||
val relays = outboxRelays.flow.value
|
||||
|
||||
// Query across outbox relays
|
||||
val filterMap = relays.associateWith { listOf(filter) }
|
||||
|
||||
val event =
|
||||
client.fetchFirst(
|
||||
filters = filterMap,
|
||||
)
|
||||
|
||||
if (event == null) {
|
||||
return "Error: No KeyPackage found for this user. They may not have published one yet."
|
||||
}
|
||||
|
||||
if (event !is com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageEvent) {
|
||||
return "Error: Unexpected event type received"
|
||||
}
|
||||
|
||||
val keyPackageBase64 = event.keyPackageBase64()
|
||||
if (keyPackageBase64.isBlank()) {
|
||||
return "Error: KeyPackage event has empty content"
|
||||
}
|
||||
|
||||
val keyPackageBytes =
|
||||
kotlin.io.encoding.Base64
|
||||
.decode(keyPackageBase64)
|
||||
val keyPackageEventId = event.id
|
||||
val groupRelays = relays.toList()
|
||||
|
||||
addMarmotGroupMember(
|
||||
nostrGroupId = nostrGroupId,
|
||||
memberPubKey = memberPubKey,
|
||||
keyPackageBytes = keyPackageBytes,
|
||||
keyPackageEventId = keyPackageEventId,
|
||||
groupRelays = groupRelays,
|
||||
)
|
||||
|
||||
return "Success: Member added to group"
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a member to a Marmot MLS group.
|
||||
* Publishes the commit GroupEvent, then sends the Welcome gift wrap.
|
||||
|
||||
+5
@@ -1471,6 +1471,11 @@ class AccountViewModel(
|
||||
|
||||
fun marmotGroupMembers(nostrGroupId: String): List<com.vitorpamplona.amethyst.commons.marmot.GroupMemberInfo> = account.marmotManager?.memberPubkeys(nostrGroupId) ?: emptyList()
|
||||
|
||||
suspend fun addMarmotGroupMember(
|
||||
nostrGroupId: String,
|
||||
memberPubKey: String,
|
||||
): String = account.fetchKeyPackageAndAddMember(nostrGroupId, memberPubKey)
|
||||
|
||||
override fun onCleared() {
|
||||
Log.d("AccountViewModel", "onCleared")
|
||||
callController?.cleanup()
|
||||
|
||||
+13
-11
@@ -65,21 +65,23 @@ fun AddMemberScreen(
|
||||
onCancel = { nav.popBack() },
|
||||
onPost = {
|
||||
isAdding = true
|
||||
statusMessage = "Looking up KeyPackage..."
|
||||
val pubkey = resolvePubkey(memberInput)
|
||||
if (pubkey == null) {
|
||||
statusMessage = "Error: Invalid public key format"
|
||||
return@ActionTopBar
|
||||
}
|
||||
isAdding = true
|
||||
statusMessage = "Fetching KeyPackage for ${pubkey.take(8)}..."
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val pubkey = resolvePubkey(memberInput)
|
||||
if (pubkey == null) {
|
||||
statusMessage = "Error: Invalid public key format"
|
||||
val result =
|
||||
accountViewModel.addMarmotGroupMember(nostrGroupId, pubkey)
|
||||
statusMessage = result
|
||||
if (result.startsWith("Success")) {
|
||||
nav.popBack()
|
||||
} else {
|
||||
isAdding = false
|
||||
return@launch
|
||||
}
|
||||
statusMessage = "Fetching KeyPackage for ${pubkey.take(8)}..."
|
||||
// TODO: Fetch KeyPackage from relays and call addMarmotGroupMember
|
||||
statusMessage =
|
||||
"Error: KeyPackage fetch not yet implemented. " +
|
||||
"The member's KeyPackage (kind:30443) must be fetched from relays."
|
||||
isAdding = false
|
||||
} catch (e: Exception) {
|
||||
statusMessage = "Error: ${e.message}"
|
||||
isAdding = false
|
||||
|
||||
+27
-24
@@ -27,14 +27,12 @@ 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
|
||||
@@ -61,7 +59,9 @@ 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.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -170,9 +170,8 @@ fun MarmotGroupInfoScreen(
|
||||
MemberRow(
|
||||
member = member,
|
||||
isMe = member.pubkey == myPubkey,
|
||||
onClick = {
|
||||
nav.nav(Route.Profile(member.pubkey))
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
@@ -223,36 +222,40 @@ fun MarmotGroupInfoScreen(
|
||||
fun MemberRow(
|
||||
member: GroupMemberInfo,
|
||||
isMe: Boolean,
|
||||
onClick: () -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.clickable { nav.nav(Route.Profile(member.pubkey)) }
|
||||
.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,
|
||||
UserPicture(
|
||||
userHex = member.pubkey,
|
||||
size = 36.dp,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
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,
|
||||
)
|
||||
LoadUser(baseUserHex = member.pubkey, accountViewModel = accountViewModel) { user ->
|
||||
val displayName = user?.toBestDisplayName() ?: "${member.pubkey.take(16)}..."
|
||||
Text(
|
||||
text =
|
||||
if (isMe) {
|
||||
"$displayName (you)"
|
||||
} else {
|
||||
displayName
|
||||
},
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user