feat: read MIP-01 group metadata from MLS GroupContext extensions

Implement end-to-end reading of MarmotGroupData (extension 0xF2EE)
from the MLS GroupContext.extensions and sync to the UI layer.

Quartz (protocol):
- MarmotGroupData.decodeTls(): deserialize from TLS wire format
  (version, nostrGroupId, name, description, admin pubkeys, relays,
  image fields)
- MarmotGroupData.fromExtensions(): find and decode from extension list
- MlsGroup.extensions: expose groupContext.extensions publicly

Commons (shared logic):
- MarmotGroupChatroom: add description, adminPubkeys, relays fields
  as MutableStateFlow for reactive UI
- MarmotManager.groupMetadata(): extract MIP-01 data from group
- MarmotManager.syncMetadataTo(): sync metadata + member count to
  a MarmotGroupChatroom

Sync points (amethyst):
- Account init: sync metadata after restoreAll() for all restored groups
- GiftWrapEventHandler: sync after Welcome processing (group join)
- GroupEventHandler: sync after CommitProcessed (epoch advances may
  update extensions via GroupContextExtensions proposals)

UI (GroupInfoScreen):
- Display group description from MIP-01 metadata
- Display relay URLs from MIP-01 metadata
- Mark admin members with "- admin" suffix (from adminPubkeys)

https://claude.ai/code/session_0194SxKfAU61PY92eqP4cCXM
This commit is contained in:
Claude
2026-04-05 22:10:33 +00:00
parent 0748d2a998
commit 3e49e650c2
7 changed files with 161 additions and 18 deletions
@@ -20,6 +20,7 @@
*/
package com.vitorpamplona.amethyst.commons.marmot
import com.vitorpamplona.amethyst.commons.model.marmotGroups.MarmotGroupChatroom
import com.vitorpamplona.quartz.marmot.GroupEventResult
import com.vitorpamplona.quartz.marmot.MarmotInboundProcessor
import com.vitorpamplona.quartz.marmot.MarmotOutboundProcessor
@@ -31,6 +32,7 @@ import com.vitorpamplona.quartz.marmot.WelcomeResult
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageEvent
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRotationManager
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageUtils
import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData
import com.vitorpamplona.quartz.marmot.mip02Welcome.WelcomeEvent
import com.vitorpamplona.quartz.marmot.mip03GroupMessages.GroupEvent
import com.vitorpamplona.quartz.marmot.mls.group.MlsGroupManager
@@ -287,6 +289,37 @@ class MarmotManager(
* Get the current epoch for a group.
*/
fun groupEpoch(nostrGroupId: HexKey): Long? = groupManager.getGroup(nostrGroupId)?.epoch
/**
* Get the MIP-01 group metadata from the MLS GroupContext extensions.
* Returns null if the group doesn't exist or has no MarmotGroupData extension.
*/
fun groupMetadata(nostrGroupId: HexKey): MarmotGroupData? {
val group = groupManager.getGroup(nostrGroupId) ?: return null
return MarmotGroupData.fromExtensions(group.extensions)
}
/**
* Sync MIP-01 metadata and member info from the MLS group into a [MarmotGroupChatroom].
* Call after joining a group, processing a commit, or restoring from storage.
*/
fun syncMetadataTo(
nostrGroupId: HexKey,
chatroom: MarmotGroupChatroom,
) {
val metadata = groupMetadata(nostrGroupId)
if (metadata != null) {
if (metadata.name.isNotEmpty()) {
chatroom.displayName.value = metadata.name
}
if (metadata.description.isNotEmpty()) {
chatroom.description.value = metadata.description
}
chatroom.adminPubkeys.value = metadata.adminPubkeys
chatroom.relays.value = metadata.relays
}
chatroom.memberCount.value = memberCount(nostrGroupId)
}
}
/**
@@ -42,6 +42,9 @@ class MarmotGroupChatroom(
) : NotesGatherer {
var messages: Set<Note> = setOf()
var displayName = MutableStateFlow<String?>(null)
var description = MutableStateFlow<String?>(null)
var adminPubkeys = MutableStateFlow<List<HexKey>>(emptyList())
var relays = MutableStateFlow<List<String>>(emptyList())
var memberCount = MutableStateFlow(0)
var newestMessage: Note? = null