mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 11:48:24 +00:00
feat(nip29): add RelayGroupChannel model + group message datasource
First feature-layer slice for NIP-29 relay groups, cloning the ephemeral-chat (NIP-C7) pattern and reusing the existing relay-pinned send/subscribe primitives (RelayBasedFilter + signAndSendPrivatelyOrBroadcast) — no new transport. - quartz: GroupId(id, relayUrl) identifier for a relay group (host relay + group id), mirroring ephemChat's RoomId. - commons: RelayGroupChannel — a metadata-backed Channel (like NIP-28's PublicChatChannel) keyed by GroupId, deriving name/picture/about/flags from the relay-signed kind 39000 event and pinning relays() to the single host. - amethyst: filterMessagesToRelayGroup / filterMyMessagesToRelayGroup datasource sub-assemblers — kind 9 + poll timeline scoped by the `h` tag and pinned to the host relay via RelayBasedFilter, mirroring the ephemeral-chat sub-assemblers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
This commit is contained in:
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.publicChannels.datasource.subassemblies
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.tags.GroupIdTag
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.nipC7Chats.ChatEvent
|
||||
|
||||
/** Timeline kinds shown in a NIP-29 group: chat messages and polls. */
|
||||
private val RELAY_GROUP_TIMELINE_KINDS = listOf(ChatEvent.KIND, PollEvent.KIND)
|
||||
|
||||
/**
|
||||
* The message timeline for a NIP-29 group. A group's chat lives entirely on its
|
||||
* host relay, scoped by the `h` tag, so the filter is pinned to
|
||||
* [RelayGroupChannel.relays] (always the single host) and never fans out to the
|
||||
* user's other relays.
|
||||
*/
|
||||
fun filterMessagesToRelayGroup(
|
||||
channel: RelayGroupChannel,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter> =
|
||||
channel.relays().toSet().map {
|
||||
RelayBasedFilter(
|
||||
relay = it,
|
||||
filter =
|
||||
Filter(
|
||||
kinds = RELAY_GROUP_TIMELINE_KINDS,
|
||||
tags = mapOf(GroupIdTag.TAG_NAME to listOf(channel.groupId.id)),
|
||||
limit = 200,
|
||||
since = since?.get(it)?.time,
|
||||
),
|
||||
)
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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.publicChannels.datasource.subassemblies
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.tags.GroupIdTag
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.nipC7Chats.ChatEvent
|
||||
|
||||
private val RELAY_GROUP_TIMELINE_KINDS = listOf(ChatEvent.KIND, PollEvent.KIND)
|
||||
|
||||
/**
|
||||
* The current user's own messages in a NIP-29 group. A smaller companion to
|
||||
* [filterMessagesToRelayGroup] that back-fills the user's sent messages (so
|
||||
* optimistic sends reconcile) — pinned to the group's host relay and scoped by
|
||||
* both the `h` group tag and the author.
|
||||
*/
|
||||
fun filterMyMessagesToRelayGroup(
|
||||
channel: RelayGroupChannel,
|
||||
pubKey: HexKey,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter> =
|
||||
channel.relays().toSet().map {
|
||||
RelayBasedFilter(
|
||||
relay = it,
|
||||
filter =
|
||||
Filter(
|
||||
kinds = RELAY_GROUP_TIMELINE_KINDS,
|
||||
tags = mapOf(GroupIdTag.TAG_NAME to listOf(channel.groupId.id)),
|
||||
authors = listOf(pubKey),
|
||||
limit = 50,
|
||||
since = since?.get(it)?.time,
|
||||
),
|
||||
)
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.commons.model.nip29RelayGroups
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.model.Channel
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.GroupId
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent
|
||||
|
||||
/**
|
||||
* A NIP-29 relay-based group ("channel" in Discord terms). Unlike a NIP-C7
|
||||
* [com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel] — a
|
||||
* bare relay+room with no server-owned metadata — a relay group has a
|
||||
* relay-signed metadata event (kind 39000) carrying its name, picture, about and
|
||||
* status flags, so this channel is metadata-backed like a NIP-28
|
||||
* [com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel].
|
||||
*
|
||||
* It is keyed by [GroupId] (host relay + group id): the group only exists on its
|
||||
* host relay, so every read and write is pinned there via [relays].
|
||||
*/
|
||||
@Stable
|
||||
class RelayGroupChannel(
|
||||
val groupId: GroupId,
|
||||
) : Channel() {
|
||||
/** The latest relay-signed kind 39000 metadata event, when known. */
|
||||
var event: GroupMetadataEvent? = null
|
||||
|
||||
/** Long-term reference to the metadata note (LocalCache holds weak references). */
|
||||
var metadataNote: Note? = null
|
||||
|
||||
var updatedMetadataAt: Long = 0
|
||||
|
||||
/** A relay group lives on exactly one relay: its host. */
|
||||
override fun relays() = setOf(groupId.relayUrl)
|
||||
|
||||
override fun toBestDisplayName(): String = event?.name() ?: groupId.id
|
||||
|
||||
fun summary(): String? = event?.about()
|
||||
|
||||
fun profilePicture(): String? = event?.picture()
|
||||
|
||||
fun isPrivate(): Boolean = event?.isPrivate() ?: false
|
||||
|
||||
fun isRestricted(): Boolean = event?.isRestricted() ?: false
|
||||
|
||||
fun isClosed(): Boolean = event?.isClosed() ?: false
|
||||
|
||||
fun isHidden(): Boolean = event?.isHidden() ?: false
|
||||
|
||||
fun hasLivekit(): Boolean = event?.hasLivekit() ?: false
|
||||
|
||||
fun updateGroupInfo(
|
||||
event: GroupMetadataEvent,
|
||||
eventNote: Note? = null,
|
||||
) {
|
||||
// Only newer metadata supersedes.
|
||||
if (event.createdAt < updatedMetadataAt) return
|
||||
this.event = event
|
||||
this.metadataNote = eventNote
|
||||
this.updatedMetadataAt = event.createdAt
|
||||
updateChannelInfo()
|
||||
}
|
||||
|
||||
fun anyNameStartsWith(prefix: String): Boolean =
|
||||
groupId.id.contains(prefix, true) ||
|
||||
event?.name()?.contains(prefix, true) == true ||
|
||||
event?.about()?.contains(prefix, true) == true
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
|
||||
|
||||
/**
|
||||
* The identity of a NIP-29 relay-based group. A group is NOT global: its id is
|
||||
* only unique within its host relay, so a group is always the pair of the host
|
||||
* relay and the group id (the `h` tag on its events, the `d` tag on its kind
|
||||
* 39000 metadata). Mirrors [com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId].
|
||||
*/
|
||||
data class GroupId(
|
||||
val id: String,
|
||||
val relayUrl: NormalizedRelayUrl,
|
||||
) : Comparable<GroupId> {
|
||||
fun toKey() = "$id@${relayUrl.url}"
|
||||
|
||||
fun toDisplayKey() = "$id@${relayUrl.displayUrl()}"
|
||||
|
||||
override fun compareTo(other: GroupId): Int {
|
||||
val result = id.compareTo(other.id)
|
||||
return if (result == 0) {
|
||||
relayUrl.url.compareTo(other.relayUrl.url)
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user