fix(desktop): feed loading issues — missing events, broken profile navigation

- Profile feed now includes reposts (kind 6/16), not just text notes
- Metadata consume returns false to avoid wasteful null note lookups
- Feed subscription limits raised from 50 to 200 for global/following
- Thread replies subscription fetches NIP-22 comments (kind 1111)
- Cache now handles CommentEvent for thread display
- Wire onNavigateToThread through UserProfileScreen so tapping notes opens threads

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
nrobi144
2026-03-30 13:33:18 +03:00
parent 882c39fd0e
commit 0a41806d7e
5 changed files with 59 additions and 14 deletions
@@ -39,6 +39,7 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent
@@ -173,7 +174,7 @@ class DesktopLocalCache : ICacheProvider {
when (event) { when (event) {
is MetadataEvent -> { is MetadataEvent -> {
consumeMetadata(event) consumeMetadata(event)
true false // metadata updates User, not Note — skip event stream
} }
is TextNoteEvent -> { is TextNoteEvent -> {
@@ -212,6 +213,10 @@ class DesktopLocalCache : ICacheProvider {
consumeBookmarkList(event) consumeBookmarkList(event)
} }
is CommentEvent -> {
consumeComment(event, relay)
}
else -> { else -> {
false false
} }
@@ -235,6 +240,24 @@ class DesktopLocalCache : ICacheProvider {
return true return true
} }
/**
* Consumes a kind 1111 comment event (NIP-22).
* Like text notes but uses BaseThreadedEvent reply structure.
*/
private fun consumeComment(
event: CommentEvent,
relay: NormalizedRelayUrl?,
): Boolean {
val note = getOrCreateNote(event.id)
if (note.event != null) return false
val author = getOrCreateUser(event.pubKey)
val repliesTo = event.tagsWithoutCitations().map { getOrCreateNote(it) }
note.loadEvent(event, author, repliesTo)
relay?.let { note.addRelay(it) }
repliesTo.forEach { it.addReply(note) }
return true
}
/** /**
* Consumes a kind 7 reaction event. * Consumes a kind 7 reaction event.
* Links reaction to target notes via e-tags and a-tags. * Links reaction to target notes via e-tags and a-tags.
@@ -34,7 +34,10 @@ import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
private fun isFeedNote(event: com.vitorpamplona.quartz.nip01Core.core.Event?): Boolean = event is TextNoteEvent || event is RepostEvent || event is GenericRepostEvent private fun isFeedNote(event: com.vitorpamplona.quartz.nip01Core.core.Event?): Boolean =
event is TextNoteEvent ||
event is RepostEvent ||
event is GenericRepostEvent
private fun List<Note>.deduplicateReposts(): List<Note> = private fun List<Note>.deduplicateReposts(): List<Note> =
distinctBy { note -> distinctBy { note ->
@@ -132,7 +135,7 @@ class DesktopThreadFilter(
} }
/** /**
* Profile feed: all kind 1 notes by a specific pubkey. * Profile feed: text notes + reposts by a specific pubkey.
*/ */
class DesktopProfileFeedFilter( class DesktopProfileFeedFilter(
private val pubkey: HexKey, private val pubkey: HexKey,
@@ -140,19 +143,21 @@ class DesktopProfileFeedFilter(
) : AdditiveFeedFilter<Note>() { ) : AdditiveFeedFilter<Note>() {
override fun feedKey(): String = "profile-$pubkey" override fun feedKey(): String = "profile-$pubkey"
private fun isProfileNote(note: Note): Boolean {
val event = note.event ?: return false
return note.author?.pubkeyHex == pubkey && isFeedNote(event)
}
override fun feed(): List<Note> = override fun feed(): List<Note> =
cache.notes cache.notes
.filterIntoSet { _, note -> .filterIntoSet { _, note -> isProfileNote(note) }
note.event is TextNoteEvent && note.author?.pubkeyHex == pubkey .sortedWith(DefaultFeedOrder)
}.sortedWith(DefaultFeedOrder) .deduplicateReposts()
.take(limit()) .take(limit())
override fun applyFilter(newItems: Set<Note>): Set<Note> = override fun applyFilter(newItems: Set<Note>): Set<Note> = newItems.filterTo(HashSet()) { isProfileNote(it) }
newItems.filterTo(HashSet()) {
it.event is TextNoteEvent && it.author?.pubkeyHex == pubkey
}
override fun sort(items: Set<Note>): List<Note> = items.sortedWith(DefaultFeedOrder) override fun sort(items: Set<Note>): List<Note> = items.sortedWith(DefaultFeedOrder).deduplicateReposts()
override fun limit(): Int = 1000 override fun limit(): Int = 1000
} }
@@ -37,7 +37,7 @@ enum class FeedMode {
*/ */
fun createGlobalFeedSubscription( fun createGlobalFeedSubscription(
relays: Set<NormalizedRelayUrl>, relays: Set<NormalizedRelayUrl>,
limit: Int = 50, limit: Int = 200,
onEvent: (Event, Boolean, NormalizedRelayUrl, List<Filter>?) -> Unit, onEvent: (Event, Boolean, NormalizedRelayUrl, List<Filter>?) -> Unit,
onEose: (NormalizedRelayUrl, List<Filter>?) -> Unit = { _, _ -> }, onEose: (NormalizedRelayUrl, List<Filter>?) -> Unit = { _, _ -> },
): SubscriptionConfig = ): SubscriptionConfig =
@@ -55,7 +55,7 @@ fun createGlobalFeedSubscription(
fun createFollowingFeedSubscription( fun createFollowingFeedSubscription(
relays: Set<NormalizedRelayUrl>, relays: Set<NormalizedRelayUrl>,
followedUsers: List<String>, followedUsers: List<String>,
limit: Int = 50, limit: Int = 200,
onEvent: (Event, Boolean, NormalizedRelayUrl, List<Filter>?) -> Unit, onEvent: (Event, Boolean, NormalizedRelayUrl, List<Filter>?) -> Unit,
onEose: (NormalizedRelayUrl, List<Filter>?) -> Unit = { _, _ -> }, onEose: (NormalizedRelayUrl, List<Filter>?) -> Unit = { _, _ -> },
): SubscriptionConfig? { ): SubscriptionConfig? {
@@ -121,9 +121,21 @@ fun createThreadRepliesSubscription(
subId = generateSubId("thread-${noteId.take(8)}"), subId = generateSubId("thread-${noteId.take(8)}"),
filters = filters =
listOf( listOf(
// Kind 1 replies via lowercase e-tag (NIP-10)
FilterBuilders.byETags( FilterBuilders.byETags(
eventIds = listOf(noteId), eventIds = listOf(noteId),
kinds = listOf(1), // TextNoteEvent kinds = listOf(1),
limit = limit,
),
// Kind 1111 comments via lowercase e-tag (reply parent) or uppercase E-tag (root)
FilterBuilders.byTags(
tags = mapOf("e" to listOf(noteId)),
kinds = listOf(1111),
limit = limit,
),
FilterBuilders.byTags(
tags = mapOf("E" to listOf(noteId)),
kinds = listOf(1111),
limit = limit, limit = limit,
), ),
), ),
@@ -121,6 +121,7 @@ fun UserProfileScreen(
onBack: () -> Unit, onBack: () -> Unit,
onCompose: () -> Unit = {}, onCompose: () -> Unit = {},
onNavigateToProfile: (String) -> Unit = {}, onNavigateToProfile: (String) -> Unit = {},
onNavigateToThread: (String) -> Unit = {},
onNavigateToArticle: (String) -> Unit = {}, onNavigateToArticle: (String) -> Unit = {},
onZapFeedback: (ZapFeedback) -> Unit = {}, onZapFeedback: (ZapFeedback) -> Unit = {},
) { ) {
@@ -796,6 +797,7 @@ fun UserProfileScreen(
onReply = onCompose, onReply = onCompose,
onZapFeedback = onZapFeedback, onZapFeedback = onZapFeedback,
onNavigateToProfile = onNavigateToProfile, onNavigateToProfile = onNavigateToProfile,
onNavigateToThread = onNavigateToThread,
onImageClick = { urls, index -> onImageClick = { urls, index ->
lightboxState = LightboxState(urls, index) lightboxState = LightboxState(urls, index)
}, },
@@ -296,6 +296,7 @@ internal fun RootContent(
onBack = {}, onBack = {},
onCompose = onShowComposeDialog, onCompose = onShowComposeDialog,
onNavigateToProfile = onNavigateToProfile, onNavigateToProfile = onNavigateToProfile,
onNavigateToThread = onNavigateToThread,
onNavigateToArticle = onNavigateToArticle, onNavigateToArticle = onNavigateToArticle,
onZapFeedback = onZapFeedback, onZapFeedback = onZapFeedback,
) )
@@ -325,6 +326,7 @@ internal fun RootContent(
onBack = {}, onBack = {},
onCompose = onShowComposeDialog, onCompose = onShowComposeDialog,
onNavigateToProfile = onNavigateToProfile, onNavigateToProfile = onNavigateToProfile,
onNavigateToThread = onNavigateToThread,
onZapFeedback = onZapFeedback, onZapFeedback = onZapFeedback,
) )
} }
@@ -426,6 +428,7 @@ internal fun OverlayContent(
onBack = onBack, onBack = onBack,
onCompose = onShowComposeDialog, onCompose = onShowComposeDialog,
onNavigateToProfile = onNavigateToProfile, onNavigateToProfile = onNavigateToProfile,
onNavigateToThread = onNavigateToThread,
onNavigateToArticle = onNavigateToArticle, onNavigateToArticle = onNavigateToArticle,
onZapFeedback = onZapFeedback, onZapFeedback = onZapFeedback,
) )