Adds support for nip54

This commit is contained in:
Vitor Pamplona
2023-12-11 12:51:53 -05:00
parent 49e1da8812
commit 3d225423ef
12 changed files with 143 additions and 22 deletions
@@ -11,6 +11,7 @@ import com.vitorpamplona.amethyst.ui.components.ZoomableUrlImage
import com.vitorpamplona.amethyst.ui.components.ZoomableUrlVideo
import com.vitorpamplona.amethyst.ui.components.hashTagsPattern
import com.vitorpamplona.amethyst.ui.components.imageExtensions
import com.vitorpamplona.amethyst.ui.components.removeQueryParams
import com.vitorpamplona.amethyst.ui.components.tagIndex
import com.vitorpamplona.amethyst.ui.components.videoExtensions
import com.vitorpamplona.quartz.events.ImmutableListOfLists
@@ -21,6 +22,8 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.collections.immutable.toImmutableSet
import java.net.URI
import java.net.URLDecoder
import java.util.regex.Pattern
@Immutable
@@ -85,11 +88,26 @@ class RichTextParser() {
}
val imagesForPager = urlSet.mapNotNull { fullUrl ->
val removedParamsFromUrl = fullUrl.split("?")[0].lowercase()
val removedParamsFromUrl = removeQueryParams(fullUrl)
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
ZoomableUrlImage(fullUrl)
val frags = URI(fullUrl).fragments()
println("Image $fullUrl $frags")
ZoomableUrlImage(
url = fullUrl,
description = frags["alt"]?.let { URLDecoder.decode(it, "UTF-8") },
hash = frags["x"]?.let { URLDecoder.decode(it, "UTF-8") },
blurhash = frags["blurhash"]?.let { URLDecoder.decode(it, "UTF-8") },
dim = frags["dim"]?.let { URLDecoder.decode(it, "UTF-8") }
)
} else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) {
ZoomableUrlVideo(fullUrl)
val frags = URI(fullUrl).fragments()
ZoomableUrlVideo(
url = fullUrl,
description = frags["alt"]?.let { URLDecoder.decode(it, "UTF-8") },
hash = frags["x"]?.let { URLDecoder.decode(it, "UTF-8") },
dim = frags["blurhash"]?.let { URLDecoder.decode(it, "UTF-8") },
uri = frags["dim"]?.let { URLDecoder.decode(it, "UTF-8") }
)
} else {
null
}
@@ -110,6 +128,16 @@ class RichTextParser() {
)
}
private fun URI.fragments(): Map<String, String> {
if (rawFragment == null) return emptyMap()
return rawFragment.split('&').associate {
val parts = it.split('=')
val name = parts.firstOrNull() ?: ""
val value = parts.getOrNull(1) ?: ""
Pair(name, value)
}
}
private fun findTextSegments(content: String, images: Set<String>, urls: Set<String>, emojis: Map<String, String>, tags: ImmutableListOfLists<String>): ImmutableList<ParagraphState> {
var paragraphSegments = persistentListOf<ParagraphState>()
@@ -460,8 +460,14 @@ fun NewPostView(
if (myUrlPreview != null) {
Row(modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
if (isValidURL(myUrlPreview)) {
val removedParamsFromUrl =
val removedParamsFromUrl = if (myUrlPreview.contains("?")) {
myUrlPreview.split("?")[0].lowercase()
} else if (myUrlPreview.contains("#")) {
myUrlPreview.split("#")[0].lowercase()
} else {
myUrlPreview
}
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
AsyncImage(
model = myUrlPreview,
@@ -48,6 +48,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.launch
import java.net.URLEncoder
enum class UserSuggestionAnchor {
MAIN_MESSAGE,
@@ -376,7 +377,7 @@ open class NewPostViewModel() : ViewModel() {
onReady = { fileUri, contentType, size ->
if (server.isNip95) {
contentResolver.openInputStream(fileUri)?.use {
createNIP95Record(it.readBytes(), contentType, alt, sensitiveContent, relayList = relayList)
createNIP95Record(it.readBytes(), contentType, alt, sensitiveContent)
}
} else {
viewModelScope.launch(Dispatchers.IO) {
@@ -400,11 +401,12 @@ open class NewPostViewModel() : ViewModel() {
sensitiveContent = sensitiveContent
)
} else {
val url = result.tags?.firstOrNull() { it.size > 1 && it[0] == "url" }?.get(1)
isUploadingImage = false
message = TextFieldValue(message.text + "\n" + url)
urlPreview = findUrlInMessage()
noNIP94(
uploadingResult = result,
localContentType = contentType,
alt = alt,
sensitiveContent = sensitiveContent
)
}
} catch (e: Exception) {
Log.e(
@@ -652,7 +654,8 @@ open class NewPostViewModel() : ViewModel() {
account?.createHeader(imageUrl, magnet, header, alt, sensitiveContent, originalHash) { event ->
isUploadingImage = false
nip94attachments = nip94attachments + event
message = TextFieldValue(message.text + "\n" + imageUrl)
val contentWarning = if (sensitiveContent) "" else null
message = TextFieldValue(message.text + "\n" + addInlineMetadataAsNIP54(imageUrl, header.dim, header.mimeType, alt, header.blurHash, header.hash, contentWarning))
urlPreview = findUrlInMessage()
}
},
@@ -665,12 +668,76 @@ open class NewPostViewModel() : ViewModel() {
)
}
suspend fun noNIP94(
uploadingResult: Nip96Uploader.PartialEvent,
localContentType: String?,
alt: String?,
sensitiveContent: Boolean
) {
// Images don't seem to be ready immediately after upload
val imageUrl = uploadingResult.tags?.firstOrNull() { it.size > 1 && it[0] == "url" }?.get(1)
val remoteMimeType = uploadingResult.tags?.firstOrNull() { it.size > 1 && it[0] == "m" }?.get(1)?.ifBlank { null }
val dim = uploadingResult.tags?.firstOrNull() { it.size > 1 && it[0] == "dim" }?.get(1)?.ifBlank { null }
if (imageUrl.isNullOrBlank()) {
Log.e("ImageDownload", "Couldn't download image from server")
cancel()
isUploadingImage = false
viewModelScope.launch {
imageUploadingError.emit("Server failed to return a url")
}
return
}
FileHeader.prepare(
fileUrl = imageUrl,
mimeType = remoteMimeType ?: localContentType,
dimPrecomputed = dim,
onReady = { header: FileHeader ->
isUploadingImage = false
val contentWarning = if (sensitiveContent) "" else null
message = TextFieldValue(message.text + "\n" + addInlineMetadataAsNIP54(imageUrl, header.dim, header.mimeType, alt, header.blurHash, header.hash, contentWarning))
urlPreview = findUrlInMessage()
},
onError = {
isUploadingImage = false
viewModelScope.launch {
imageUploadingError.emit("Failed to upload the image / video")
}
}
)
}
fun addInlineMetadataAsNIP54(
imageUrl: String,
dim: String?,
m: String?,
alt: String?,
blurHash: String?,
x: String?,
sensitiveContent: String?
): String {
val extension = listOfNotNull(
m?.ifBlank { null }?.let { "m=${URLEncoder.encode(it, "utf-8")}" },
dim?.ifBlank { null }?.let { "dim=${URLEncoder.encode(it, "utf-8")}" },
alt?.ifBlank { null }?.let { "alt=${URLEncoder.encode(it, "utf-8")}" },
blurHash?.ifBlank { null }?.let { "blurhash=${URLEncoder.encode(it, "utf-8")}" },
x?.ifBlank { null }?.let { "x=${URLEncoder.encode(it, "utf-8")}" },
sensitiveContent?.ifBlank { null }?.let { "content-warning=${URLEncoder.encode(it, "utf-8")}" }
).joinToString("&")
return if (imageUrl.contains("#")) {
"$imageUrl&$extension"
} else {
"$imageUrl#$extension"
}
}
fun createNIP95Record(
bytes: ByteArray,
mimeType: String?,
alt: String?,
sensitiveContent: Boolean,
relayList: List<Relay>? = null
sensitiveContent: Boolean
) {
viewModelScope.launch(Dispatchers.IO) {
FileHeader.prepare(
@@ -93,7 +93,7 @@ class MarkdownParser {
content.split('\n').forEach { paragraph ->
paragraph.split(' ').forEach { word: String ->
if (isValidURL(word)) {
val removedParamsFromUrl = word.split("?")[0].lowercase()
val removedParamsFromUrl = removeQueryParams(word)
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
returnContent += "![]($word) "
} else {
@@ -95,6 +95,16 @@ val videoExtensions = listOf("mp4", "avi", "wmv", "mpg", "amv", "webm", "mov", "
val tagIndex = Pattern.compile("\\#\\[([0-9]+)\\](.*)")
val hashTagsPattern: Pattern = Pattern.compile("#([^\\s!@#\$%^&*()=+./,\\[{\\]};:'\"?><]+)(.*)", Pattern.CASE_INSENSITIVE)
fun removeQueryParams(fullUrl: String): String {
return if (fullUrl.contains("?")) {
fullUrl.split("?")[0].lowercase()
} else if (fullUrl.contains("#")) {
fullUrl.split("#")[0].lowercase()
} else {
fullUrl
}
}
fun isValidURL(url: String?): Boolean {
return try {
URL(url).toURI()
@@ -182,7 +182,7 @@ class ZoomableLocalVideo(
) : ZoomablePreloadedContent(localFile, description, mimeType, isVerified, dim, uri)
fun figureOutMimeType(fullUrl: String): ZoomableContent {
val removedParamsFromUrl = fullUrl.split("?")[0].lowercase()
val removedParamsFromUrl = removeQueryParams(fullUrl)
val isImage = imageExtensions.any { removedParamsFromUrl.endsWith(it) }
val isVideo = videoExtensions.any { removedParamsFromUrl.endsWith(it) }
@@ -389,6 +389,7 @@ private fun UrlImageView(
}
if (showImage.value) {
/*
AsyncImage(
model = content.url,
contentDescription = content.description,
@@ -398,6 +399,8 @@ private fun UrlImageView(
painterState.value = it
}
)
*/
}
AddedImageFeatures(
@@ -113,6 +113,7 @@ import com.vitorpamplona.amethyst.ui.components.ZoomableUrlImage
import com.vitorpamplona.amethyst.ui.components.ZoomableUrlVideo
import com.vitorpamplona.amethyst.ui.components.figureOutMimeType
import com.vitorpamplona.amethyst.ui.components.imageExtensions
import com.vitorpamplona.amethyst.ui.components.removeQueryParams
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
@@ -3454,7 +3455,7 @@ fun FileHeaderDisplay(note: Note, roundedCorner: Boolean, accountViewModel: Acco
val hash = event.hash()
val dimensions = event.dimensions()
val description = event.alt() ?: event.content
val isImage = imageExtensions.any { fullUrl.split("?")[0].lowercase().endsWith(it) }
val isImage = imageExtensions.any { removeQueryParams(fullUrl).lowercase().endsWith(it) }
val uri = note.toNostrUri()
mutableStateOf<ZoomableContent>(
@@ -63,7 +63,7 @@ class ChannelMessageEvent(
}
nip94attachments?.let {
it.forEach {
tags.add(arrayOf("nip94", it.toJson()))
//tags.add(arrayOf("nip94", it.toJson()))
}
}
@@ -119,7 +119,13 @@ class ClassifiedsEvent(
tags.add(arrayOf("zap", it.lnAddressOrPubKeyHex, it.relay ?: "", it.weight.toString()))
}
findURLs(message).forEach {
val removedParamsFromUrl = it.split("?")[0].lowercase()
val removedParamsFromUrl = if (it.contains("?"))
it.split("?")[0].lowercase()
else if (it.contains("#"))
it.split("#")[0].lowercase()
else
it
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
tags.add(arrayOf("image", it))
}
@@ -136,7 +142,7 @@ class ClassifiedsEvent(
}
nip94attachments?.let {
it.forEach {
tags.add(arrayOf("nip94", it.toJson()))
//tags.add(arrayOf("nip94", it.toJson()))
}
}
@@ -83,7 +83,7 @@ class LiveActivitiesChatMessageEvent(
}
nip94attachments?.let {
it.forEach {
tags.add(arrayOf("nip94", it.toJson()))
//tags.add(arrayOf("nip94", it.toJson()))
}
}
@@ -102,7 +102,7 @@ class PollNoteEvent(
}
nip94attachments?.let {
it.forEach {
tags.add(arrayOf("nip94", it.toJson()))
//tags.add(arrayOf("nip94", it.toJson()))
}
}
@@ -99,7 +99,7 @@ class TextNoteEvent(
}
nip94attachments?.let {
it.forEach {
tags.add(arrayOf("nip94", it.toJson()))
//tags.add(arrayOf("nip94", it.toJson()))
}
}