Adds support for nip54
This commit is contained in:
@@ -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 += " "
|
||||
} 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()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user