Adds support for displaying video events.

This commit is contained in:
Vitor Pamplona 2023-12-29 17:24:50 -05:00
parent 57430c4366
commit 2de3d19a34
7 changed files with 176 additions and 5 deletions

View File

@ -130,7 +130,7 @@ class Account(
var translateTo: String = Locale.getDefault().language, var translateTo: String = Locale.getDefault().language,
var zapAmountChoices: List<Long> = DefaultZapAmounts, var zapAmountChoices: List<Long> = DefaultZapAmounts,
var reactionChoices: List<String> = DefaultReactions, var reactionChoices: List<String> = DefaultReactions,
var defaultZapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PRIVATE, var defaultZapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PUBLIC,
var defaultFileServer: Nip96MediaServers.ServerName = Nip96MediaServers.DEFAULT[0], var defaultFileServer: Nip96MediaServers.ServerName = Nip96MediaServers.DEFAULT[0],
var defaultHomeFollowList: MutableStateFlow<String> = MutableStateFlow(KIND3_FOLLOWS), var defaultHomeFollowList: MutableStateFlow<String> = MutableStateFlow(KIND3_FOLLOWS),
var defaultStoriesFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS), var defaultStoriesFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),

View File

@ -72,6 +72,8 @@ import com.vitorpamplona.quartz.events.RepostEvent
import com.vitorpamplona.quartz.events.SealedGossipEvent import com.vitorpamplona.quartz.events.SealedGossipEvent
import com.vitorpamplona.quartz.events.StatusEvent import com.vitorpamplona.quartz.events.StatusEvent
import com.vitorpamplona.quartz.events.TextNoteEvent import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.VideoHorizontalEvent
import com.vitorpamplona.quartz.events.VideoVerticalEvent
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -438,6 +440,9 @@ object LocalCache {
private fun consume(event: PinListEvent, relay: Relay?) { consumeBaseReplaceable(event, relay) } private fun consume(event: PinListEvent, relay: Relay?) { consumeBaseReplaceable(event, relay) }
private fun consume(event: RelaySetEvent, relay: Relay?) { consumeBaseReplaceable(event, relay) } private fun consume(event: RelaySetEvent, relay: Relay?) { consumeBaseReplaceable(event, relay) }
private fun consume(event: AudioTrackEvent, relay: Relay?) { consumeBaseReplaceable(event, relay) } private fun consume(event: AudioTrackEvent, relay: Relay?) { consumeBaseReplaceable(event, relay) }
private fun consume(event: VideoVerticalEvent, relay: Relay?) { consumeBaseReplaceable(event, relay) }
private fun consume(event: VideoHorizontalEvent, relay: Relay?) { consumeBaseReplaceable(event, relay) }
fun consume(event: StatusEvent, relay: Relay?) { fun consume(event: StatusEvent, relay: Relay?) {
val version = getOrCreateNote(event.id) val version = getOrCreateNote(event.id)
val note = getOrCreateAddressableNote(event.address()) val note = getOrCreateAddressableNote(event.address())
@ -1593,7 +1598,8 @@ object LocalCache {
} }
is StatusEvent -> consume(event, relay) is StatusEvent -> consume(event, relay)
is TextNoteEvent -> consume(event, relay) is TextNoteEvent -> consume(event, relay)
is VideoHorizontalEvent -> consume(event, relay)
is VideoVerticalEvent -> consume(event, relay)
else -> { else -> {
Log.w("Event Not Supported", event.toJson()) Log.w("Event Not Supported", event.toJson())
} }

View File

@ -156,6 +156,7 @@ import com.vitorpamplona.amethyst.ui.theme.WidthAuthorPictureModifier
import com.vitorpamplona.amethyst.ui.theme.boostedNoteModifier import com.vitorpamplona.amethyst.ui.theme.boostedNoteModifier
import com.vitorpamplona.amethyst.ui.theme.channelNotePictureModifier import com.vitorpamplona.amethyst.ui.theme.channelNotePictureModifier
import com.vitorpamplona.amethyst.ui.theme.grayText import com.vitorpamplona.amethyst.ui.theme.grayText
import com.vitorpamplona.amethyst.ui.theme.imageModifier
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
import com.vitorpamplona.amethyst.ui.theme.normalNoteModifier import com.vitorpamplona.amethyst.ui.theme.normalNoteModifier
@ -203,6 +204,9 @@ import com.vitorpamplona.quartz.events.ReportEvent
import com.vitorpamplona.quartz.events.RepostEvent import com.vitorpamplona.quartz.events.RepostEvent
import com.vitorpamplona.quartz.events.TextNoteEvent import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.UserMetadata import com.vitorpamplona.quartz.events.UserMetadata
import com.vitorpamplona.quartz.events.VideoEvent
import com.vitorpamplona.quartz.events.VideoHorizontalEvent
import com.vitorpamplona.quartz.events.VideoVerticalEvent
import com.vitorpamplona.quartz.events.toImmutableListOfLists import com.vitorpamplona.quartz.events.toImmutableListOfLists
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -636,7 +640,7 @@ fun LongCommunityHeader(
) )
} }
if (noteEvent.hasHashtags()) { if (summary != null && noteEvent.hasHashtags()) {
DisplayUncitedHashtags( DisplayUncitedHashtags(
remember(noteEvent) { noteEvent.hashtags().toImmutableList() }, remember(noteEvent) { noteEvent.hashtags().toImmutableList() },
summary ?: "", summary ?: "",
@ -1171,6 +1175,14 @@ private fun RenderNoteRow(
FileHeaderDisplay(baseNote, true, accountViewModel) FileHeaderDisplay(baseNote, true, accountViewModel)
} }
is VideoHorizontalEvent -> {
VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, accountViewModel, nav)
}
is VideoVerticalEvent -> {
VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, accountViewModel, nav)
}
is FileStorageHeaderEvent -> { is FileStorageHeaderEvent -> {
FileStorageHeaderDisplay(baseNote, true, accountViewModel) FileStorageHeaderDisplay(baseNote, true, accountViewModel)
} }
@ -2384,7 +2396,21 @@ private fun ReplyRow(
} }
if (showReply) { if (showReply) {
val replyingDirectlyTo = remember { note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.kind } } val replyingDirectlyTo = remember(note) {
if (noteEvent is BaseTextNoteEvent) {
val replyingTo = noteEvent.replyingTo()
if (replyingTo != null) {
note.replyTo?.firstOrNull() {
// important to test both ids in case it's a replaceable event.
it.idHex == replyingTo || it.event?.id() == replyingTo
}
} else {
note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.kind }
}
} else {
note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.kind }
}
}
if (replyingDirectlyTo != null && unPackReply) { if (replyingDirectlyTo != null && unPackReply) {
ReplyNoteComposition(replyingDirectlyTo, backgroundColor, accountViewModel, nav) ReplyNoteComposition(replyingDirectlyTo, backgroundColor, accountViewModel, nav)
Spacer(modifier = StdVertSpacer) Spacer(modifier = StdVertSpacer)
@ -3028,6 +3054,129 @@ fun FileHeaderDisplay(note: Note, roundedCorner: Boolean, accountViewModel: Acco
} }
} }
@Composable
fun VideoDisplay(
note: Note,
makeItShort: Boolean,
canPreview: Boolean,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val event = (note.event as? VideoEvent) ?: return
val fullUrl = event.url() ?: return
val title = event.title()
val summary = event.content.ifBlank { null }?.takeIf { title != it }
val image = event.thumb() ?: event.image()
val isYouTube = fullUrl.contains("youtube.com") || fullUrl.contains("youtu.be")
val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList }
val content by remember(note) {
val blurHash = event.blurhash()
val hash = event.hash()
val dimensions = event.dimensions()
val description = event.alt() ?: event.content
val isImage = imageExtensions.any {
removeQueryParamsForExtensionComparison(fullUrl).lowercase().endsWith(it)
}
val uri = note.toNostrUri()
mutableStateOf<ZoomableContent>(
if (isImage) {
ZoomableUrlImage(
url = fullUrl,
description = description,
hash = hash,
blurhash = blurHash,
dim = dimensions,
uri = uri
)
} else {
ZoomableUrlVideo(
url = fullUrl,
description = description,
hash = hash,
dim = dimensions,
uri = uri,
authorName = note.author?.toBestDisplayName(),
artworkUri = event.thumb() ?: event.image()
)
}
)
}
SensitivityWarning(note = note, accountViewModel = accountViewModel) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(top = 5.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (isYouTube) {
val uri = LocalUriHandler.current
Row(
modifier = Modifier.clickable { runCatching { uri.openUri(fullUrl) } }
) {
image?.let {
AsyncImage(
model = it,
contentDescription = stringResource(
R.string.preview_card_image_for,
it
),
contentScale = ContentScale.FillWidth,
modifier = MaterialTheme.colorScheme.imageModifier
)
} ?: CreateImageHeader(note, accountViewModel)
}
} else {
ZoomableContentView(
content = content,
roundedCorner = true,
accountViewModel = accountViewModel
)
}
title?.let {
Text(
text = it,
fontWeight = FontWeight.Bold,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.fillMaxWidth()
.padding(top = 5.dp)
)
}
summary?.let {
TranslatableRichTextViewer(
content = it,
canPreview = canPreview && !makeItShort,
modifier = Modifier.fillMaxWidth(),
tags = tags,
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav
)
}
if (event.hasHashtags()) {
Row(
Modifier.fillMaxWidth()
) {
DisplayUncitedHashtags(
remember(event) { event.hashtags().toImmutableList() },
summary ?: "",
nav
)
}
}
}
}
}
@Composable @Composable
fun FileStorageHeaderDisplay(baseNote: Note, roundedCorner: Boolean, accountViewModel: AccountViewModel) { fun FileStorageHeaderDisplay(baseNote: Note, roundedCorner: Boolean, accountViewModel: AccountViewModel) {
val eventHeader = (baseNote.event as? FileStorageHeaderEvent) ?: return val eventHeader = (baseNote.event as? FileStorageHeaderEvent) ?: return

View File

@ -96,6 +96,7 @@ import com.vitorpamplona.amethyst.ui.note.RenderPoll
import com.vitorpamplona.amethyst.ui.note.RenderPostApproval import com.vitorpamplona.amethyst.ui.note.RenderPostApproval
import com.vitorpamplona.amethyst.ui.note.RenderRepost import com.vitorpamplona.amethyst.ui.note.RenderRepost
import com.vitorpamplona.amethyst.ui.note.RenderTextEvent import com.vitorpamplona.amethyst.ui.note.RenderTextEvent
import com.vitorpamplona.amethyst.ui.note.VideoDisplay
import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.note.showAmount
import com.vitorpamplona.amethyst.ui.note.timeAgo import com.vitorpamplona.amethyst.ui.note.timeAgo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@ -130,6 +131,7 @@ import com.vitorpamplona.quartz.events.PinListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.RelaySetEvent import com.vitorpamplona.quartz.events.RelaySetEvent
import com.vitorpamplona.quartz.events.RepostEvent import com.vitorpamplona.quartz.events.RepostEvent
import com.vitorpamplona.quartz.events.VideoEvent
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -423,6 +425,8 @@ fun NoteMaster(
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav nav = nav
) )
} else if (noteEvent is VideoEvent) {
VideoDisplay(baseNote, false, true, backgroundColor, accountViewModel, nav)
} else if (noteEvent is FileHeaderEvent) { } else if (noteEvent is FileHeaderEvent) {
FileHeaderDisplay(baseNote, true, accountViewModel) FileHeaderDisplay(baseNote, true, accountViewModel)
} else if (noteEvent is FileStorageHeaderEvent) { } else if (noteEvent is FileStorageHeaderEvent) {

View File

@ -23,6 +23,13 @@ open class BaseTextNoteEvent(
fun mentions() = taggedUsers() fun mentions() = taggedUsers()
open fun replyTos() = taggedEvents() open fun replyTos() = taggedEvents()
fun replyingTo(): HexKey? {
val oldStylePositional = tags.lastOrNull() { it.size > 1 && it[0] == "e" }?.get(1)
val newStyle = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "reply" }?.get(1)
return newStyle ?: oldStylePositional
}
@Transient @Transient
private var citedUsersCache: Set<HexKey>? = null private var citedUsersCache: Set<HexKey>? = null

View File

@ -39,7 +39,7 @@ class GoalEvent(
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
onReady: (GoalEvent) -> Unit onReady: (GoalEvent) -> Unit
) { ) {
var tags = mutableListOf( val tags = mutableListOf(
arrayOf(AMOUNT, amount.toString()), arrayOf(AMOUNT, amount.toString()),
arrayOf("relays") + relays, arrayOf("relays") + relays,
arrayOf("alt", alt) arrayOf("alt", alt)

View File

@ -28,6 +28,11 @@ abstract class VideoEvent(
fun torrentInfoHash() = tags.firstOrNull { it.size > 1 && it[0] == TORRENT_INFOHASH }?.get(1) fun torrentInfoHash() = tags.firstOrNull { it.size > 1 && it[0] == TORRENT_INFOHASH }?.get(1)
fun blurhash() = tags.firstOrNull { it.size > 1 && it[0] == BLUR_HASH }?.get(1) fun blurhash() = tags.firstOrNull { it.size > 1 && it[0] == BLUR_HASH }?.get(1)
fun title() = tags.firstOrNull { it.size > 1 && it[0] == TITLE }?.get(1)
fun summary() = tags.firstOrNull { it.size > 1 && it[0] == SUMMARY }?.get(1)
fun image() = tags.firstOrNull { it.size > 1 && it[0] == IMAGE }?.get(1)
fun thumb() = tags.firstOrNull { it.size > 1 && it[0] == THUMB }?.get(1)
fun hasUrl() = tags.any { it.size > 1 && it[0] == URL } fun hasUrl() = tags.any { it.size > 1 && it[0] == URL }
companion object { companion object {