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.ZoomableUrlVideo
|
||||||
import com.vitorpamplona.amethyst.ui.components.hashTagsPattern
|
import com.vitorpamplona.amethyst.ui.components.hashTagsPattern
|
||||||
import com.vitorpamplona.amethyst.ui.components.imageExtensions
|
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.tagIndex
|
||||||
import com.vitorpamplona.amethyst.ui.components.videoExtensions
|
import com.vitorpamplona.amethyst.ui.components.videoExtensions
|
||||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||||
@@ -21,6 +22,8 @@ import kotlinx.collections.immutable.persistentListOf
|
|||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableMap
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
import kotlinx.collections.immutable.toImmutableSet
|
import kotlinx.collections.immutable.toImmutableSet
|
||||||
|
import java.net.URI
|
||||||
|
import java.net.URLDecoder
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@@ -85,11 +88,26 @@ class RichTextParser() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val imagesForPager = urlSet.mapNotNull { fullUrl ->
|
val imagesForPager = urlSet.mapNotNull { fullUrl ->
|
||||||
val removedParamsFromUrl = fullUrl.split("?")[0].lowercase()
|
val removedParamsFromUrl = removeQueryParams(fullUrl)
|
||||||
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
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) }) {
|
} 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 {
|
} else {
|
||||||
null
|
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> {
|
private fun findTextSegments(content: String, images: Set<String>, urls: Set<String>, emojis: Map<String, String>, tags: ImmutableListOfLists<String>): ImmutableList<ParagraphState> {
|
||||||
var paragraphSegments = persistentListOf<ParagraphState>()
|
var paragraphSegments = persistentListOf<ParagraphState>()
|
||||||
|
|
||||||
|
|||||||
@@ -460,8 +460,14 @@ fun NewPostView(
|
|||||||
if (myUrlPreview != null) {
|
if (myUrlPreview != null) {
|
||||||
Row(modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
|
Row(modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
|
||||||
if (isValidURL(myUrlPreview)) {
|
if (isValidURL(myUrlPreview)) {
|
||||||
val removedParamsFromUrl =
|
val removedParamsFromUrl = if (myUrlPreview.contains("?")) {
|
||||||
myUrlPreview.split("?")[0].lowercase()
|
myUrlPreview.split("?")[0].lowercase()
|
||||||
|
} else if (myUrlPreview.contains("#")) {
|
||||||
|
myUrlPreview.split("#")[0].lowercase()
|
||||||
|
} else {
|
||||||
|
myUrlPreview
|
||||||
|
}
|
||||||
|
|
||||||
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = myUrlPreview,
|
model = myUrlPreview,
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.net.URLEncoder
|
||||||
|
|
||||||
enum class UserSuggestionAnchor {
|
enum class UserSuggestionAnchor {
|
||||||
MAIN_MESSAGE,
|
MAIN_MESSAGE,
|
||||||
@@ -376,7 +377,7 @@ open class NewPostViewModel() : ViewModel() {
|
|||||||
onReady = { fileUri, contentType, size ->
|
onReady = { fileUri, contentType, size ->
|
||||||
if (server.isNip95) {
|
if (server.isNip95) {
|
||||||
contentResolver.openInputStream(fileUri)?.use {
|
contentResolver.openInputStream(fileUri)?.use {
|
||||||
createNIP95Record(it.readBytes(), contentType, alt, sensitiveContent, relayList = relayList)
|
createNIP95Record(it.readBytes(), contentType, alt, sensitiveContent)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
@@ -400,11 +401,12 @@ open class NewPostViewModel() : ViewModel() {
|
|||||||
sensitiveContent = sensitiveContent
|
sensitiveContent = sensitiveContent
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
val url = result.tags?.firstOrNull() { it.size > 1 && it[0] == "url" }?.get(1)
|
noNIP94(
|
||||||
|
uploadingResult = result,
|
||||||
isUploadingImage = false
|
localContentType = contentType,
|
||||||
message = TextFieldValue(message.text + "\n" + url)
|
alt = alt,
|
||||||
urlPreview = findUrlInMessage()
|
sensitiveContent = sensitiveContent
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(
|
Log.e(
|
||||||
@@ -652,7 +654,8 @@ open class NewPostViewModel() : ViewModel() {
|
|||||||
account?.createHeader(imageUrl, magnet, header, alt, sensitiveContent, originalHash) { event ->
|
account?.createHeader(imageUrl, magnet, header, alt, sensitiveContent, originalHash) { event ->
|
||||||
isUploadingImage = false
|
isUploadingImage = false
|
||||||
nip94attachments = nip94attachments + event
|
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()
|
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(
|
fun createNIP95Record(
|
||||||
bytes: ByteArray,
|
bytes: ByteArray,
|
||||||
mimeType: String?,
|
mimeType: String?,
|
||||||
alt: String?,
|
alt: String?,
|
||||||
sensitiveContent: Boolean,
|
sensitiveContent: Boolean
|
||||||
relayList: List<Relay>? = null
|
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
FileHeader.prepare(
|
FileHeader.prepare(
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ class MarkdownParser {
|
|||||||
content.split('\n').forEach { paragraph ->
|
content.split('\n').forEach { paragraph ->
|
||||||
paragraph.split(' ').forEach { word: String ->
|
paragraph.split(' ').forEach { word: String ->
|
||||||
if (isValidURL(word)) {
|
if (isValidURL(word)) {
|
||||||
val removedParamsFromUrl = word.split("?")[0].lowercase()
|
val removedParamsFromUrl = removeQueryParams(word)
|
||||||
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
||||||
returnContent += " "
|
returnContent += " "
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -95,6 +95,16 @@ val videoExtensions = listOf("mp4", "avi", "wmv", "mpg", "amv", "webm", "mov", "
|
|||||||
val tagIndex = Pattern.compile("\\#\\[([0-9]+)\\](.*)")
|
val tagIndex = Pattern.compile("\\#\\[([0-9]+)\\](.*)")
|
||||||
val hashTagsPattern: Pattern = Pattern.compile("#([^\\s!@#\$%^&*()=+./,\\[{\\]};:'\"?><]+)(.*)", Pattern.CASE_INSENSITIVE)
|
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 {
|
fun isValidURL(url: String?): Boolean {
|
||||||
return try {
|
return try {
|
||||||
URL(url).toURI()
|
URL(url).toURI()
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ class ZoomableLocalVideo(
|
|||||||
) : ZoomablePreloadedContent(localFile, description, mimeType, isVerified, dim, uri)
|
) : ZoomablePreloadedContent(localFile, description, mimeType, isVerified, dim, uri)
|
||||||
|
|
||||||
fun figureOutMimeType(fullUrl: String): ZoomableContent {
|
fun figureOutMimeType(fullUrl: String): ZoomableContent {
|
||||||
val removedParamsFromUrl = fullUrl.split("?")[0].lowercase()
|
val removedParamsFromUrl = removeQueryParams(fullUrl)
|
||||||
val isImage = imageExtensions.any { removedParamsFromUrl.endsWith(it) }
|
val isImage = imageExtensions.any { removedParamsFromUrl.endsWith(it) }
|
||||||
val isVideo = videoExtensions.any { removedParamsFromUrl.endsWith(it) }
|
val isVideo = videoExtensions.any { removedParamsFromUrl.endsWith(it) }
|
||||||
|
|
||||||
@@ -389,6 +389,7 @@ private fun UrlImageView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showImage.value) {
|
if (showImage.value) {
|
||||||
|
/*
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = content.url,
|
model = content.url,
|
||||||
contentDescription = content.description,
|
contentDescription = content.description,
|
||||||
@@ -398,6 +399,8 @@ private fun UrlImageView(
|
|||||||
painterState.value = it
|
painterState.value = it
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
AddedImageFeatures(
|
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.ZoomableUrlVideo
|
||||||
import com.vitorpamplona.amethyst.ui.components.figureOutMimeType
|
import com.vitorpamplona.amethyst.ui.components.figureOutMimeType
|
||||||
import com.vitorpamplona.amethyst.ui.components.imageExtensions
|
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.equalImmutableLists
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
|
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 hash = event.hash()
|
||||||
val dimensions = event.dimensions()
|
val dimensions = event.dimensions()
|
||||||
val description = event.alt() ?: event.content
|
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()
|
val uri = note.toNostrUri()
|
||||||
|
|
||||||
mutableStateOf<ZoomableContent>(
|
mutableStateOf<ZoomableContent>(
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class ChannelMessageEvent(
|
|||||||
}
|
}
|
||||||
nip94attachments?.let {
|
nip94attachments?.let {
|
||||||
it.forEach {
|
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()))
|
tags.add(arrayOf("zap", it.lnAddressOrPubKeyHex, it.relay ?: "", it.weight.toString()))
|
||||||
}
|
}
|
||||||
findURLs(message).forEach {
|
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) }) {
|
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
||||||
tags.add(arrayOf("image", it))
|
tags.add(arrayOf("image", it))
|
||||||
}
|
}
|
||||||
@@ -136,7 +142,7 @@ class ClassifiedsEvent(
|
|||||||
}
|
}
|
||||||
nip94attachments?.let {
|
nip94attachments?.let {
|
||||||
it.forEach {
|
it.forEach {
|
||||||
tags.add(arrayOf("nip94", it.toJson()))
|
//tags.add(arrayOf("nip94", it.toJson()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -83,7 +83,7 @@ class LiveActivitiesChatMessageEvent(
|
|||||||
}
|
}
|
||||||
nip94attachments?.let {
|
nip94attachments?.let {
|
||||||
it.forEach {
|
it.forEach {
|
||||||
tags.add(arrayOf("nip94", it.toJson()))
|
//tags.add(arrayOf("nip94", it.toJson()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ class PollNoteEvent(
|
|||||||
}
|
}
|
||||||
nip94attachments?.let {
|
nip94attachments?.let {
|
||||||
it.forEach {
|
it.forEach {
|
||||||
tags.add(arrayOf("nip94", it.toJson()))
|
//tags.add(arrayOf("nip94", it.toJson()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ class TextNoteEvent(
|
|||||||
}
|
}
|
||||||
nip94attachments?.let {
|
nip94attachments?.let {
|
||||||
it.forEach {
|
it.forEach {
|
||||||
tags.add(arrayOf("nip94", it.toJson()))
|
//tags.add(arrayOf("nip94", it.toJson()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user