use alternative gallery using 1163 events instead of 10011 lists.

(This might still need some tuning but works in general)
This commit is contained in:
Believethehype 2024-07-06 00:11:35 +02:00
parent 06a1862d17
commit 511a7030b8
13 changed files with 291 additions and 156 deletions

View File

@ -71,7 +71,6 @@ import com.vitorpamplona.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.FileServersEvent
import com.vitorpamplona.quartz.events.FileStorageEvent
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
import com.vitorpamplona.quartz.events.GalleryListEvent
import com.vitorpamplona.quartz.events.GeneralListEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.GiftWrapEvent
@ -92,6 +91,7 @@ import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.Price
import com.vitorpamplona.quartz.events.PrivateDmEvent
import com.vitorpamplona.quartz.events.PrivateOutboxRelayListEvent
import com.vitorpamplona.quartz.events.ProfileGalleryEntryEvent
import com.vitorpamplona.quartz.events.ReactionEvent
import com.vitorpamplona.quartz.events.RelayAuthEvent
import com.vitorpamplona.quartz.events.ReportEvent
@ -2204,48 +2204,27 @@ class Account(
relay: String?,
) {
if (!isWriteable()) return
GalleryListEvent.addEvent(
userProfile().latestGalleryList,
idHex,
url,
relay,
signer,
) {
Client.send(it)
LocalCache.consume(it)
ProfileGalleryEntryEvent.create(
url = url,
eventid = idHex,
/*magnetUri = magnetUri,
mimeType = headerInfo.mimeType,
hash = headerInfo.hash,
size = headerInfo.size.toString(),
dimensions = headerInfo.dim,
blurhash = headerInfo.blurHash,
alt = alt,
originalHash = originalHash,
sensitiveContent = sensitiveContent, */
signer = signer,
) { event ->
Client.send(event)
LocalCache.consume(event, null)
}
}
fun removeFromGallery(
note: Note,
url: String,
) {
if (!isWriteable()) return
val galleryentries = userProfile().latestGalleryList ?: return
if (note is AddressableNote) {
GalleryListEvent.removeReplaceable(
galleryentries,
note.address,
url,
signer,
) {
Client.send(it)
LocalCache.consume(it)
}
} else {
GalleryListEvent.removeEvent(
galleryentries,
note.idHex,
url,
signer,
) {
Client.send(it)
LocalCache.consume(it)
}
}
fun removeFromGallery(note: Note) {
delete(note)
}
fun addBookmark(

View File

@ -75,7 +75,6 @@ import com.vitorpamplona.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.FileServersEvent
import com.vitorpamplona.quartz.events.FileStorageEvent
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
import com.vitorpamplona.quartz.events.GalleryListEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.GiftWrapEvent
import com.vitorpamplona.quartz.events.GitIssueEvent
@ -104,6 +103,7 @@ import com.vitorpamplona.quartz.events.PinListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.PrivateDmEvent
import com.vitorpamplona.quartz.events.PrivateOutboxRelayListEvent
import com.vitorpamplona.quartz.events.ProfileGalleryEntryEvent
import com.vitorpamplona.quartz.events.ReactionEvent
import com.vitorpamplona.quartz.events.RecommendRelayEvent
import com.vitorpamplona.quartz.events.RelaySetEvent
@ -424,17 +424,6 @@ object LocalCache {
}
}
fun consume(event: GalleryListEvent) {
val user = getOrCreateUser(event.pubKey)
if (user.latestGalleryList == null || event.createdAt > user.latestGalleryList!!.createdAt) {
user.updateGallery(event)
// Log.d("MT", "New User Metadata ${oldUser.pubkeyDisplayHex} ${oldUser.toBestDisplayName()}")
} else {
// Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()}
// ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}")
}
}
fun formattedDateTime(timestamp: Long): String =
Instant
.ofEpochSecond(timestamp)
@ -1680,6 +1669,26 @@ object LocalCache {
refreshObservers(note)
}
fun consume(
event: ProfileGalleryEntryEvent,
relay: Relay?,
) {
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
fun consume(
event: FileStorageHeaderEvent,
relay: Relay?,
@ -2535,13 +2544,13 @@ object LocalCache {
is DraftEvent -> consume(event, relay)
is EmojiPackEvent -> consume(event, relay)
is EmojiPackSelectionEvent -> consume(event, relay)
is GalleryListEvent -> consume(event)
is GenericRepostEvent -> {
event.containedPost()?.let { verifyAndConsume(it, relay) }
consume(event)
}
is FhirResourceEvent -> consume(event, relay)
is FileHeaderEvent -> consume(event, relay)
is ProfileGalleryEntryEvent -> consume(event, relay)
is FileServersEvent -> consume(event, relay)
is FileStorageEvent -> consume(event, relay)
is FileStorageHeaderEvent -> consume(event, relay)

View File

@ -139,7 +139,7 @@ open class Note(
var relays = listOf<RelayBriefInfoCache.RelayBriefInfo>()
private set
var headerImage: String? = null
var associatedNote: Note? = null
var lastReactionsDownloadTime: Map<String, EOSETime> = emptyMap()

View File

@ -40,7 +40,6 @@ import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.BookmarkListEvent
import com.vitorpamplona.quartz.events.ChatroomKey
import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.GalleryListEvent
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.MetadataEvent
import com.vitorpamplona.quartz.events.ReportEvent
@ -61,7 +60,6 @@ class User(
var latestMetadataRelay: String? = null
var latestContactList: ContactListEvent? = null
var latestBookmarkList: BookmarkListEvent? = null
var latestGalleryList: GalleryListEvent? = null
var reports = mapOf<User, Set<Note>>()
private set
@ -125,13 +123,6 @@ class User(
liveSet?.innerBookmarks?.invalidateData()
}
fun updateGallery(event: GalleryListEvent) {
if (event.id == latestGalleryList?.id) return
print("GALLERY " + event.id())
latestGalleryList = event
liveSet?.innerGallery?.invalidateData()
}
fun clearEOSE() {
latestEOSEs = emptyMap()
}

View File

@ -31,7 +31,6 @@ import com.vitorpamplona.quartz.events.BadgeAwardEvent
import com.vitorpamplona.quartz.events.BadgeProfilesEvent
import com.vitorpamplona.quartz.events.BookmarkListEvent
import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.GalleryListEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.HighlightEvent
import com.vitorpamplona.quartz.events.LnZapEvent
@ -40,6 +39,7 @@ import com.vitorpamplona.quartz.events.MetadataEvent
import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.events.PinListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.ProfileGalleryEntryEvent
import com.vitorpamplona.quartz.events.RepostEvent
import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.WikiNoteEvent
@ -84,7 +84,7 @@ object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") {
WikiNoteEvent.KIND,
),
authors = listOf(it.pubkeyHex),
limit = 200,
limit = 500,
),
)
}
@ -147,13 +147,27 @@ object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") {
filter =
Filter(
kinds =
listOf(BookmarkListEvent.KIND, PeopleListEvent.KIND, AppRecommendationEvent.KIND, GalleryListEvent.KIND),
listOf(BookmarkListEvent.KIND, PeopleListEvent.KIND, AppRecommendationEvent.KIND),
authors = listOf(it.pubkeyHex),
limit = 100,
),
)
}
fun createProfileGalleryFilter() =
user?.let {
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
Filter(
kinds =
listOf(ProfileGalleryEntryEvent.KIND),
authors = listOf(it.pubkeyHex),
limit = 1000,
),
)
}
fun createReceivedAwardsFilter() =
user?.let {
TypedFilter(
@ -174,6 +188,7 @@ object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") {
listOfNotNull(
createUserInfoFilter(),
createUserPostsFilter(),
createProfileGalleryFilter(),
createFollowFilter(),
createFollowersFilter(),
createUserReceivedZapsFilter(),

View File

@ -24,47 +24,65 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.ammolite.relays.Relay
import com.vitorpamplona.quartz.events.MuteListEvent
import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.events.ProfileGalleryEntryEvent
class UserProfileGalleryFeedFilter(
val user: User,
val account: Account,
) : FeedFilter<Note>() {
override fun feedKey(): String = account.userProfile().pubkeyHex + "-Gallery-" + user.pubkeyHex
) : AdditiveFeedFilter<Note>() {
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + account.defaultStoriesFollowList.value
override fun showHiddenKey(): Boolean =
account.defaultStoriesFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
account.defaultStoriesFollowList.value == MuteListEvent.blockListFor(account.userProfile().pubkeyHex)
override fun feed(): List<Note> {
val params = buildFilterParams(account)
val notes =
user.latestGalleryList
?.taggedGalleryEntries()
?.map {
Triple(
// (
// if (ATag.isATag(it.id)) {
// ATag.parse(it.id, null)?.let { it1 -> LocalCache.getOrCreateAddressableNote(it1) }
// } else {
LocalCache.getOrCreateNote(it.id),
// }
// )!!
it.url,
it.relay,
)
}?.toSet()
?: emptySet()
LocalCache.notes.filterIntoSet { _, it ->
acceptableEvent(it, params, user)
}
var finalnotes = setOf<Note>()
for (pair in notes) {
pair.first.headerImage = pair.second
if (pair.third != null) {
val relay = Relay(pair.third!!, true, false, setOf(FeedType.GLOBAL))
pair.first.createdAt()?.let { user.addRelayBeingUsed(relay, it) }
pair.first.addRelay(relay)
}
finalnotes = finalnotes + pair.first
for (item in notes) {
item.associatedNote = (item.event as ProfileGalleryEntryEvent).event()?.let { LocalCache.getOrCreateNote(it) }
finalnotes = finalnotes + item
}
println(finalnotes)
return (finalnotes)
.filter { account.isAcceptable(it) }
return sort(finalnotes)
}
override fun applyFilter(collection: Set<Note>): Set<Note> = innerApplyFilter(collection)
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
val params = buildFilterParams(account)
return collection.filterTo(HashSet()) { acceptableEvent(it, params, user) }
}
fun acceptableEvent(
it: Note,
params: FilterByListParams,
user: User,
): Boolean {
val noteEvent = it.event
return (
(it.event?.pubKey() == user.pubkeyHex && noteEvent is ProfileGalleryEntryEvent) && noteEvent.hasUrl() && noteEvent.hasEvent() // && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET))
) &&
params.match(noteEvent) &&
account.isAcceptable(it)
}
fun buildFilterParams(account: Account): FilterByListParams =
FilterByListParams.create(
userHex = account.userProfile().pubkeyHex,
selectedListName = account.defaultStoriesFollowList.value,
followLists = account.liveStoriesFollowLists.value,
hiddenUsers = account.flowHiddenUsers.value,
)
override fun sort(collection: Set<Note>): List<Note> = collection.sortedWith(DefaultFeedOrder)
}

View File

@ -671,11 +671,7 @@ fun DeleteFromGalleryDialog(
buttonIcon = Icons.Default.Delete,
buttonText = stringRes(R.string.quick_action_delete_dialog_btn),
onClickDoOnce = {
note.headerImage.let {
if (it != null) {
accountViewModel.removefromMediaGallery(note, it)
}
}
accountViewModel.removefromMediaGallery(note)
onDismiss()
},
onDismiss = onDismiss,

View File

@ -677,11 +677,8 @@ class AccountViewModel(
viewModelScope.launch(Dispatchers.IO) { account.addToGallery(hex, url, relay) }
}
fun removefromMediaGallery(
note: Note,
url: String,
) {
viewModelScope.launch(Dispatchers.IO) { account.removeFromGallery(note, url) }
fun removefromMediaGallery(note: Note) {
viewModelScope.launch(Dispatchers.IO) { account.removeFromGallery(note) }
}
fun addPrivateBookmark(note: Note) {

View File

@ -33,7 +33,6 @@ import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
@ -74,7 +73,7 @@ import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.ProfileGalleryEntryEvent
@Composable
fun RenderGalleryFeed(
@ -134,27 +133,45 @@ private fun GalleryFeedLoaded(
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
val defaultModifier = remember { Modifier.fillMaxWidth().animateItemPlacement() }
Row(defaultModifier) {
GalleryCardCompose(
baseNote = item,
routeForLastRead = routeForLastRead,
modifier = Modifier,
forceEventKind = forceEventKind,
accountViewModel = accountViewModel,
nav = nav,
)
}
if (item.associatedNote != null) {
if (item.associatedNote!!.event != null) {
if ((item.event as ProfileGalleryEntryEvent).hasUrl() &&
(item.event as ProfileGalleryEntryEvent).hasEvent()
) {
val image = (item.event as ProfileGalleryEntryEvent).url()
HorizontalDivider(
thickness = DividerThickness,
)
Row(defaultModifier) {
if (image != null) {
GalleryCardCompose(
galleryNote = item,
image = image,
baseNote = item.associatedNote!!,
routeForLastRead = routeForLastRead,
modifier = Modifier,
forceEventKind = forceEventKind,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
HorizontalDivider(
thickness = DividerThickness,
)
} else {
accountViewModel.delete(item)
}
}
}
}
}
}
@Composable
fun GalleryCardCompose(
galleryNote: Note,
baseNote: Note,
image: String,
routeForLastRead: String? = null,
modifier: Modifier = Modifier,
parentBackgroundColor: MutableState<Color>? = null,
@ -172,8 +189,11 @@ fun GalleryCardCompose(
accountViewModel = accountViewModel,
nav = nav,
) { canPreview ->
GalleryCard(
galleryNote = galleryNote,
baseNote = baseNote,
image = image,
modifier = modifier,
parentBackgroundColor = parentBackgroundColor,
accountViewModel = accountViewModel,
@ -185,16 +205,19 @@ fun GalleryCardCompose(
@Composable
fun GalleryCard(
galleryNote: Note,
baseNote: Note,
image: String,
modifier: Modifier = Modifier,
parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
// baseNote.event?.let { Text(text = it.pubKey()) }
LongPressToQuickActionGallery(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
LongPressToQuickActionGallery(baseNote = galleryNote, accountViewModel = accountViewModel) { showPopup ->
CheckNewAndRenderChannelCard(
baseNote,
image,
modifier,
parentBackgroundColor,
accountViewModel,
@ -207,6 +230,7 @@ fun GalleryCard(
@Composable
private fun CheckNewAndRenderChannelCard(
baseNote: Note,
image: String,
modifier: Modifier = Modifier,
parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel,
@ -228,26 +252,14 @@ private fun CheckNewAndRenderChannelCard(
showPopup = showPopup,
nav = nav,
) {
InnerGalleryCardWithReactions(
baseNote = baseNote,
accountViewModel = accountViewModel,
nav = nav,
)
InnerGalleryCardBox(baseNote, image, accountViewModel, nav)
}
}
@Composable
fun InnerGalleryCardWithReactions(
baseNote: Note,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
InnerGalleryCardBox(baseNote, accountViewModel, nav)
}
@Composable
fun InnerGalleryCardBox(
baseNote: Note,
image: String,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
@ -256,7 +268,7 @@ fun InnerGalleryCardBox(
note = baseNote,
accountViewModel = accountViewModel,
) {
RenderGalleryThumb(baseNote, accountViewModel, nav)
RenderGalleryThumb(baseNote, image, accountViewModel, nav)
}
}
}
@ -272,21 +284,19 @@ data class GalleryThumb(
@Composable
fun RenderGalleryThumb(
baseNote: Note,
image: String,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
val noteEvent = baseNote.event as? TextNoteEvent ?: return
val card by
baseNote
.live()
.metadata
.map {
val noteEvent = baseNote.event as TextNoteEvent
GalleryThumb(
id = "",
image = baseNote.headerImage,
title = noteEvent.content(),
image = image,
title = "",
// noteEvent?.title(),
// price = noteEvent?.price(),
)
@ -294,11 +304,8 @@ fun RenderGalleryThumb(
.observeAsState(
GalleryThumb(
id = "",
image = baseNote.headerImage,
title = noteEvent.content(),
// image = noteEvent.image(),
// title = noteEvent.title(),
// price = noteEvent.price(),
image = image,
title = "",
),
)

View File

@ -119,11 +119,6 @@ open class Event(
override fun taggedEvents() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] }
override fun taggedGalleryEntries() =
tags.filter { it.size > 2 && it[0] == GalleryListEvent.GALLERYTAGNAME }.map {
GalleryListEvent.GalleryUrl(it[1], it[2], it.getOrNull(3))
}
override fun taggedUrls() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] }
override fun firstTagFor(vararg key: String) = tags.firstOrNull { it.size > 1 && it[0] in key }?.let { it[1] }

View File

@ -85,6 +85,7 @@ class EventFactory {
EmojiPackSelectionEvent.KIND ->
EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig)
FileHeaderEvent.KIND -> FileHeaderEvent(id, pubKey, createdAt, tags, content, sig)
ProfileGalleryEntryEvent.KIND -> ProfileGalleryEntryEvent(id, pubKey, createdAt, tags, content, sig)
FileServersEvent.KIND -> FileServersEvent(id, pubKey, createdAt, tags, content, sig)
FileStorageEvent.KIND -> FileStorageEvent(id, pubKey, createdAt, tags, content, sig)
FileStorageHeaderEvent.KIND ->
@ -97,7 +98,6 @@ class EventFactory {
GitPatchEvent.KIND -> GitPatchEvent(id, pubKey, createdAt, tags, content, sig)
GitRepositoryEvent.KIND -> GitRepositoryEvent(id, pubKey, createdAt, tags, content, sig)
GoalEvent.KIND -> GoalEvent(id, pubKey, createdAt, tags, content, sig)
GalleryListEvent.KIND -> GalleryListEvent(id, pubKey, createdAt, tags, content, sig)
HighlightEvent.KIND -> HighlightEvent(id, pubKey, createdAt, tags, content, sig)
HTTPAuthorizationEvent.KIND ->
HTTPAuthorizationEvent(id, pubKey, createdAt, tags, content, sig)

View File

@ -145,8 +145,6 @@ interface EventInterface {
fun firstTaggedK(): Int?
fun taggedGalleryEntries(): List<GalleryListEvent.GalleryUrl>
fun taggedEmojis(): List<EmojiUrl>
fun matchTag1With(text: String): Boolean

View File

@ -0,0 +1,130 @@
/**
* 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.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.signers.NostrSigner
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
class ProfileGalleryEntryEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
fun url() = tags.firstOrNull { it.size > 1 && it[0] == URL }?.get(1)
fun urls() = tags.filter { it.size > 1 && it[0] == URL }.map { it[1] }
fun encryptionKey() = tags.firstOrNull { it.size > 2 && it[0] == ENCRYPTION_KEY }?.let { AESGCM(it[1], it[2]) }
fun mimeType() = tags.firstOrNull { it.size > 1 && it[0] == MIME_TYPE }?.get(1)
fun hash() = tags.firstOrNull { it.size > 1 && it[0] == HASH }?.get(1)
fun size() = tags.firstOrNull { it.size > 1 && it[0] == FILE_SIZE }?.get(1)
fun alt() = tags.firstOrNull { it.size > 1 && it[0] == ALT }?.get(1)
fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)
fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.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 hasUrl() = tags.any { it.size > 1 && it[0] == URL }
fun event() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1)
fun hasEvent() = tags.any { it.size > 1 && it[0] == "e" }
fun isOneOf(mimeTypes: Set<String>) = tags.any { it.size > 1 && it[0] == MIME_TYPE && mimeTypes.contains(it[1]) }
companion object {
const val KIND = 1163
const val ALT_DESCRIPTION = "Profile Gallery Entry"
const val URL = "url"
const val ENCRYPTION_KEY = "aes-256-gcm"
const val MIME_TYPE = "m"
const val FILE_SIZE = "size"
const val DIMENSION = "dim"
const val HASH = "x"
const val MAGNET_URI = "magnet"
const val TORRENT_INFOHASH = "i"
const val BLUR_HASH = "blurhash"
const val ORIGINAL_HASH = "ox"
const val ALT = "alt"
fun create(
url: String,
eventid: String? = null,
magnetUri: String? = null,
mimeType: String? = null,
alt: String? = null,
hash: String? = null,
size: String? = null,
dimensions: String? = null,
blurhash: String? = null,
originalHash: String? = null,
magnetURI: String? = null,
torrentInfoHash: String? = null,
encryptionKey: AESGCM? = null,
sensitiveContent: Boolean? = null,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (ProfileGalleryEntryEvent) -> Unit,
) {
val tags =
listOfNotNull(
arrayOf(URL, url),
eventid?.let { arrayOf("e", it) },
magnetUri?.let { arrayOf(MAGNET_URI, it) },
mimeType?.let { arrayOf(MIME_TYPE, it) },
alt?.ifBlank { null }?.let { arrayOf(ALT, it) } ?: arrayOf("alt", ALT_DESCRIPTION),
hash?.let { arrayOf(HASH, it) },
size?.let { arrayOf(FILE_SIZE, it) },
dimensions?.let { arrayOf(DIMENSION, it) },
blurhash?.let { arrayOf(BLUR_HASH, it) },
originalHash?.let { arrayOf(ORIGINAL_HASH, it) },
magnetURI?.let { arrayOf(MAGNET_URI, it) },
torrentInfoHash?.let { arrayOf(TORRENT_INFOHASH, it) },
encryptionKey?.let { arrayOf(ENCRYPTION_KEY, it.key, it.nonce) },
sensitiveContent?.let {
if (it) {
arrayOf("content-warning", "")
} else {
null
}
},
)
val content = alt ?: ""
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady)
}
}
}