mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 08:47:33 +00:00
Merge remote-tracking branch 'origin/claude/buzz-repo-analysis-7k54ga' into claude/buzz-repo-analysis-7k54ga
This commit is contained in:
+32
-1
@@ -20,6 +20,8 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.buzz.BuzzDmChannels
|
||||
import com.vitorpamplona.amethyst.commons.model.buzz.BuzzDmRegistry
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState
|
||||
@@ -99,7 +101,28 @@ class AccountNotificationsEoseFromInboxRelaysManager(
|
||||
)
|
||||
}
|
||||
|
||||
return inbox + groups
|
||||
// Buzz DM channels are NOT in the published group list (membership is server-side, tracked in
|
||||
// BuzzDmChannels), so the joined-group query above skips them. Poll each DM host relay the same
|
||||
// way — `#p` = me + `#h` = my DM channels — so a reaction/zap/repost on my DM message surfaces
|
||||
// in notifications, not only as a chip inside the open conversation. Hidden DMs are excluded.
|
||||
val myPubkey = user(key).pubkeyHex
|
||||
val hiddenDms = BuzzDmRegistry.hiddenFor(myPubkey)
|
||||
val dmGroups =
|
||||
BuzzDmChannels
|
||||
.channelsFor(myPubkey)
|
||||
.filterKeys { it !in hiddenDms }
|
||||
.entries
|
||||
.groupBy({ it.value }, { it.key })
|
||||
.flatMap { (relay, channelIds) ->
|
||||
filterGroupNotificationsToPubkey(
|
||||
relay = relay,
|
||||
pubkey = myPubkey,
|
||||
groupIds = channelIds.distinct(),
|
||||
since = since?.get(relay)?.time ?: pagingBoundary,
|
||||
)
|
||||
}
|
||||
|
||||
return inbox + groups + dmGroups
|
||||
}
|
||||
|
||||
val userJobMap = mutableMapOf<User, List<Job>>()
|
||||
@@ -122,6 +145,14 @@ class AccountNotificationsEoseFromInboxRelaysManager(
|
||||
invalidateFilters()
|
||||
}
|
||||
},
|
||||
// Re-subscribe when a Buzz DM is discovered/hidden so its host relay is polled for
|
||||
// reactions/zaps on my DM messages.
|
||||
key.account.scope.launch(Dispatchers.IO) {
|
||||
BuzzDmChannels.flow.sample(1000).collectLatest { invalidateFilters() }
|
||||
},
|
||||
key.account.scope.launch(Dispatchers.IO) {
|
||||
BuzzDmRegistry.hidden.sample(1000).collectLatest { invalidateFilters() }
|
||||
},
|
||||
key.account.scope.launch(Dispatchers.IO) {
|
||||
key.feedContentStates.notifications.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
|
||||
invalidateFilters()
|
||||
|
||||
+29
-6
@@ -20,6 +20,8 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.buzz.BuzzDmChannels
|
||||
import com.vitorpamplona.amethyst.commons.model.buzz.BuzzDmRegistry
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.paging.BackwardRelayPager
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.paging.PagingStatus
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
@@ -82,12 +84,26 @@ class AccountNotificationsHistoryEoseManager(
|
||||
val status: StateFlow<PagingStatus> = pager.status
|
||||
|
||||
// Each joined group's id, bucketed by the normalized host relay it lives on. Used both to route the
|
||||
// group filter and (its keys) to add group host relays to the paged relay set.
|
||||
private fun groupsByRelay(account: Account): Map<NormalizedRelayUrl, List<String>> =
|
||||
account.relayGroupList.liveRelayGroupList.value
|
||||
.groupBy({ RelayUrlNormalizer.normalizeOrNull(it.relayUrl) }, { it.groupId })
|
||||
.mapNotNull { (relay, ids) -> relay?.let { it to ids.distinct() } }
|
||||
.toMap()
|
||||
// group filter and (its keys) to add group host relays to the paged relay set. Buzz DM channels aren't
|
||||
// in the published list (server-side membership, tracked in BuzzDmChannels), so include them here too —
|
||||
// otherwise paging back never loads older reactions/zaps on my DM messages. Hidden DMs are excluded.
|
||||
private fun groupsByRelay(account: Account): Map<NormalizedRelayUrl, List<String>> {
|
||||
val myPubkey = account.userProfile().pubkeyHex
|
||||
val hiddenDms = BuzzDmRegistry.hiddenFor(myPubkey)
|
||||
|
||||
val listGroups =
|
||||
account.relayGroupList.liveRelayGroupList.value
|
||||
.mapNotNull { tag -> RelayUrlNormalizer.normalizeOrNull(tag.relayUrl)?.let { it to tag.groupId } }
|
||||
val dmGroups =
|
||||
BuzzDmChannels
|
||||
.channelsFor(myPubkey)
|
||||
.filterKeys { it !in hiddenDms }
|
||||
.map { (channelId, relay) -> relay to channelId }
|
||||
|
||||
return (listGroups + dmGroups)
|
||||
.groupBy({ it.first }, { it.second })
|
||||
.mapValues { (_, ids) -> ids.distinct() }
|
||||
}
|
||||
|
||||
// The full relay set this account pages notifications back through: inbox relays + group host relays.
|
||||
private fun notificationRelaySet(account: Account): Set<NormalizedRelayUrl> = account.notificationRelays.flow.value + groupsByRelay(account).keys
|
||||
@@ -159,6 +175,13 @@ class AccountNotificationsHistoryEoseManager(
|
||||
.sample(1000)
|
||||
.collectLatest { invalidateFilters() }
|
||||
},
|
||||
// A Buzz DM discovered/hidden adds or drops its host relay from the paged set.
|
||||
key.account.scope.launch(Dispatchers.IO) {
|
||||
BuzzDmChannels.flow.sample(1000).collectLatest { invalidateFilters() }
|
||||
},
|
||||
key.account.scope.launch(Dispatchers.IO) {
|
||||
BuzzDmRegistry.hidden.sample(1000).collectLatest { invalidateFilters() }
|
||||
},
|
||||
)
|
||||
|
||||
return requestNewSubscription(historyListener(key))
|
||||
|
||||
+28
-2
@@ -390,6 +390,27 @@ class NotificationFeedFilter(
|
||||
return md.isBuzzDm() && md.buzzParticipants().contains(me)
|
||||
}
|
||||
|
||||
/**
|
||||
* A reaction/repost that carries NO `p` tag — so neither the follow filter nor the p-tag gate can
|
||||
* route it — but whose reacted target (the LAST `e` tag, already loaded) is MY note. Buzz likes are a
|
||||
* bare `["e", <id>]` with no author `p` tag (unlike NIP-25, which p-tags the reacted author), so being
|
||||
* the author of the liked post is the only relevance signal. Restricted to the no-`p`-tag case so a
|
||||
* well-formed reaction keeps its normal p-tag routing, and to an already-loaded target so it can't
|
||||
* accept blindly.
|
||||
*/
|
||||
private fun isReactionToMyEvent(
|
||||
note: Note,
|
||||
me: HexKey,
|
||||
): Boolean {
|
||||
val event = note.event
|
||||
if (event !is ReactionEvent && event !is RepostEvent && event !is GenericRepostEvent) return false
|
||||
if (event.tags.any { it.getOrNull(0) == "p" }) return false
|
||||
return note.replyTo
|
||||
?.lastOrNull()
|
||||
?.author
|
||||
?.pubkeyHex == me
|
||||
}
|
||||
|
||||
fun acceptableEvent(
|
||||
it: Note,
|
||||
filterParams: FilterByListParams,
|
||||
@@ -502,6 +523,11 @@ class NotificationFeedFilter(
|
||||
|
||||
val isConcord = isConcordMessage || isConcordReaction
|
||||
|
||||
// A bare reaction/repost (no p-tag) to my own already-loaded note — e.g. a Buzz like, which is
|
||||
// just `["e", <id>]` — is relevant to me: bypass the follow filter and satisfy the p-tag gate,
|
||||
// exactly like a Concord reaction, since being the author of the liked post is the only signal.
|
||||
val isReactionToMe = isReactionToMyEvent(it, loggedInUserHex)
|
||||
|
||||
// Concord CHAT (a message/reply) honors the "Messages in notifications" toggle that silences DMs
|
||||
// and Marmot groups above. A reaction isn't a message — regular reactions ignore that toggle, so
|
||||
// Concord reactions do too (only isConcordMessage is gated).
|
||||
@@ -520,8 +546,8 @@ class NotificationFeedFilter(
|
||||
// to genuine replies, so unrelated channel chatter never leaks through.
|
||||
return noteEvent?.kind in NOTIFICATION_KINDS &&
|
||||
(noteEvent is LnZapEvent || notifAuthor != loggedInUserHex) &&
|
||||
(isChessEvent || isConcord || filterParams.isGlobal() || notifAuthor == null || filterParams.isAuthorInFollows(notifAuthor)) &&
|
||||
(noteEvent?.isTaggedUser(loggedInUserHex) == true || isNotifiablePublicChatReply(it, loggedInUserHex)) &&
|
||||
(isChessEvent || isConcord || isReactionToMe || filterParams.isGlobal() || notifAuthor == null || filterParams.isAuthorInFollows(notifAuthor)) &&
|
||||
(noteEvent?.isTaggedUser(loggedInUserHex) == true || isNotifiablePublicChatReply(it, loggedInUserHex) || isReactionToMe) &&
|
||||
(filterParams.isHiddenList || notifAuthor == null || !account.isHidden(notifAuthor)) &&
|
||||
(noteEvent !is PrivateDmEvent || !account.isDecryptedContentHidden(noteEvent)) &&
|
||||
// For a Concord note the explicit p-tag above IS the relevance signal (the reply/reaction/
|
||||
|
||||
Reference in New Issue
Block a user