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
This commit is contained in:
@@ -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.ReplyRenderType
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay
|
import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
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.screen.loggedIn.chats.publicChannels.nip28PublicChat.RenderPublicChatChannelHeader
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
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.day.CalendarDateSlotEvent
|
||||||
import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent
|
import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent
|
||||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
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.MeetingRoomEvent
|
||||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
|
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
|
||||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||||
@@ -938,6 +940,10 @@ private fun RenderNoteRow(
|
|||||||
RenderLnZap(baseNote, backgroundColor, accountViewModel, nav)
|
RenderLnZap(baseNote, backgroundColor, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is LiveActivitiesClipEvent -> {
|
||||||
|
RenderChatClip(baseNote, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
|
||||||
is FhirResourceEvent -> {
|
is FhirResourceEvent -> {
|
||||||
RenderFhirResource(baseNote, accountViewModel, nav)
|
RenderFhirResource(baseNote, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-10
@@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
|||||||
import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent
|
import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
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.nip68Picture.PictureEvent
|
||||||
import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent
|
import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent
|
||||||
import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent
|
import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent
|
||||||
@@ -51,16 +52,30 @@ fun filterUserProfileMedia(
|
|||||||
?: user.allUsedRelaysOrNull()
|
?: user.allUsedRelaysOrNull()
|
||||||
?: LocalCache.relayHints.hintsForKey(user.pubkeyHex)
|
?: LocalCache.relayHints.hintsForKey(user.pubkeyHex)
|
||||||
|
|
||||||
return relays.map { relay ->
|
return relays.flatMap { relay ->
|
||||||
RelayBasedFilter(
|
listOf(
|
||||||
relay = relay,
|
RelayBasedFilter(
|
||||||
filter =
|
relay = relay,
|
||||||
Filter(
|
filter =
|
||||||
kinds = UserProfileMediaKinds,
|
Filter(
|
||||||
authors = listOf(user.pubkeyHex),
|
kinds = UserProfileMediaKinds,
|
||||||
limit = 200,
|
authors = listOf(user.pubkeyHex),
|
||||||
since = since?.get(relay)?.time,
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
@@ -62,6 +62,7 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
|
||||||
import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent
|
import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent
|
||||||
|
import com.vitorpamplona.quartz.nip53LiveActivities.clip.LiveActivitiesClipEvent
|
||||||
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
|
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
|
||||||
import com.vitorpamplona.quartz.nip71Video.VideoEvent
|
import com.vitorpamplona.quartz.nip71Video.VideoEvent
|
||||||
|
|
||||||
@@ -130,6 +131,21 @@ fun GalleryThumbnail(
|
|||||||
thumbhash = imeta.thumbhash,
|
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 {
|
} else {
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-11
@@ -30,6 +30,7 @@ import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
|||||||
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||||
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
|
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
|
||||||
import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent
|
import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent
|
||||||
|
import com.vitorpamplona.quartz.nip53LiveActivities.clip.LiveActivitiesClipEvent
|
||||||
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
|
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
|
||||||
import com.vitorpamplona.quartz.nip71Video.RegularVideoEvent
|
import com.vitorpamplona.quartz.nip71Video.RegularVideoEvent
|
||||||
import com.vitorpamplona.quartz.nip71Video.ReplaceableVideoEvent
|
import com.vitorpamplona.quartz.nip71Video.ReplaceableVideoEvent
|
||||||
@@ -75,17 +76,23 @@ class UserProfileGalleryFeedFilter(
|
|||||||
user: User,
|
user: User,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val noteEvent = it.event
|
val noteEvent = it.event
|
||||||
return (
|
val authoredByUser =
|
||||||
(
|
it.event?.pubKey == user.pubkeyHex &&
|
||||||
it.event?.pubKey == user.pubkeyHex &&
|
(
|
||||||
(
|
noteEvent is PictureEvent ||
|
||||||
noteEvent is PictureEvent ||
|
noteEvent is RegularVideoEvent ||
|
||||||
noteEvent is RegularVideoEvent ||
|
(noteEvent is ReplaceableVideoEvent && it is AddressableNote) ||
|
||||||
(noteEvent is ReplaceableVideoEvent && it is AddressableNote) ||
|
(noteEvent is ProfileGalleryEntryEvent && noteEvent.hasUrl() && noteEvent.hasFromEvent())
|
||||||
(noteEvent is ProfileGalleryEntryEvent && noteEvent.hasUrl() && noteEvent.hasFromEvent())
|
)
|
||||||
)
|
|
||||||
) // && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET))
|
// 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) &&
|
params.match(noteEvent, it.relays) &&
|
||||||
account.isAcceptable(it)
|
account.isAcceptable(it)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user