Adds flare.pub videos to the media tab on Amethyst

This commit is contained in:
Vitor Pamplona
2024-06-04 17:12:14 -04:00
parent 1bf8165641
commit f1e516662c
10 changed files with 124 additions and 24 deletions

View File

@@ -28,10 +28,15 @@ import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter
import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
import com.vitorpamplona.quartz.events.VideoHorizontalEvent
import com.vitorpamplona.quartz.events.VideoVerticalEvent
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
val SUPPORTED_VIDEO_FEED_MIME_TYPES = listOf("image/jpeg", "image/gif", "image/png", "image/webp", "video/mp4", "video/mpeg", "video/webm", "audio/aac", "audio/mpeg", "audio/webm", "audio/wav")
val SUPPORTED_VIDEO_FEED_MIME_TYPES_SET = SUPPORTED_VIDEO_FEED_MIME_TYPES.toSet()
object NostrVideoDataSource : NostrDataSource("VideoFeed") { object NostrVideoDataSource : NostrDataSource("VideoFeed") {
lateinit var account: Account lateinit var account: Account
@@ -40,8 +45,6 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
var job: Job? = null var job: Job? = null
val SUPPORTED_VIDEO_MIME_TYPES = listOf("image/jpeg", "image/gif", "image/png", "image/webp", "video/mp4", "video/mpeg", "video/webm", "audio/aac", "audio/mpeg", "audio/webm", "audio/wav")
override fun start() { override fun start() {
job?.cancel() job?.cancel()
job = job =
@@ -68,9 +71,9 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
filter = filter =
JsonFilter( JsonFilter(
authors = follows, authors = follows,
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND), kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND),
limit = 200, limit = 200,
tags = mapOf("m" to SUPPORTED_VIDEO_MIME_TYPES), tags = mapOf("m" to SUPPORTED_VIDEO_FEED_MIME_TYPES),
since = since =
latestEOSEs.users[account.userProfile()] latestEOSEs.users[account.userProfile()]
?.followList ?.followList
@@ -89,14 +92,14 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
types = setOf(FeedType.GLOBAL), types = setOf(FeedType.GLOBAL),
filter = filter =
JsonFilter( JsonFilter(
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND), kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND),
tags = tags =
mapOf( mapOf(
"t" to "t" to
hashToLoad hashToLoad
.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) } .map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) }
.flatten(), .flatten(),
"m" to SUPPORTED_VIDEO_MIME_TYPES, "m" to SUPPORTED_VIDEO_FEED_MIME_TYPES,
), ),
limit = 100, limit = 100,
since = since =
@@ -117,14 +120,14 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
types = setOf(FeedType.GLOBAL), types = setOf(FeedType.GLOBAL),
filter = filter =
JsonFilter( JsonFilter(
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND), kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND),
tags = tags =
mapOf( mapOf(
"g" to "g" to
hashToLoad hashToLoad
.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) } .map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) }
.flatten(), .flatten(),
"m" to SUPPORTED_VIDEO_MIME_TYPES, "m" to SUPPORTED_VIDEO_FEED_MIME_TYPES,
), ),
limit = 100, limit = 100,
since = since =

View File

@@ -23,10 +23,13 @@ package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.SUPPORTED_VIDEO_FEED_MIME_TYPES_SET
import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.MuteListEvent
import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.events.VideoHorizontalEvent
import com.vitorpamplona.quartz.events.VideoVerticalEvent
class VideoFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() { class VideoFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
override fun feedKey(): String { override fun feedKey(): String {
@@ -65,7 +68,12 @@ class VideoFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
): Boolean { ): Boolean {
val noteEvent = it.event val noteEvent = it.event
return ((noteEvent is FileHeaderEvent && noteEvent.hasUrl() && noteEvent.isImageOrVideo()) || (noteEvent is FileStorageHeaderEvent && noteEvent.isImageOrVideo())) && return (
(noteEvent is FileHeaderEvent && noteEvent.hasUrl() && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET)) ||
(noteEvent is VideoVerticalEvent && noteEvent.hasUrl() && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET)) ||
(noteEvent is VideoHorizontalEvent && noteEvent.hasUrl() && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET)) ||
(noteEvent is FileStorageHeaderEvent && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET))
) &&
params.match(noteEvent) && params.match(noteEvent) &&
account.isAcceptable(it) account.isAcceptable(it)
} }

View File

@@ -82,6 +82,7 @@ import com.vitorpamplona.amethyst.ui.note.types.DisplayRelaySet
import com.vitorpamplona.amethyst.ui.note.types.EditState import com.vitorpamplona.amethyst.ui.note.types.EditState
import com.vitorpamplona.amethyst.ui.note.types.FileHeaderDisplay import com.vitorpamplona.amethyst.ui.note.types.FileHeaderDisplay
import com.vitorpamplona.amethyst.ui.note.types.FileStorageHeaderDisplay import com.vitorpamplona.amethyst.ui.note.types.FileStorageHeaderDisplay
import com.vitorpamplona.amethyst.ui.note.types.JustVideoDisplay
import com.vitorpamplona.amethyst.ui.note.types.RenderAppDefinition import com.vitorpamplona.amethyst.ui.note.types.RenderAppDefinition
import com.vitorpamplona.amethyst.ui.note.types.RenderAudioHeader import com.vitorpamplona.amethyst.ui.note.types.RenderAudioHeader
import com.vitorpamplona.amethyst.ui.note.types.RenderAudioTrack import com.vitorpamplona.amethyst.ui.note.types.RenderAudioTrack
@@ -174,6 +175,7 @@ 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.TextNoteModificationEvent import com.vitorpamplona.quartz.events.TextNoteModificationEvent
import com.vitorpamplona.quartz.events.VideoEvent
import com.vitorpamplona.quartz.events.VideoHorizontalEvent import com.vitorpamplona.quartz.events.VideoHorizontalEvent
import com.vitorpamplona.quartz.events.VideoVerticalEvent import com.vitorpamplona.quartz.events.VideoVerticalEvent
import com.vitorpamplona.quartz.events.WikiNoteEvent import com.vitorpamplona.quartz.events.WikiNoteEvent
@@ -305,6 +307,7 @@ fun AcceptableNote(
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote) is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote)
is FileHeaderEvent -> FileHeaderDisplay(baseNote, false, false, accountViewModel) is FileHeaderEvent -> FileHeaderDisplay(baseNote, false, false, accountViewModel)
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, false, false, accountViewModel) is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, false, false, accountViewModel)
is VideoEvent -> JustVideoDisplay(baseNote, false, false, accountViewModel)
else -> else ->
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup -> LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
CheckNewAndRenderNote( CheckNewAndRenderNote(

View File

@@ -0,0 +1,88 @@
/**
* Copyright (c) 2024 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.ui.note.types
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.events.VideoEvent
@Composable
fun JustVideoDisplay(
note: Note,
roundedCorner: Boolean,
isFiniteHeight: Boolean,
accountViewModel: AccountViewModel,
) {
val event = (note.event as? VideoEvent) ?: return
val fullUrl = event.url() ?: return
val content by
remember(note) {
val blurHash = event.blurhash()
val hash = event.hash()
val dimensions = event.dimensions()
val description = event.content.ifEmpty { null } ?: event.alt()
val isImage = event.mimeType()?.startsWith("image/") == true || RichTextParser.isImageUrl(fullUrl)
val uri = note.toNostrUri()
mutableStateOf<BaseMediaContent>(
if (isImage) {
MediaUrlImage(
url = fullUrl,
description = description,
hash = hash,
blurhash = blurHash,
dim = dimensions,
uri = uri,
)
} else {
MediaUrlVideo(
url = fullUrl,
description = description,
hash = hash,
blurhash = blurHash,
dim = dimensions,
uri = uri,
authorName = note.author?.toBestDisplayName(),
)
},
)
}
SensitivityWarning(note = note, accountViewModel = accountViewModel) {
ZoomableContentView(
content = content,
roundedCorner = roundedCorner,
isFiniteHeight = isFiniteHeight,
accountViewModel = accountViewModel,
)
}
}

View File

@@ -82,6 +82,7 @@ import com.vitorpamplona.amethyst.ui.note.ZapReaction
import com.vitorpamplona.amethyst.ui.note.elements.NoteDropDownMenu import com.vitorpamplona.amethyst.ui.note.elements.NoteDropDownMenu
import com.vitorpamplona.amethyst.ui.note.types.FileHeaderDisplay import com.vitorpamplona.amethyst.ui.note.types.FileHeaderDisplay
import com.vitorpamplona.amethyst.ui.note.types.FileStorageHeaderDisplay import com.vitorpamplona.amethyst.ui.note.types.FileStorageHeaderDisplay
import com.vitorpamplona.amethyst.ui.note.types.JustVideoDisplay
import com.vitorpamplona.amethyst.ui.screen.FeedEmpty import com.vitorpamplona.amethyst.ui.screen.FeedEmpty
import com.vitorpamplona.amethyst.ui.screen.FeedError import com.vitorpamplona.amethyst.ui.screen.FeedError
import com.vitorpamplona.amethyst.ui.screen.FeedState import com.vitorpamplona.amethyst.ui.screen.FeedState
@@ -104,6 +105,7 @@ import com.vitorpamplona.amethyst.ui.theme.onBackgroundColorFilter
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
import com.vitorpamplona.quartz.events.VideoEvent
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -331,6 +333,8 @@ private fun RenderVideoOrPictureNote(
FileHeaderDisplay(note, false, true, accountViewModel) FileHeaderDisplay(note, false, true, accountViewModel)
} else if (noteEvent is FileStorageHeaderEvent) { } else if (noteEvent is FileStorageHeaderEvent) {
FileStorageHeaderDisplay(note, false, true, accountViewModel) FileStorageHeaderDisplay(note, false, true, accountViewModel)
} else if (noteEvent is VideoEvent) {
JustVideoDisplay(note, false, true, accountViewModel)
} }
} }
} }

View File

@@ -58,11 +58,7 @@ class FileHeaderEvent(
fun hasUrl() = tags.any { it.size > 1 && it[0] == URL } fun hasUrl() = tags.any { it.size > 1 && it[0] == URL }
fun isImageOrVideo(): Boolean { fun isOneOf(mimeTypes: Set<String>) = tags.any { it.size > 1 && it[0] == MIME_TYPE && mimeTypes.contains(it[1]) }
val mimeType = mimeType() ?: return false
return mimeType.startsWith("image/") || mimeType.startsWith("video/")
}
companion object { companion object {
const val KIND = 1063 const val KIND = 1063

View File

@@ -54,11 +54,7 @@ class FileStorageHeaderEvent(
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 isImageOrVideo(): Boolean { fun isOneOf(mimeTypes: Set<String>) = tags.any { it.size > 1 && it[0] == FileHeaderEvent.MIME_TYPE && mimeTypes.contains(it[1]) }
val mimeType = mimeType() ?: return false
return mimeType.startsWith("image/") || mimeType.startsWith("video/")
}
companion object { companion object {
const val KIND = 1065 const val KIND = 1065

View File

@@ -65,6 +65,8 @@ abstract class VideoEvent(
fun hasUrl() = tags.any { it.size > 1 && it[0] == URL } fun hasUrl() = tags.any { it.size > 1 && it[0] == URL }
fun isOneOf(mimeTypes: Set<String>) = tags.any { it.size > 1 && it[0] == FileHeaderEvent.MIME_TYPE && mimeTypes.contains(it[1]) }
companion object { companion object {
private const val URL = "url" private const val URL = "url"
private const val ENCRYPTION_KEY = "aes-256-gcm" private const val ENCRYPTION_KEY = "aes-256-gcm"
@@ -84,7 +86,7 @@ abstract class VideoEvent(
private const val IMAGE = "image" private const val IMAGE = "image"
private const val THUMB = "thumb" private const val THUMB = "thumb"
fun create( fun <T : VideoEvent> create(
kind: Int, kind: Int,
url: String, url: String,
magnetUri: String? = null, magnetUri: String? = null,
@@ -102,7 +104,7 @@ abstract class VideoEvent(
altDescription: String, altDescription: String,
signer: NostrSigner, signer: NostrSigner,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
onReady: (FileHeaderEvent) -> Unit, onReady: (T) -> Unit,
) { ) {
val tags = val tags =
listOfNotNull( listOfNotNull(
@@ -128,7 +130,7 @@ abstract class VideoEvent(
) )
val content = alt ?: "" val content = alt ?: ""
signer.sign(createdAt, kind, tags.toTypedArray(), content, onReady) signer.sign<T>(createdAt, kind, tags.toTypedArray(), content, onReady)
} }
} }
} }

View File

@@ -54,7 +54,7 @@ class VideoHorizontalEvent(
sensitiveContent: Boolean? = null, sensitiveContent: Boolean? = null,
signer: NostrSigner, signer: NostrSigner,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
onReady: (FileHeaderEvent) -> Unit, onReady: (VideoHorizontalEvent) -> Unit,
) { ) {
create( create(
KIND, KIND,

View File

@@ -54,7 +54,7 @@ class VideoVerticalEvent(
sensitiveContent: Boolean? = null, sensitiveContent: Boolean? = null,
signer: NostrSigner, signer: NostrSigner,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
onReady: (FileHeaderEvent) -> Unit, onReady: (VideoVerticalEvent) -> Unit,
) { ) {
create( create(
KIND, KIND,