From f723310dc77fce717da1977a6b4fc55028ed488b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 15:05:59 +0000 Subject: [PATCH] feat(buzz): archive/unarchive channels + fix visibility edits on Buzz MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two channel-management gaps vs the Buzz interface (both ride kind-9002 tags; no new protocol), verified against block/buzz: Archive/Unarchive — the reversible hide-from-the-sidebar the Buzz client has, distinct from delete. EditMetadataEvent gains an `archived` tag; Account/ AccountViewModel expose archiveRelayGroup; the channel and forum top bars offer Archive/Unarchive to admins (no confirm — it's reversible). The relay stamps `["archived","true"]` on the 39000, so GroupMetadataEvent.isArchived() / RelayGroupChannel.isArchived() read it directly; the community list drops archived channels out of Channels/Forums into a collapsed "Archived" tail from which they can be reopened and unarchived. Visibility-on-edit — a Buzz relay reads its own `visibility` (open/private) tag, not the NIP-29 `private` status flag, so the edit screen's private toggle was a silent no-op on Buzz. editRelayGroupMetadata now sends the `visibility` tag on Buzz relays (status flag still sent for vanilla NIP-29). Not gaps (checked): topic/purpose/TTL are in the relay's system-message vocabulary but not extracted on 9002/9007, so there's nothing to mirror. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_016MNVEKhaAu4vQRZnXv3rfG --- .../vitorpamplona/amethyst/model/Account.kt | 18 +++++ .../ui/screen/loggedIn/AccountViewModel.kt | 9 +++ .../relayGroup/RelayGroupChannelListScreen.kt | 53 ++++++++++++- .../relayGroup/RelayGroupThreadsScreen.kt | 18 ++++- .../relayGroup/RelayGroupTopBar.kt | 13 +++ amethyst/src/main/res/values/strings.xml | 3 + .../nip29RelayGroups/RelayGroupChannel.kt | 3 + .../metadata/GroupMetadataEvent.kt | 8 ++ .../moderation/EditMetadataEvent.kt | 8 ++ .../ChannelSettingsTagTest.kt | 79 +++++++++++++++++++ 10 files changed, 206 insertions(+), 6 deletions(-) create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/ChannelSettingsTagTest.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 91f929fc61..68df7e7ed0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -3676,6 +3676,10 @@ class Account( parent: String? = channel.parentGroupId(), children: List = channel.childGroupIds(), ) { + // On a Buzz relay, visibility rides a `visibility` ("open"/"private") tag — the relay does NOT + // read NIP-29's `private` status flag — so a Buzz channel's visibility only actually changes on + // edit when we send that tag. A plain NIP-29 relay ignores it and honours the status flag. + val isBuzz = BuzzRelayDialect.isBuzz(channel.groupId.relayUrl) val template = EditMetadataEvent.build( channel.groupId.id, @@ -3687,10 +3691,24 @@ class Account( geohashes = geohashes, parent = parent, children = children, + visibility = if (isBuzz) (if (isPrivate) BUZZ_VISIBILITY_PRIVATE else BUZZ_VISIBILITY_OPEN) else null, ) signAndSendPrivatelyOrBroadcast(template) { channel.relays().toList() } } + /** + * Archive or unarchive a Buzz channel (a minimal kind-9002 carrying only the `archived` tag). The + * relay hides an archived channel from the sidebar and stamps the 39000, but keeps it and its + * history — the reversible counterpart to [deleteRelayGroup]. Admin/owner only; the relay enforces. + */ + suspend fun archiveRelayGroup( + channel: RelayGroupChannel, + archived: Boolean, + ) { + val template = EditMetadataEvent.build(channel.groupId.id, archived = archived) + signAndSendPrivatelyOrBroadcast(template) { channel.relays().toList() } + } + suspend fun follow(community: AddressableNote) = sendMyPublicAndPrivateOutbox(communityList.follow(community)) suspend fun unfollow(community: AddressableNote) = sendMyPublicAndPrivateOutbox(communityList.unfollow(community)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 363a703d06..a247e1cf1e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -1675,6 +1675,15 @@ class AccountViewModel( /** Delete the channel/group for everyone (kind-9008). Owner/admin only; the relay enforces it. */ fun deleteRelayGroup(channel: RelayGroupChannel) = launchSigner { account.deleteRelayGroup(channel) } + /** + * Archive/unarchive a Buzz channel (kind-9002 `archived` tag) — hides it from the sidebar without + * destroying it, and is reversible. Owner/admin only; the relay enforces it. + */ + fun archiveRelayGroup( + channel: RelayGroupChannel, + archived: Boolean, + ) = launchSigner { account.archiveRelayGroup(channel, archived) } + /** * Take a relay group off Messages WITHOUT leaving it: drop it from my kind-10009 list so it stops * showing, but send no kind-9022 — I stay in the relay roster and can still read/post, and re-joining diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt index a1cc027c72..3cbc0f9e39 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt @@ -274,21 +274,34 @@ fun RelayGroupChannelListScreen( fun buzzSortKey(groupId: GroupId): String = channelsById[groupId.id]?.toBestDisplayName()?.lowercase() ?: groupId.id + // Archived channels (relay-signed `archived` tag on the 39000) drop out of their normal section and + // gather in a collapsed "Archived" tail — the same hide-from-the-sidebar behavior the Buzz client + // has. They stay reachable there so an admin can open one and Unarchive it from the top bar. + fun isArchived(groupId: GroupId): Boolean = channelsById[groupId.id]?.isArchived() == true + val buzzChatChannels = remember(buzzGroupIds, channelsById, starred) { buzzGroupIds - .filter { buzzTypeOf(it).let { t -> t != BUZZ_CHANNEL_TYPE_FORUM && t != BUZZ_CHANNEL_TYPE_DM } } + .filter { buzzTypeOf(it).let { t -> t != BUZZ_CHANNEL_TYPE_FORUM && t != BUZZ_CHANNEL_TYPE_DM } && !isArchived(it) } .sortedWith(compareByDescending { it.id in starred }.thenBy { buzzSortKey(it) }) } val buzzForumChannels = remember(buzzGroupIds, channelsById, starred) { buzzGroupIds - .filter { buzzTypeOf(it) == BUZZ_CHANNEL_TYPE_FORUM } + .filter { buzzTypeOf(it) == BUZZ_CHANNEL_TYPE_FORUM && !isArchived(it) } .sortedWith(compareByDescending { it.id in starred }.thenBy { buzzSortKey(it) }) } + // Every archived non-DM channel (chat + forum together), newest section at the bottom. + val buzzArchivedChannels = + remember(buzzGroupIds, channelsById) { + buzzGroupIds + .filter { buzzTypeOf(it) != BUZZ_CHANNEL_TYPE_DM && isArchived(it) } + .sortedBy { buzzSortKey(it) } + } - // Which sections the user has collapsed (session-scoped). Keyed by section id below. - var collapsedSections by remember { mutableStateOf(emptySet()) } + // Which sections the user has collapsed (session-scoped). Keyed by section id below. Archived + // starts collapsed — it's the out-of-the-way tail, expanded only when someone goes looking. + var collapsedSections by remember { mutableStateOf(setOf("archived")) } fun toggleSection(key: String) { collapsedSections = if (key in collapsedSections) collapsedSections - key else collapsedSections + key @@ -508,6 +521,38 @@ fun RelayGroupChannelListScreen( } } + // -- ARCHIVED -- Channels the relay has archived (chat + forum), tucked into a + // collapsed tail. Opening one and using the top-bar Unarchive brings it back. + if (buzzArchivedChannels.isNotEmpty()) { + val archivedCollapsed = "archived" in collapsedSections + item(key = "sec-archived") { + RelayGroupSectionHeader( + title = stringRes(R.string.relay_group_section_archived), + collapsed = archivedCollapsed, + onToggle = { toggleSection("archived") }, + ) + } + if (!archivedCollapsed) { + itemsIndexed(buzzArchivedChannels, key = { _, it -> "archived-${it.id}" }) { index, groupId -> + RowHairline(index) + val isForum = buzzTypeOf(groupId) == BUZZ_CHANNEL_TYPE_FORUM + BuzzImportRow( + groupId = groupId, + accountViewModel = accountViewModel, + onOpen = { + if (isForum) { + nav.nav(Route.RelayGroupThreads(groupId.id, relay.url)) + } else { + nav.nav(Route.RelayGroup(groupId.id, relay.url)) + } + }, + isStarred = groupId.id in starred, + showActivityPreview = !isForum, + ) + } + } + } + // -- DIRECT MESSAGES -- (this community's private conversations, most recent first) item(key = "sec-dms") { RelayGroupSectionHeader(title = stringRes(R.string.buzz_dm_title)) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupThreadsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupThreadsScreen.kt index 85f0725a78..33354badb9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupThreadsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupThreadsScreen.kt @@ -35,6 +35,7 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.IconButton @@ -61,6 +62,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel +import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupMembership import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteReplyCount import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route @@ -172,10 +174,12 @@ private fun RelayGroupThreads( }, actions = { // The forum's per-item actions, moved off the community-list row into this screen's - // top-bar overflow: Pin/Unpin and the Add/Remove-from-Messages toggle. Buzz-only, - // which every forum channel is. + // top-bar overflow: Pin/Unpin and the Add/Remove-from-Messages toggle, plus the + // admin-only Archive/Unarchive (so an archived forum can be brought back from here). + // Buzz-only, which every forum channel is. if (isBuzz) { var menuOpen by remember { mutableStateOf(false) } + val isAdmin = channel.membershipOf(accountViewModel.userProfile().pubkeyHex) == RelayGroupMembership.ADMIN IconButton(onClick = { menuOpen = true }) { Icon( symbol = MaterialSymbols.MoreVert, @@ -186,6 +190,16 @@ private fun RelayGroupThreads( DropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) { BuzzPinDropdownItem(channel.groupId) { menuOpen = false } RelayGroupMessagesDropdownItem(channel, accountViewModel) { menuOpen = false } + if (isAdmin) { + val archived = channel.isArchived() + DropdownMenuItem( + text = { Text(stringRes(if (archived) R.string.buzz_channel_unarchive else R.string.buzz_channel_archive)) }, + onClick = { + menuOpen = false + accountViewModel.archiveRelayGroup(channel, !archived) + }, + ) + } } } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt index afb2682855..4d725b781b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt @@ -370,6 +370,19 @@ fun RelayGroupTopBar( if (canPop) nav.popBack() }, ) + // Archive/Unarchive (kind-9002 `archived` tag) — a reversible hide-from-the-sidebar, + // Buzz-only and admin-gated like Delete but NOT destructive, so no confirm dialog. + // A DM is never archived (it has its own hide), so this is channels/forums only. + if (isBuzzRelay && !isDm && displayMembership == RelayGroupMembership.ADMIN) { + val archived = channel.isArchived() + DropdownMenuItem( + text = { Text(stringRes(if (archived) R.string.buzz_channel_unarchive else R.string.buzz_channel_archive)) }, + onClick = { + menuOpen = false + accountViewModel.archiveRelayGroup(channel, !archived) + }, + ) + } // Deleting the whole channel/group (kind-9008) is destructive for everyone, so it's // shown ONLY to an admin/owner — the same authorization gate as Edit above — and // routed through a confirmation dialog rather than firing on tap. diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 75d6b194a8..75d3d2a7c8 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -2613,6 +2613,8 @@ Delete \"%1$s\"? This removes the group and its history for everyone, and cannot be undone. Delete channel Delete \"%1$s\"? This removes the channel and its messages for everyone, and cannot be undone. + Archive channel + Unarchive channel Threads Pin message Unpin message @@ -3604,6 +3606,7 @@ Channels Forums + Archived Working… Add member Add people to this workspace diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip29RelayGroups/RelayGroupChannel.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip29RelayGroups/RelayGroupChannel.kt index 11c5bb23bd..9c029296f8 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip29RelayGroups/RelayGroupChannel.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip29RelayGroups/RelayGroupChannel.kt @@ -178,6 +178,9 @@ class RelayGroupChannel( fun isPrivate(): Boolean = event?.isPrivate() ?: false + /** Buzz-only: the relay has archived this channel (hidden from the sidebar, but not deleted). */ + fun isArchived(): Boolean = event?.isArchived() ?: false + fun isRestricted(): Boolean = event?.isRestricted() ?: false fun isClosed(): Boolean = event?.isClosed() ?: false diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/metadata/GroupMetadataEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/metadata/GroupMetadataEvent.kt index a05f2d7b9f..aeb4721570 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/metadata/GroupMetadataEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/metadata/GroupMetadataEvent.kt @@ -57,6 +57,14 @@ class GroupMetadataEvent( fun picture() = tags.firstTagValue("picture") + /** + * Buzz-only: whether the relay has marked this channel **archived** — a hide-from-the-sidebar + * state, distinct from a delete (the channel and its history live on). The Buzz relay stamps an + * `["archived","true"]` tag onto the 39000 for an archived channel and clients hide it; a plain + * NIP-29 relay has no such concept, so this is false there. + */ + fun isArchived() = tags.firstTagValue("archived") == "true" + /** * Topic hashtags (`t` tags) the relay advertises for this group, used by the * discovery feed's hashtag filter. NIP-29 doesn't define these; a group only diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/EditMetadataEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/EditMetadataEvent.kt index b503c830d3..61ccd41f3c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/EditMetadataEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/EditMetadataEvent.kt @@ -82,6 +82,12 @@ class EditMetadataEvent( parent: String? = null, children: List = emptyList(), previousEvents: List = emptyList(), + // Buzz-only channel-settings tags (a Buzz relay reads these on 9002; a plain NIP-29 relay + // ignores them). [visibility] is "open"/"private" — Buzz's own vocabulary, which it reads + // instead of the NIP-29 `private` status flag, so editing visibility on a Buzz channel needs + // this tag to take. [archived] toggles the channel's archived state ("true"/"false"). + visibility: String? = null, + archived: Boolean? = null, createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { @@ -90,6 +96,8 @@ class EditMetadataEvent( about?.let { add(arrayOf("about", it)) } picture?.let { add(arrayOf("picture", it)) } status.forEach { add(arrayOf(it.code)) } + visibility?.let { add(arrayOf("visibility", it)) } + archived?.let { add(arrayOf("archived", if (it) "true" else "false")) } addAll(HashtagTag.assemble(hashtags)) // Mip-map each geohash into every prefix so a coarser followed geohash still matches. geohashes.forEach { addAll(GeoHashTag.assemble(it).toList()) } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/ChannelSettingsTagTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/ChannelSettingsTagTest.kt new file mode 100644 index 0000000000..080e477790 --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/ChannelSettingsTagTest.kt @@ -0,0 +1,79 @@ +/* + * 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.quartz.nip29RelayGroups + +import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.EditMetadataEvent +import com.vitorpamplona.quartz.utils.EventFactory +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue + +/** + * The Buzz-only channel-settings tags on kind-9002 edit-metadata (`visibility`, `archived`) and the + * `archived` reflection on the relay-signed kind-39000. These ride the 9002 as tags — Buzz reads its + * own `visibility` vocabulary rather than the NIP-29 `private` status flag, and stamps `archived` onto + * the 39000 for an archived channel. + */ +class ChannelSettingsTagTest { + private val relaySelf = "aa".repeat(32) + private val sig = "bb".repeat(64) + private val id = "00".repeat(32) + private val gid = "0123456789abcdef" + + private fun tagValue( + tags: Array>, + name: String, + ): String? = tags.firstOrNull { it.isNotEmpty() && it[0] == name }?.getOrNull(1) + + @Test + fun editEmitsVisibilityTagOnlyWhenSet() { + val priv = EditMetadataEvent.build(gid, visibility = "private") + assertEquals("private", tagValue(priv.tags, "visibility")) + + val open = EditMetadataEvent.build(gid, visibility = "open") + assertEquals("open", tagValue(open.tags, "visibility")) + + // Absent by default so an ordinary metadata edit doesn't reclassify visibility. + assertNull(tagValue(EditMetadataEvent.build(gid, name = "x").tags, "visibility")) + } + + @Test + fun editEmitsArchivedTagAsTrueFalse() { + assertEquals("true", tagValue(EditMetadataEvent.build(gid, archived = true).tags, "archived")) + assertEquals("false", tagValue(EditMetadataEvent.build(gid, archived = false).tags, "archived")) + // Null archived means "don't touch it" — no tag emitted. + assertNull(tagValue(EditMetadataEvent.build(gid, name = "x").tags, "archived")) + } + + @Test + fun metadataReflectsArchivedFlag() { + val archivedTemplate = GroupMetadataEvent.build(gid, name = gid) { add(arrayOf("archived", "true")) } + val archived = EventFactory.create(id, relaySelf, archivedTemplate.createdAt, GroupMetadataEvent.KIND, archivedTemplate.tags, "", sig) as GroupMetadataEvent + assertTrue(archived.isArchived()) + + val plainTemplate = GroupMetadataEvent.build(gid, name = gid) + val plain = EventFactory.create(id, relaySelf, plainTemplate.createdAt, GroupMetadataEvent.KIND, plainTemplate.tags, "", sig) as GroupMetadataEvent + assertFalse(plain.isArchived()) + } +}