From e12b6b4172b0bd5476746940f47ebdfeb7b87d9e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 23:33:55 +0000 Subject: [PATCH] feat(live-chat): surface stream clips in the profile gallery tab Clips are authored by viewers but carry a `p` tag identifying the stream host, so they naturally belong on the host's profile. Extend the profile media subscription with `{kinds:[1313], #p:[pubkey]}`, accept LiveActivitiesClipEvent in UserProfileGalleryFeedFilter when the clip references a stream hosted by this user, and render the clip's `r`-tag URL as a MediaUrlVideo tile in GalleryThumbnail. Also plug LiveActivitiesClipEvent into NoteCompose via the existing RenderChatClip so tapping a clip tile opens a working thread view instead of an empty one. https://claude.ai/code/session_01WJA5PvDABegBYUu6h42YZi --- .../amethyst/ui/note/NoteCompose.kt | 6 ++++ .../datasource/FilterUserProfileMedia.kt | 35 +++++++++++++------ .../loggedIn/profile/gallery/GalleryThumb.kt | 16 +++++++++ .../dal/UserProfileGalleryFeedFilter.kt | 29 +++++++++------ 4 files changed, 65 insertions(+), 21 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index a869d042b9..8f007e18fc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -172,6 +172,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderZapPoll import com.vitorpamplona.amethyst.ui.note.types.ReplyRenderType import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderChatClip import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.RenderPublicChatChannelHeader import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer @@ -250,6 +251,7 @@ import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.clip.LiveActivitiesClipEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent @@ -938,6 +940,10 @@ private fun RenderNoteRow( RenderLnZap(baseNote, backgroundColor, accountViewModel, nav) } + is LiveActivitiesClipEvent -> { + RenderChatClip(baseNote, accountViewModel, nav) + } + is FhirResourceEvent -> { RenderFhirResource(baseNote, accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMedia.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMedia.kt index 93170f9d33..ae247c5dd8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMedia.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMedia.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip53LiveActivities.clip.LiveActivitiesClipEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent @@ -51,16 +52,30 @@ fun filterUserProfileMedia( ?: user.allUsedRelaysOrNull() ?: LocalCache.relayHints.hintsForKey(user.pubkeyHex) - return relays.map { relay -> - RelayBasedFilter( - relay = relay, - filter = - Filter( - kinds = UserProfileMediaKinds, - authors = listOf(user.pubkeyHex), - limit = 200, - since = since?.get(relay)?.time, - ), + return relays.flatMap { relay -> + listOf( + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = UserProfileMediaKinds, + authors = listOf(user.pubkeyHex), + limit = 200, + since = since?.get(relay)?.time, + ), + ), + // Clips are authored by viewers but carry a `p` tag identifying the stream host. + // Fetch clips OF this profile's streams so they surface in their gallery. + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = listOf(LiveActivitiesClipEvent.KIND), + tags = mapOf("p" to listOf(user.pubkeyHex)), + limit = 100, + since = since?.get(relay)?.time, + ), + ), ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt index 66f06d75e1..69f4399362 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt @@ -62,6 +62,7 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.ui.theme.Size50Modifier import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent +import com.vitorpamplona.quartz.nip53LiveActivities.clip.LiveActivitiesClipEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip71Video.VideoEvent @@ -130,6 +131,21 @@ fun GalleryThumbnail( thumbhash = imeta.thumbhash, ) } + } else if (noteEvent is LiveActivitiesClipEvent) { + noteEvent.videoUrl()?.let { url -> + listOf( + MediaUrlVideo( + url = url, + description = noteEvent.title() ?: noteEvent.content, + hash = null, + blurhash = null, + dim = null, + uri = null, + mimeType = null, + thumbhash = null, + ), + ) + } ?: emptyList() } else { emptyList() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt index 0e943641de..bcb315079c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt @@ -30,6 +30,7 @@ import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent +import com.vitorpamplona.quartz.nip53LiveActivities.clip.LiveActivitiesClipEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip71Video.RegularVideoEvent import com.vitorpamplona.quartz.nip71Video.ReplaceableVideoEvent @@ -75,17 +76,23 @@ class UserProfileGalleryFeedFilter( user: User, ): Boolean { val noteEvent = it.event - return ( - ( - it.event?.pubKey == user.pubkeyHex && - ( - noteEvent is PictureEvent || - noteEvent is RegularVideoEvent || - (noteEvent is ReplaceableVideoEvent && it is AddressableNote) || - (noteEvent is ProfileGalleryEntryEvent && noteEvent.hasUrl() && noteEvent.hasFromEvent()) - ) - ) // && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET)) - ) && + val authoredByUser = + it.event?.pubKey == user.pubkeyHex && + ( + noteEvent is PictureEvent || + noteEvent is RegularVideoEvent || + (noteEvent is ReplaceableVideoEvent && it is AddressableNote) || + (noteEvent is ProfileGalleryEntryEvent && noteEvent.hasUrl() && noteEvent.hasFromEvent()) + ) + + // Clips are authored by viewers, not the host — accept them when they reference + // a stream hosted by this user AND carry a playable URL. + val clipOfUsersStream = + noteEvent is LiveActivitiesClipEvent && + noteEvent.host() == user.pubkeyHex && + !noteEvent.videoUrl().isNullOrBlank() + + return (authoredByUser || clipOfUsersStream) && params.match(noteEvent, it.relays) && account.isAcceptable(it) }