Massive refactoring of Quartz to:

- Define each tag in their own class.
- Allow extension functions to additional responsibilities to other classes
- Migrate from hardcoded tag filters in events to the Tag's parser and assemble functions.
- Migrate hardcoded event.create to builders that use extension functions
- Restructures threading infrastructure for NIP-10
- Decouple the event signing from the Event building functions via event templates
- Create classes to represent Tags and TagArrays and use extension functions to add domain-related methods to the tag array of each nip.
- Uses external functions on event template builders to better point to which functions and which tags can be used in which event kinds.
- Separates Event kinds in packages inside each nip.
- Improves support for NIP-89
- Correctly establishes which imeta params can be used in each nip (video, picture, files)
- Decouples the iMeta builder from any nip.
- Fixes mute list word and user removal when inserted from a different client.
- Migrates the Account class to avoiding having to build each Event inside of it
This commit is contained in:
Vitor Pamplona
2025-02-13 17:39:13 -05:00
parent 0f3b3c4b2d
commit 9c21a8a7da
614 changed files with 15380 additions and 7132 deletions
@@ -21,13 +21,13 @@
package com.vitorpamplona.amethyst.commons.richtext
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip94FileMetadata.Dimension
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
import java.io.File
@Immutable
abstract class BaseMediaContent(
val description: String? = null,
val dim: Dimension? = null,
val dim: DimensionTag? = null,
val blurhash: String? = null,
)
@@ -36,7 +36,7 @@ abstract class MediaUrlContent(
val url: String,
description: String? = null,
val hash: String? = null,
dim: Dimension? = null,
dim: DimensionTag? = null,
blurhash: String? = null,
val uri: String? = null,
val mimeType: String? = null,
@@ -48,7 +48,7 @@ open class MediaUrlImage(
description: String? = null,
hash: String? = null,
blurhash: String? = null,
dim: Dimension? = null,
dim: DimensionTag? = null,
uri: String? = null,
val contentWarning: String? = null,
mimeType: String? = null,
@@ -59,7 +59,7 @@ class EncryptedMediaUrlImage(
description: String? = null,
hash: String? = null,
blurhash: String? = null,
dim: Dimension? = null,
dim: DimensionTag? = null,
uri: String? = null,
contentWarning: String? = null,
mimeType: String? = null,
@@ -73,7 +73,7 @@ open class MediaUrlVideo(
url: String,
description: String? = null,
hash: String? = null,
dim: Dimension? = null,
dim: DimensionTag? = null,
uri: String? = null,
val artworkUri: String? = null,
val authorName: String? = null,
@@ -87,7 +87,7 @@ class EncryptedMediaUrlVideo(
url: String,
description: String? = null,
hash: String? = null,
dim: Dimension? = null,
dim: DimensionTag? = null,
uri: String? = null,
artworkUri: String? = null,
authorName: String? = null,
@@ -105,7 +105,7 @@ abstract class MediaPreloadedContent(
description: String? = null,
val mimeType: String? = null,
val isVerified: Boolean? = null,
dim: Dimension? = null,
dim: DimensionTag? = null,
blurhash: String? = null,
val uri: String,
val id: String? = null,
@@ -118,7 +118,7 @@ class MediaLocalImage(
localFile: File?,
mimeType: String? = null,
description: String? = null,
dim: Dimension? = null,
dim: DimensionTag? = null,
blurhash: String? = null,
isVerified: Boolean? = null,
uri: String,
@@ -129,7 +129,7 @@ class MediaLocalVideo(
localFile: File?,
mimeType: String? = null,
description: String? = null,
dim: Dimension? = null,
dim: DimensionTag? = null,
blurhash: String? = null,
isVerified: Boolean? = null,
uri: String,
@@ -27,10 +27,14 @@ import com.linkedin.urls.detection.UrlDetectorOptions
import com.vitorpamplona.quartz.experimental.inlineMetadata.Nip54InlineMetadata
import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists
import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji
import com.vitorpamplona.quartz.nip36SensitiveContent.CONTENT_WARNING
import com.vitorpamplona.quartz.nip92IMeta.Nip92MediaAttachments
import com.vitorpamplona.quartz.nip94FileMetadata.Dimension
import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningTag
import com.vitorpamplona.quartz.nip92IMeta.IMetaTag
import com.vitorpamplona.quartz.nip92IMeta.imetasByUrl
import com.vitorpamplona.quartz.nip94FileMetadata.tags.BlurhashTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@@ -46,14 +50,15 @@ import kotlin.coroutines.cancellation.CancellationException
class RichTextParser {
fun createMediaContent(
fullUrl: String,
eventTags: ImmutableListOfLists<String>,
eventTags: Map<String, IMetaTag>,
description: String?,
callbackUri: String? = null,
): MediaUrlContent? {
val frags = Nip54InlineMetadata().parse(fullUrl)
val tags = Nip92MediaAttachments.parse(fullUrl, eventTags.lists)
val contentType = frags[FileHeaderEvent.MIME_TYPE] ?: tags[FileHeaderEvent.MIME_TYPE]
val tags = eventTags.get(fullUrl)?.properties ?: emptyMap()
val contentType = frags[MimeTypeTag.TAG_NAME] ?: tags[MimeTypeTag.TAG_NAME]?.firstOrNull()
val isImage: Boolean
val isVideo: Boolean
@@ -73,22 +78,22 @@ class RichTextParser {
return if (isImage) {
MediaUrlImage(
url = fullUrl,
description = description ?: frags[FileHeaderEvent.ALT] ?: tags[FileHeaderEvent.ALT],
hash = frags[FileHeaderEvent.HASH] ?: tags[FileHeaderEvent.HASH],
blurhash = frags[FileHeaderEvent.BLUR_HASH] ?: tags[FileHeaderEvent.BLUR_HASH],
dim = frags[FileHeaderEvent.DIMENSION]?.let { Dimension.parse(it) } ?: tags[FileHeaderEvent.DIMENSION]?.let { Dimension.parse(it) },
contentWarning = frags[CONTENT_WARNING] ?: tags[CONTENT_WARNING],
description = description ?: frags[AltTag.TAG_NAME] ?: tags[AltTag.TAG_NAME]?.firstOrNull(),
hash = frags[HashTag.TAG_NAME] ?: tags[HashTag.TAG_NAME]?.firstOrNull(),
blurhash = frags[BlurhashTag.TAG_NAME] ?: tags[BlurhashTag.TAG_NAME]?.firstOrNull(),
dim = frags[DimensionTag.TAG_NAME]?.let { DimensionTag.parse(it) } ?: tags[DimensionTag.TAG_NAME]?.firstOrNull()?.let { DimensionTag.parse(it) },
contentWarning = frags[ContentWarningTag.TAG_NAME] ?: tags[ContentWarningTag.TAG_NAME]?.firstOrNull(),
uri = callbackUri,
mimeType = contentType,
)
} else if (isVideo) {
MediaUrlVideo(
url = fullUrl,
description = description ?: frags[FileHeaderEvent.ALT] ?: tags[FileHeaderEvent.ALT],
hash = frags[FileHeaderEvent.HASH] ?: tags[FileHeaderEvent.HASH],
blurhash = frags[FileHeaderEvent.BLUR_HASH] ?: tags[FileHeaderEvent.BLUR_HASH],
dim = frags[FileHeaderEvent.DIMENSION]?.let { Dimension.parse(it) } ?: tags[FileHeaderEvent.DIMENSION]?.let { Dimension.parse(it) },
contentWarning = frags[CONTENT_WARNING] ?: tags[CONTENT_WARNING],
description = description ?: frags[AltTag.TAG_NAME] ?: tags[AltTag.TAG_NAME]?.firstOrNull(),
hash = frags[HashTag.TAG_NAME] ?: tags[HashTag.TAG_NAME]?.firstOrNull(),
blurhash = frags[BlurhashTag.TAG_NAME] ?: tags[BlurhashTag.TAG_NAME]?.firstOrNull(),
dim = frags[DimensionTag.TAG_NAME]?.let { DimensionTag.parse(it) } ?: tags[DimensionTag.TAG_NAME]?.firstOrNull()?.let { DimensionTag.parse(it) },
contentWarning = frags[ContentWarningTag.TAG_NAME] ?: tags[ContentWarningTag.TAG_NAME]?.firstOrNull(),
uri = callbackUri,
mimeType = contentType,
)
@@ -131,10 +136,11 @@ class RichTextParser {
tags: ImmutableListOfLists<String>,
callbackUri: String?,
): RichTextViewerState {
val imetas = tags.lists.imetasByUrl()
val urlSet = parseValidUrls(content)
val imagesForPager =
urlSet.mapNotNull { fullUrl -> createMediaContent(fullUrl, tags, content, callbackUri) }.associateBy { it.url }
urlSet.mapNotNull { fullUrl -> createMediaContent(fullUrl, imetas, content, callbackUri) }.associateBy { it.url }
val emojiMap = CustomEmoji.createEmojiMap(tags)
@@ -145,7 +151,7 @@ class RichTextParser {
val imagesForPagerWithBase64 =
imagesForPager +
base64Images
.mapNotNull { createMediaContent(it.segmentText, tags, content, callbackUri) }
.mapNotNull { createMediaContent(it.segmentText, emptyMap(), content, callbackUri) }
.associateBy { it.url }
return RichTextViewerState(