mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 16:33:27 +00:00
Merge pull request #3586 from vitorpamplona/claude/nip29-message-pinning-elzck9
NIP-29 message pinning: UI bar, moderation events, and relay group integration
This commit is contained in:
+20
@@ -31,6 +31,7 @@ import com.vitorpamplona.quartz.nip29RelayGroups.GroupId
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupAdminsEvent
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMembersEvent
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupPinnedEvent
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.tags.GroupAdminTag
|
||||
import com.vitorpamplona.quartz.utils.cache.LargeCache
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -70,6 +71,14 @@ class RelayGroupChannel(
|
||||
private set
|
||||
private var adminsUpdatedAt: Long = 0
|
||||
|
||||
/**
|
||||
* Relay-signed pinned message ids (kind 39005), in the relay's display order.
|
||||
* Pinning replaces the whole list, so this is always the full current set.
|
||||
*/
|
||||
var pinnedEventIds: List<HexKey> = emptyList()
|
||||
private set
|
||||
private var pinnedUpdatedAt: Long = 0
|
||||
|
||||
/**
|
||||
* Members ∪ admins, recomputed only when a roster event lands. [memberCount] and the discovery
|
||||
* feed read this per note / per recomposition, so caching it avoids rebuilding the set each read.
|
||||
@@ -158,6 +167,17 @@ class RelayGroupChannel(
|
||||
updateChannelInfo()
|
||||
}
|
||||
|
||||
fun updatePinned(event: GroupPinnedEvent) {
|
||||
// Only newer lists supersede; equal-or-older is dropped so a duplicate
|
||||
// arrival isn't reprocessed (no redundant emit).
|
||||
if (event.createdAt <= pinnedUpdatedAt) return
|
||||
pinnedEventIds = event.pinnedEventIds()
|
||||
pinnedUpdatedAt = event.createdAt
|
||||
updateChannelInfo()
|
||||
}
|
||||
|
||||
fun isPinned(eventId: HexKey): Boolean = eventId in pinnedEventIds
|
||||
|
||||
/**
|
||||
* The shareable NIP-19 `naddr` coordinate for this group's metadata (kind
|
||||
* 39000, authored by the relay's own key, with the host relay as a hint), or
|
||||
|
||||
+65
@@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip29RelayGroups.GroupId
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupAdminsEvent
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMembersEvent
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupPinnedEvent
|
||||
import com.vitorpamplona.quartz.utils.EventFactory
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
@@ -79,6 +80,70 @@ class RelayGroupChannelTest {
|
||||
return EventFactory.create("00".repeat(32), relaySelf, createdAt, GroupAdminsEvent.KIND, tags, "", "22".repeat(64)) as GroupAdminsEvent
|
||||
}
|
||||
|
||||
private val msg1 = "11".repeat(32)
|
||||
private val msg2 = "22".repeat(32)
|
||||
private val msg3 = "33".repeat(32)
|
||||
|
||||
private fun pinned(
|
||||
createdAt: Long,
|
||||
vararg eventIds: String,
|
||||
): GroupPinnedEvent {
|
||||
val tags = (listOf(arrayOf("d", gid)) + eventIds.map { arrayOf("e", it) }).toTypedArray()
|
||||
return EventFactory.create("00".repeat(32), relaySelf, createdAt, GroupPinnedEvent.KIND, tags, "", "22".repeat(64)) as GroupPinnedEvent
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pinnedListFoldsInOrderAndReportsMembership() {
|
||||
val c = channel()
|
||||
assertTrue(c.pinnedEventIds.isEmpty())
|
||||
assertFalse(c.isPinned(msg1))
|
||||
|
||||
c.updatePinned(pinned(100, msg1, msg2))
|
||||
|
||||
assertEquals(listOf(msg1, msg2), c.pinnedEventIds)
|
||||
assertTrue(c.isPinned(msg1))
|
||||
assertTrue(c.isPinned(msg2))
|
||||
assertFalse(c.isPinned(msg3))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pinListIsFullyReplacedByNewerEvent() {
|
||||
val c = channel()
|
||||
c.updatePinned(pinned(100, msg1, msg2))
|
||||
// A newer list drops msg1, keeps msg2, adds msg3 — the whole set is replaced.
|
||||
c.updatePinned(pinned(200, msg2, msg3))
|
||||
|
||||
assertEquals(listOf(msg2, msg3), c.pinnedEventIds)
|
||||
assertFalse(c.isPinned(msg1))
|
||||
assertTrue(c.isPinned(msg3))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun clearingPinsWithEmptyNewerList() {
|
||||
val c = channel()
|
||||
c.updatePinned(pinned(100, msg1))
|
||||
c.updatePinned(pinned(200))
|
||||
assertTrue(c.pinnedEventIds.isEmpty())
|
||||
assertFalse(c.isPinned(msg1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun stalePinEventDoesNotSupersede() {
|
||||
val c = channel()
|
||||
c.updatePinned(pinned(200, msg1, msg2))
|
||||
// An OLDER pin list arrives late — ignore it.
|
||||
c.updatePinned(pinned(100, msg3))
|
||||
assertEquals(listOf(msg1, msg2), c.pinnedEventIds)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun equalCreatedAtPinDoesNotResupersede() {
|
||||
val c = channel()
|
||||
c.updatePinned(pinned(100, msg1, msg2))
|
||||
c.updatePinned(pinned(100, msg3))
|
||||
assertEquals(listOf(msg1, msg2), c.pinnedEventIds)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun placeholderNoteCarriesChannelGathererAndIsStable() {
|
||||
val c = channel()
|
||||
|
||||
Reference in New Issue
Block a user