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.RepostEvent
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent
@@ -173,7 +174,7 @@ class DesktopLocalCache : ICacheProvider {
when (event) {
is MetadataEvent -> {
consumeMetadata(event)
true
false // metadata updates User, not Note — skip event stream
}
is TextNoteEvent -> {
@@ -212,6 +213,10 @@ class DesktopLocalCache : ICacheProvider {
consumeBookmarkList(event)
}
is CommentEvent -> {
consumeComment(event, relay)
}
else -> {
false
}
@@ -235,6 +240,24 @@ class DesktopLocalCache : ICacheProvider {
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.
* 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.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> =
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(
private val pubkey: HexKey,
@@ -140,19 +143,21 @@ class DesktopProfileFeedFilter(
) : AdditiveFeedFilter<Note>() {
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> =
cache.notes
.filterIntoSet { _, note ->
note.event is TextNoteEvent && note.author?.pubkeyHex == pubkey
}.sortedWith(DefaultFeedOrder)
.filterIntoSet { _, note -> isProfileNote(note) }
.sortedWith(DefaultFeedOrder)
.deduplicateReposts()
.take(limit())
override fun applyFilter(newItems: Set<Note>): Set<Note> =
newItems.filterTo(HashSet()) {
it.event is TextNoteEvent && it.author?.pubkeyHex == pubkey
}
override fun applyFilter(newItems: Set<Note>): Set<Note> = newItems.filterTo(HashSet()) { isProfileNote(it) }
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
}
@@ -37,7 +37,7 @@ enum class FeedMode {
*/
fun createGlobalFeedSubscription(
relays: Set<NormalizedRelayUrl>,
limit: Int = 50,
limit: Int = 200,
onEvent: (Event, Boolean, NormalizedRelayUrl, List<Filter>?) -> Unit,
onEose: (NormalizedRelayUrl, List<Filter>?) -> Unit = { _, _ -> },
): SubscriptionConfig =
@@ -55,7 +55,7 @@ fun createGlobalFeedSubscription(
fun createFollowingFeedSubscription(
relays: Set<NormalizedRelayUrl>,
followedUsers: List<String>,
limit: Int = 50,
limit: Int = 200,
onEvent: (Event, Boolean, NormalizedRelayUrl, List<Filter>?) -> Unit,
onEose: (NormalizedRelayUrl, List<Filter>?) -> Unit = { _, _ -> },
): SubscriptionConfig? {
@@ -121,9 +121,21 @@ fun createThreadRepliesSubscription(
subId = generateSubId("thread-${noteId.take(8)}"),
filters =
listOf(
// Kind 1 replies via lowercase e-tag (NIP-10)
FilterBuilders.byETags(
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,
),
),
@@ -121,6 +121,7 @@ fun UserProfileScreen(
onBack: () -> Unit,
onCompose: () -> Unit = {},
onNavigateToProfile: (String) -> Unit = {},
onNavigateToThread: (String) -> Unit = {},
onNavigateToArticle: (String) -> Unit = {},
onZapFeedback: (ZapFeedback) -> Unit = {},
) {
@@ -796,6 +797,7 @@ fun UserProfileScreen(
onReply = onCompose,
onZapFeedback = onZapFeedback,
onNavigateToProfile = onNavigateToProfile,
onNavigateToThread = onNavigateToThread,
onImageClick = { urls, index ->
lightboxState = LightboxState(urls, index)
},
@@ -296,6 +296,7 @@ internal fun RootContent(
onBack = {},
onCompose = onShowComposeDialog,
onNavigateToProfile = onNavigateToProfile,
onNavigateToThread = onNavigateToThread,
onNavigateToArticle = onNavigateToArticle,
onZapFeedback = onZapFeedback,
)
@@ -325,6 +326,7 @@ internal fun RootContent(
onBack = {},
onCompose = onShowComposeDialog,
onNavigateToProfile = onNavigateToProfile,
onNavigateToThread = onNavigateToThread,
onZapFeedback = onZapFeedback,
)
}
@@ -426,6 +428,7 @@ internal fun OverlayContent(
onBack = onBack,
onCompose = onShowComposeDialog,
onNavigateToProfile = onNavigateToProfile,
onNavigateToThread = onNavigateToThread,
onNavigateToArticle = onNavigateToArticle,
onZapFeedback = onZapFeedback,
)