From d2da781c33a3e2be3f63f3a8dd4ce8cdf3b2c840 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 18:48:03 +0000 Subject: [PATCH] feat(home): request pictures and every NIP-71 video kind on all home legs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "All Follows" home feed fans out to four REQ builders — authors, hashtags, geohashes and communities. Only the authors leg asked for pictures (kind 20) and the NIP-71 videos (21, 22, 34235, 34236); the other three stopped at text notes, articles, classifieds, highlights, wiki and friends. HomeNewThreadFeedFilter already accepts all of those kinds, and AllFollowsTopNavFilter.match() already matches an event by a followed hashtag, geohash or community, so the events would have rendered fine — they were simply never requested. A kind-22 short tagged #bitcoin never reached Home while a kind-1 with the same tag did. Adds the five media kinds to HomePostsBuHashtagsKinds, HomePostsByGeohashKinds and HomePostsFromCommunityKinds, and derives HomePostsFromCommunityKindsStr (the `k` tag values on a NIP-72 post approval) from the kind list instead of repeating it by hand, so the two cannot drift apart again. Kinds 34235/34236 are current NIP-71 kinds, not deprecated ones — drops the "legacy" framing from the comments that described them that way. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019V3yENZpF2eTDSzjnj1ZCi --- .../signers/NostrSignerPermissionLedger.kt | 6 +- .../nip01Core/FilterHomePostsByGeohashes.kt | 10 +++ .../nip01Core/FilterHomePostsByHashtags.kt | 10 +++ .../FilterHomePostsFromCommunities.kt | 25 ++++--- .../home/HomeMediaKindCoverageTest.kt | 72 +++++++++++++++++++ .../AddressableVideoEventAddressTest.kt | 2 +- 6 files changed, 108 insertions(+), 17 deletions(-) create mode 100644 commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/HomeMediaKindCoverageTest.kt diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/connectedApps/signers/NostrSignerPermissionLedger.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/connectedApps/signers/NostrSignerPermissionLedger.kt index feb37f0cde..0991204ea0 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/connectedApps/signers/NostrSignerPermissionLedger.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/connectedApps/signers/NostrSignerPermissionLedger.kt @@ -186,7 +186,7 @@ class NostrSignerPermissionLedger( * delete afterwards, in the same risk class as the original kind 1/6/7 set (notes, reposts, * reactions, pictures, videos, voice, public/live/relay chat, threads, polls, comments, * highlights, code snippets, file metadata, reports, torrents, long-form articles, wiki, status). - * Some are *addressable* (long-form 30023, wiki 30818, legacy video 34235/34236): re-signing + * Some are *addressable* (long-form 30023, wiki 30818, video 34235/34236): re-signing * with the same `d` tag replaces the app's own prior version at that address — an accepted * trade-off, since an app that can already post arbitrary notes could do equal reputational harm. * The set also includes two harmless non-content signatures: @@ -247,8 +247,8 @@ class NostrSignerPermissionLedger( LongTextNoteEvent.KIND, // 30023 — NIP-23 long-form articles (addressable content) StatusEvent.KIND, // 30315 — ephemeral user status / presence WikiNoteEvent.KIND, // 30818 — NIP-54 wiki articles (addressable content) - VideoHorizontalEvent.KIND, // 34235 — legacy addressable horizontal video (NIP-71) - VideoVerticalEvent.KIND, // 34236 — legacy addressable vertical video (NIP-71) + VideoHorizontalEvent.KIND, // 34235 — addressable horizontal video (NIP-71) + VideoVerticalEvent.KIND, // 34236 — addressable vertical video (NIP-71) PublicationIndexEvent.KIND, // 30040 — NKBIP-01 publication index (addressable content) LearningResourceEvent.KIND, // 30142 — learning resources (addressable content) BlossomPieceIndexEvent.KIND, // 32176 — Blossom piece indexes (addressable content) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip01Core/FilterHomePostsByGeohashes.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip01Core/FilterHomePostsByGeohashes.kt index f366992b85..90bdc83585 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip01Core/FilterHomePostsByGeohashes.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip01Core/FilterHomePostsByGeohashes.kt @@ -35,6 +35,11 @@ import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip68Picture.PictureEvent +import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent +import com.vitorpamplona.quartz.nip71Video.VideoShortEvent +import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent @@ -52,6 +57,11 @@ val HomePostsByGeohashKinds = VoiceEvent.KIND, AttestationEvent.KIND, InteractiveStoryPrologueEvent.KIND, + PictureEvent.KIND, + VideoNormalEvent.KIND, + VideoShortEvent.KIND, + VideoHorizontalEvent.KIND, + VideoVerticalEvent.KIND, ) fun filterHomePostsByGeohashes( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip01Core/FilterHomePostsByHashtags.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip01Core/FilterHomePostsByHashtags.kt index a6a97fa7f7..6983457805 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip01Core/FilterHomePostsByHashtags.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip01Core/FilterHomePostsByHashtags.kt @@ -38,6 +38,11 @@ import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip68Picture.PictureEvent +import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent +import com.vitorpamplona.quartz.nip71Video.VideoShortEvent +import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent @@ -57,6 +62,11 @@ val HomePostsBuHashtagsKinds = VoiceEvent.KIND, AttestationEvent.KIND, InteractiveStoryPrologueEvent.KIND, + PictureEvent.KIND, + VideoNormalEvent.KIND, + VideoShortEvent.KIND, + VideoHorizontalEvent.KIND, + VideoVerticalEvent.KIND, ) fun filterHomePostsByHashtags( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip72Communities/FilterHomePostsFromCommunities.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip72Communities/FilterHomePostsFromCommunities.kt index 4b8a2250bb..096432af97 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip72Communities/FilterHomePostsFromCommunities.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip72Communities/FilterHomePostsFromCommunities.kt @@ -32,6 +32,11 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip68Picture.PictureEvent +import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent +import com.vitorpamplona.quartz.nip71Video.VideoShortEvent +import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent @@ -49,21 +54,15 @@ val HomePostsFromCommunityKinds = CommunityPostApprovalEvent.KIND, CommentEvent.KIND, InteractiveStoryPrologueEvent.KIND, + PictureEvent.KIND, + VideoNormalEvent.KIND, + VideoShortEvent.KIND, + VideoHorizontalEvent.KIND, + VideoVerticalEvent.KIND, ) -val HomePostsFromCommunityKindsStr = - listOf( - TextNoteEvent.KIND.toString(), - LongTextNoteEvent.KIND.toString(), - ClassifiedsEvent.KIND.toString(), - HighlightEvent.KIND.toString(), - PollEvent.KIND.toString(), - WikiNoteEvent.KIND.toString(), - NipTextEvent.KIND.toString(), - CommunityPostApprovalEvent.KIND.toString(), - CommentEvent.KIND.toString(), - InteractiveStoryPrologueEvent.KIND.toString(), - ) +/** The same set, as the `k` tag values a NIP-72 post approval carries. */ +val HomePostsFromCommunityKindsStr = HomePostsFromCommunityKinds.map { it.toString() } fun filterHomePostsFromCommunity( relay: NormalizedRelayUrl, diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/HomeMediaKindCoverageTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/HomeMediaKindCoverageTest.kt new file mode 100644 index 0000000000..c51e367053 --- /dev/null +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/HomeMediaKindCoverageTest.kt @@ -0,0 +1,72 @@ +/* + * 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.relayClient.home + +import com.vitorpamplona.amethyst.commons.relayClient.home.nip01Core.HomePostsBuHashtagsKinds +import com.vitorpamplona.amethyst.commons.relayClient.home.nip01Core.HomePostsByGeohashKinds +import com.vitorpamplona.amethyst.commons.relayClient.home.nip65Follows.HomePostsNewThreadKinds1 +import com.vitorpamplona.amethyst.commons.relayClient.home.nip72Communities.HomePostsFromCommunityKinds +import com.vitorpamplona.amethyst.commons.relayClient.home.nip72Communities.HomePostsFromCommunityKindsStr +import com.vitorpamplona.quartz.nip68Picture.PictureEvent +import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent +import com.vitorpamplona.quartz.nip71Video.VideoShortEvent +import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * The "All Follows" home feed fans out to four REQ builders: authors, hashtags, geohashes and + * communities. The home DAL renders pictures and every NIP-71 video kind from any of those legs, + * so a leg that omits a media kind silently hides content the feed would happily show — a + * kind-22 short tagged `#bitcoin` never arriving while a kind-1 with the same tag does. + */ +class HomeMediaKindCoverageTest { + private val mediaKinds = + listOf( + PictureEvent.KIND, + VideoNormalEvent.KIND, + VideoShortEvent.KIND, + VideoHorizontalEvent.KIND, + VideoVerticalEvent.KIND, + ) + + private fun assertCarriesMedia( + legName: String, + kinds: List, + ) = mediaKinds.forEach { + assertTrue(it in kinds, "$legName must request kind $it") + } + + @Test + fun everyHomeLegRequestsPicturesAndAllVideoKinds() { + assertCarriesMedia("authors", HomePostsNewThreadKinds1) + assertCarriesMedia("hashtags", HomePostsBuHashtagsKinds) + assertCarriesMedia("geohashes", HomePostsByGeohashKinds) + assertCarriesMedia("communities", HomePostsFromCommunityKinds) + } + + @Test + fun communityApprovalKTagsMirrorTheCommunityKinds() { + assertEquals(HomePostsFromCommunityKinds.map { it.toString() }, HomePostsFromCommunityKindsStr) + } +} diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip71Video/AddressableVideoEventAddressTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip71Video/AddressableVideoEventAddressTest.kt index 0f878d0ee0..e31e88f745 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip71Video/AddressableVideoEventAddressTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip71Video/AddressableVideoEventAddressTest.kt @@ -25,7 +25,7 @@ import kotlin.test.Test import kotlin.test.assertEquals /** - * Kinds 34235/34236 (legacy NIP-71 videos) are parameterized replaceable + * Kinds 34235/34236 (the addressable NIP-71 videos) are parameterized replaceable * events: their address MUST include the `d` tag. A wrong (empty-dTag) * address makes LocalCache consume the event into a different * AddressableNote than the one `a` tags point to, so quotes/reposts of