From e1c54f52e3d4d14672fa8c23544f6bda892dd2da Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 25 Oct 2025 21:24:12 +0200 Subject: [PATCH 01/19] stream file to calculate both hash and size without loading it all at once --- .../uploads/blossom/BlossomUploader.kt | 45 ++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt index d97367448..7f8cf4166 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt @@ -36,7 +36,6 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent import com.vitorpamplona.quartz.nipB7Blossom.BlossomUploadResult import com.vitorpamplona.quartz.utils.RandomInstance -import com.vitorpamplona.quartz.utils.sha256.sha256 import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.MediaType.Companion.toMediaType @@ -48,9 +47,34 @@ import okio.BufferedSink import okio.source import java.io.File import java.io.InputStream +import java.security.MessageDigest import java.util.Base64 class BlossomUploader { + data class StreamInfo( + val hash: HexKey, + val size: Long, + ) + + /** + * Calculate SHA256 hash and size of a file by streaming it in chunks + * to avoid loading the entire file into memory. + */ + private fun calculateHashAndSize(inputStream: InputStream): StreamInfo { + val digest = MessageDigest.getInstance("SHA-256") + val buffer = ByteArray(8192) // 8KB buffer + var bytesRead: Int + var totalBytes = 0L + + while (inputStream.read(buffer).also { bytesRead = it } != -1) { + digest.update(buffer, 0, bytesRead) + totalBytes += bytesRead + } + + val hash = digest.digest().toHexKey() + return StreamInfo(hash, totalBytes) + } + fun Context.getFileName(uri: Uri): String? = when (uri.scheme) { ContentResolver.SCHEME_CONTENT -> getContentFileName(uri) @@ -82,25 +106,24 @@ class BlossomUploader { val myContentType = contentType ?: contentResolver.getType(uri) val fileName = context.getFileName(uri) + // Calculate hash and size by streaming the file in chunks + // to avoid loading the entire file into memory val imageInputStreamForHash = contentResolver.openInputStream(uri) - val payload = - imageInputStreamForHash?.use { - it.readBytes() + checkNotNull(imageInputStreamForHash) { "Can't open the image input stream" } + + val streamInfo = + imageInputStreamForHash.use { + calculateHashAndSize(it) } - checkNotNull(payload) { "Can't open the image input stream" } - - val hash = sha256(payload).toHexKey() - val imageInputStream = contentResolver.openInputStream(uri) - checkNotNull(imageInputStream) { "Can't open the image input stream" } return imageInputStream.use { stream -> upload( stream, - hash, - payload.size, + streamInfo.hash, + streamInfo.size.toInt(), fileName, myContentType, alt, From 93994564f6790a9c0691ac13d407c51e905c7785 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 25 Oct 2025 23:22:31 +0200 Subject: [PATCH 02/19] Add streaming hash utility function to quartz multiplatform, follow the existing pool/worker design Change hashing in ImageDownloader.kt to use streaming --- .../service/uploads/ImageDownloader.kt | 115 ++++++++++++++++++ .../service/uploads/UploadOrchestrator.kt | 44 ++++--- .../uploads/blossom/BlossomUploader.kt | 30 +++-- .../quartz/utils/sha256/Sha256.jvmAndroid.kt | 16 +++ .../utils/sha256/Sha256Hasher.jvmAndroid.kt | 23 ++++ .../utils/sha256/Sha256Pool.jvmAndroid.kt | 21 ++++ 6 files changed, 217 insertions(+), 32 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/ImageDownloader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/ImageDownloader.kt index 0407f799a..98426c9b5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/ImageDownloader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/ImageDownloader.kt @@ -20,6 +20,9 @@ */ package com.vitorpamplona.amethyst.service.uploads +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.utils.sha256.sha256Stream import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -34,6 +37,46 @@ class ImageDownloader { val contentType: String?, ) + /** + * Result of streaming verification - hash and metadata without storing full file + */ + class StreamVerification( + val hash: HexKey, + val size: Long, + val contentType: String?, + ) + + /** + * Stream download and calculate hash for verification without loading entire file into memory. + * This is memory-efficient for large files (videos, high-res images, etc.) + */ + suspend fun waitAndVerifyStream( + imageUrl: String, + okHttpClient: (url: String) -> OkHttpClient, + ): StreamVerification? = + withContext(Dispatchers.IO) { + var verification: StreamVerification? = null + var tentatives = 0 + + // Servers are usually not ready, so tries to download it for 15 times/seconds. + while (verification == null && tentatives < 15) { + verification = + try { + tryStreamAndVerify(imageUrl, okHttpClient) + } catch (e: Exception) { + if (e is CancellationException) throw e + null + } + + if (verification == null) { + tentatives++ + delay(1000) + } + } + + return@withContext verification + } + suspend fun waitAndGetImage( imageUrl: String, okHttpClient: (url: String) -> OkHttpClient, @@ -61,6 +104,78 @@ class ImageDownloader { return@withContext imageData } + private suspend fun tryStreamAndVerify( + imageUrl: String, + okHttpClient: (url: String) -> OkHttpClient, + ): StreamVerification? = + withContext(Dispatchers.IO) { + // TODO: Migrate to OkHttp + HttpURLConnection.setFollowRedirects(true) + var url = URL(imageUrl) + var clientProxy = okHttpClient(imageUrl).proxy + var huc = + if (clientProxy != null) { + url.openConnection(clientProxy) as HttpURLConnection + } else { + url.openConnection() as HttpURLConnection + } + huc.instanceFollowRedirects = true + var responseCode = huc.responseCode + + if (responseCode in 300..400) { + val newUrl: String = huc.getHeaderField("Location") + + // open the new connection again + url = URL(newUrl) + clientProxy = okHttpClient(newUrl).proxy + huc = + if (clientProxy != null) { + url.openConnection(clientProxy) as HttpURLConnection + } else { + url.openConnection() as HttpURLConnection + } + responseCode = huc.responseCode + } + + return@withContext if (responseCode in 200..300) { + var totalBytes = 0L + + // Wrap the input stream to count bytes while hashing + val countingStream = + object : java.io.InputStream() { + val inner = huc.inputStream + + override fun read(): Int { + val byte = inner.read() + if (byte != -1) totalBytes++ + return byte + } + + override fun read( + b: ByteArray, + off: Int, + len: Int, + ): Int { + val bytesRead = inner.read(b, off, len) + if (bytesRead > 0) totalBytes += bytesRead + return bytesRead + } + + override fun close() = inner.close() + } + + val hash = countingStream.use { sha256Stream(it).toHexKey() } + + StreamVerification( + hash = hash, + size = totalBytes, + contentType = huc.headerFields.get("Content-Type")?.firstOrNull(), + ) + } else { + null + } + } + private suspend fun tryGetTheImage( imageUrl: String, okHttpClient: (url: String) -> OkHttpClient, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt index 42a763dee..3c087c587 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt @@ -231,34 +231,32 @@ class UploadOrchestrator { updateState(0.6, UploadingState.Downloading) - val imageData: ImageDownloader.Blob? = ImageDownloader().waitAndGetImage(uploadResult.url, okHttpClient) + // Use streaming verification for memory efficiency with large files + val verification: ImageDownloader.StreamVerification? = ImageDownloader().waitAndVerifyStream(uploadResult.url, okHttpClient) - if (imageData != null) { + if (verification != null) { updateState(0.8, UploadingState.Hashing) - val result = - FileHeader.prepare( - imageData.bytes, - uploadResult.type ?: localContentType ?: imageData.contentType, - uploadResult.dimension, + // Create FileHeader with hash from streaming verification + // Note: We skip blurhash/dimensions since we already have them from upload + val fileHeader = + FileHeader( + mimeType = uploadResult.type ?: localContentType ?: verification.contentType, + hash = verification.hash, + size = verification.size.toInt(), + dim = uploadResult.dimension, + blurHash = null, // Skip blurhash generation for verification ) - result.fold( - onSuccess = { - return finish( - OrchestratorResult.ServerResult( - it, - uploadResult.url, - uploadResult.magnet, - uploadResult.sha256, - originalContentType, - originalHash, - ), - ) - }, - onFailure = { - return error(R.string.could_not_prepare_local_file_to_upload, it.message ?: it.javaClass.simpleName) - }, + return finish( + OrchestratorResult.ServerResult( + fileHeader, + uploadResult.url, + uploadResult.magnet, + uploadResult.sha256, + originalContentType, + originalHash, + ), ) } else { return error(R.string.could_not_download_from_the_server) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt index 7f8cf4166..032b1d91e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt @@ -36,6 +36,7 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent import com.vitorpamplona.quartz.nipB7Blossom.BlossomUploadResult import com.vitorpamplona.quartz.utils.RandomInstance +import com.vitorpamplona.quartz.utils.sha256.sha256Stream import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.MediaType.Companion.toMediaType @@ -47,7 +48,6 @@ import okio.BufferedSink import okio.source import java.io.File import java.io.InputStream -import java.security.MessageDigest import java.util.Base64 class BlossomUploader { @@ -61,17 +61,29 @@ class BlossomUploader { * to avoid loading the entire file into memory. */ private fun calculateHashAndSize(inputStream: InputStream): StreamInfo { - val digest = MessageDigest.getInstance("SHA-256") - val buffer = ByteArray(8192) // 8KB buffer - var bytesRead: Int var totalBytes = 0L - while (inputStream.read(buffer).also { bytesRead = it } != -1) { - digest.update(buffer, 0, bytesRead) - totalBytes += bytesRead - } + // Wrap the input stream to count bytes while hashing + val countingStream = + object : InputStream() { + override fun read(): Int { + val byte = inputStream.read() + if (byte != -1) totalBytes++ + return byte + } - val hash = digest.digest().toHexKey() + override fun read( + b: ByteArray, + off: Int, + len: Int, + ): Int { + val bytesRead = inputStream.read(b, off, len) + if (bytesRead > 0) totalBytes += bytesRead + return bytesRead + } + } + + val hash = sha256Stream(countingStream).toHexKey() return StreamInfo(hash, totalBytes) } diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256.jvmAndroid.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256.jvmAndroid.kt index c71dae98e..c19a70269 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256.jvmAndroid.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256.jvmAndroid.kt @@ -20,6 +20,22 @@ */ package com.vitorpamplona.quartz.utils.sha256 +import java.io.InputStream + val pool = Sha256Pool(5) // max parallel operations actual fun sha256(data: ByteArray) = pool.hash(data) + +/** + * Calculate SHA256 hash by streaming the input in chunks. + * This avoids loading the entire input into memory at once. + * Useful for hashing large files without running out of memory. + * + * @param inputStream The input stream to hash + * @param bufferSize Size of chunks to read (default 8KB) + * @return SHA256 hash bytes + */ +fun sha256Stream( + inputStream: InputStream, + bufferSize: Int = 8192, +) = pool.hashStream(inputStream, bufferSize) diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Hasher.jvmAndroid.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Hasher.jvmAndroid.kt index bd4709780..9289c678b 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Hasher.jvmAndroid.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Hasher.jvmAndroid.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.quartz.utils.sha256 +import java.io.InputStream import java.security.MessageDigest class Sha256Hasher { @@ -30,4 +31,26 @@ class Sha256Hasher { fun digest(byteArray: ByteArray) = digest.digest(byteArray) fun reset() = digest.reset() + + /** + * Calculate SHA256 hash by streaming the input in chunks. + * This avoids loading the entire input into memory at once. + * + * @param inputStream The input stream to hash + * @param bufferSize Size of chunks to read (default 8KB) + * @return SHA256 hash bytes + */ + fun hashStream( + inputStream: InputStream, + bufferSize: Int = 8192, + ): ByteArray { + val buffer = ByteArray(bufferSize) + var bytesRead: Int + + while (inputStream.read(buffer).also { bytesRead = it } != -1) { + digest.update(buffer, 0, bytesRead) + } + + return digest.digest().also { digest.reset() } + } } diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Pool.jvmAndroid.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Pool.jvmAndroid.kt index 6c76f66d4..8efa3c65c 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Pool.jvmAndroid.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Pool.jvmAndroid.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.quartz.utils.sha256 import com.vitorpamplona.quartz.utils.Log +import java.io.InputStream import java.util.concurrent.ArrayBlockingQueue class Sha256Pool( @@ -54,4 +55,24 @@ class Sha256Pool( release(hasher) } } + + /** + * Calculate SHA256 hash by streaming the input in chunks. + * This avoids loading the entire input into memory at once. + * + * @param inputStream The input stream to hash + * @param bufferSize Size of chunks to read (default 8KB) + * @return SHA256 hash bytes + */ + fun hashStream( + inputStream: InputStream, + bufferSize: Int = 8192, + ): ByteArray { + val hasher = acquire() + try { + return hasher.hashStream(inputStream, bufferSize) + } finally { + release(hasher) + } + } } From 2332623cdeb70a1a6bbf460f1400c8248dbc0326 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 25 Oct 2025 23:40:31 +0200 Subject: [PATCH 03/19] Create a CountingInputStream utility to avoid duplication Prevent INT overflow in BlossomUploader.kt Connection Cleanup in ImageDownloader.kt Added try-finally in Sha256Hasher.jvmAndroid.kt --- .../service/uploads/ImageDownloader.kt | 52 ++++++------------ .../uploads/blossom/BlossomUploader.kt | 39 +++++--------- .../sha256/CountingInputStream.jvmAndroid.kt | 53 +++++++++++++++++++ .../quartz/utils/sha256/Sha256.jvmAndroid.kt | 16 +++--- .../utils/sha256/Sha256Hasher.jvmAndroid.kt | 15 ++++-- 5 files changed, 103 insertions(+), 72 deletions(-) create mode 100644 quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/CountingInputStream.jvmAndroid.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/ImageDownloader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/ImageDownloader.kt index 98426c9b5..4a1f60643 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/ImageDownloader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/ImageDownloader.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.uploads import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey -import com.vitorpamplona.quartz.utils.sha256.sha256Stream +import com.vitorpamplona.quartz.utils.sha256.sha256StreamWithCount import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -137,42 +137,24 @@ class ImageDownloader { responseCode = huc.responseCode } - return@withContext if (responseCode in 200..300) { - var totalBytes = 0L - - // Wrap the input stream to count bytes while hashing - val countingStream = - object : java.io.InputStream() { - val inner = huc.inputStream - - override fun read(): Int { - val byte = inner.read() - if (byte != -1) totalBytes++ - return byte + return@withContext try { + if (responseCode in 200..300) { + val (hash, totalBytes) = + huc.inputStream.use { + sha256StreamWithCount(it) } - override fun read( - b: ByteArray, - off: Int, - len: Int, - ): Int { - val bytesRead = inner.read(b, off, len) - if (bytesRead > 0) totalBytes += bytesRead - return bytesRead - } - - override fun close() = inner.close() - } - - val hash = countingStream.use { sha256Stream(it).toHexKey() } - - StreamVerification( - hash = hash, - size = totalBytes, - contentType = huc.headerFields.get("Content-Type")?.firstOrNull(), - ) - } else { - null + StreamVerification( + hash = hash.toHexKey(), + size = totalBytes, + contentType = huc.headerFields.get("Content-Type")?.firstOrNull(), + ) + } else { + null + } + } finally { + // Always disconnect to release connection resources + huc.disconnect() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt index 032b1d91e..0cef56e1b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent import com.vitorpamplona.quartz.nipB7Blossom.BlossomUploadResult import com.vitorpamplona.quartz.utils.RandomInstance -import com.vitorpamplona.quartz.utils.sha256.sha256Stream +import com.vitorpamplona.quartz.utils.sha256.sha256StreamWithCount import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.MediaType.Companion.toMediaType @@ -61,30 +61,8 @@ class BlossomUploader { * to avoid loading the entire file into memory. */ private fun calculateHashAndSize(inputStream: InputStream): StreamInfo { - var totalBytes = 0L - - // Wrap the input stream to count bytes while hashing - val countingStream = - object : InputStream() { - override fun read(): Int { - val byte = inputStream.read() - if (byte != -1) totalBytes++ - return byte - } - - override fun read( - b: ByteArray, - off: Int, - len: Int, - ): Int { - val bytesRead = inputStream.read(b, off, len) - if (bytesRead > 0) totalBytes += bytesRead - return bytesRead - } - } - - val hash = sha256Stream(countingStream).toHexKey() - return StreamInfo(hash, totalBytes) + val (hash, size) = sha256StreamWithCount(inputStream) + return StreamInfo(hash.toHexKey(), size) } fun Context.getFileName(uri: Uri): String? = @@ -132,10 +110,19 @@ class BlossomUploader { checkNotNull(imageInputStream) { "Can't open the image input stream" } return imageInputStream.use { stream -> + // Validate file size to prevent overflow when converting to Int + val sizeInt = + streamInfo.size.let { + require(it <= Int.MAX_VALUE) { + "File too large: ${it / 1_048_576}MB exceeds maximum size of ${Int.MAX_VALUE / 1_048_576}MB" + } + it.toInt() + } + upload( stream, streamInfo.hash, - streamInfo.size.toInt(), + sizeInt, fileName, myContentType, alt, diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/CountingInputStream.jvmAndroid.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/CountingInputStream.jvmAndroid.kt new file mode 100644 index 000000000..b5107061e --- /dev/null +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/CountingInputStream.jvmAndroid.kt @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2025 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.utils.sha256 + +import java.io.FilterInputStream +import java.io.InputStream + +class CountingInputStream( + inputStream: InputStream, +) : FilterInputStream(inputStream) { + var bytesRead: Long = 0 + private set + + override fun read(): Int { + val byte = super.read() + if (byte != -1) bytesRead++ + return byte + } + + override fun read( + b: ByteArray, + off: Int, + len: Int, + ): Int { + val count = super.read(b, off, len) + if (count > 0) bytesRead += count + return count + } + + override fun skip(n: Long): Long { + val skipped = super.skip(n) + bytesRead += skipped + return skipped + } +} diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256.jvmAndroid.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256.jvmAndroid.kt index c19a70269..17234cfa9 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256.jvmAndroid.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256.jvmAndroid.kt @@ -27,15 +27,19 @@ val pool = Sha256Pool(5) // max parallel operations actual fun sha256(data: ByteArray) = pool.hash(data) /** - * Calculate SHA256 hash by streaming the input in chunks. - * This avoids loading the entire input into memory at once. - * Useful for hashing large files without running out of memory. + * Calculate SHA256 hash while counting bytes read from the stream. + * Returns both the hash and the number of bytes processed. + * This is more efficient than reading the stream twice. * * @param inputStream The input stream to hash * @param bufferSize Size of chunks to read (default 8KB) - * @return SHA256 hash bytes + * @return Pair of (hash bytes, bytes read count) */ -fun sha256Stream( +fun sha256StreamWithCount( inputStream: InputStream, bufferSize: Int = 8192, -) = pool.hashStream(inputStream, bufferSize) +): Pair { + val countingStream = CountingInputStream(inputStream) + val hash = pool.hashStream(countingStream, bufferSize) + return Pair(hash, countingStream.bytesRead) +} diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Hasher.jvmAndroid.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Hasher.jvmAndroid.kt index 9289c678b..04354963f 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Hasher.jvmAndroid.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Hasher.jvmAndroid.kt @@ -45,12 +45,17 @@ class Sha256Hasher { bufferSize: Int = 8192, ): ByteArray { val buffer = ByteArray(bufferSize) - var bytesRead: Int + try { + var bytesRead: Int - while (inputStream.read(buffer).also { bytesRead = it } != -1) { - digest.update(buffer, 0, bytesRead) + while (inputStream.read(buffer).also { bytesRead = it } != -1) { + digest.update(buffer, 0, bytesRead) + } + + return digest.digest() + } finally { + // Always reset digest to prevent contaminating the pool on exceptions + digest.reset() } - - return digest.digest().also { digest.reset() } } } From d01f6a21780ff7e1fa87c5f17402bb84403eaa5b Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 25 Oct 2025 23:54:56 +0200 Subject: [PATCH 04/19] Change length to Long from Int: avoids potential overflow, Long seems to be used everywhere else --- .../uploads/blossom/BlossomUploader.kt | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt index 0cef56e1b..90033b4ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt @@ -96,8 +96,6 @@ class BlossomUploader { val myContentType = contentType ?: contentResolver.getType(uri) val fileName = context.getFileName(uri) - // Calculate hash and size by streaming the file in chunks - // to avoid loading the entire file into memory val imageInputStreamForHash = contentResolver.openInputStream(uri) checkNotNull(imageInputStreamForHash) { "Can't open the image input stream" } @@ -107,22 +105,14 @@ class BlossomUploader { } val imageInputStream = contentResolver.openInputStream(uri) + checkNotNull(imageInputStream) { "Can't open the image input stream" } return imageInputStream.use { stream -> - // Validate file size to prevent overflow when converting to Int - val sizeInt = - streamInfo.size.let { - require(it <= Int.MAX_VALUE) { - "File too large: ${it / 1_048_576}MB exceeds maximum size of ${Int.MAX_VALUE / 1_048_576}MB" - } - it.toInt() - } - upload( stream, streamInfo.hash, - sizeInt, + streamInfo.size, fileName, myContentType, alt, @@ -143,7 +133,7 @@ class BlossomUploader { suspend fun upload( inputStream: InputStream, hash: HexKey, - length: Int, + length: Long, baseFileName: String?, contentType: String?, alt: String?, @@ -168,14 +158,14 @@ class BlossomUploader { object : RequestBody() { override fun contentType() = contentType?.toMediaType() - override fun contentLength() = length.toLong() + override fun contentLength() = length override fun writeTo(sink: BufferedSink) { inputStream.source().use(sink::writeAll) } } - httpAuth(hash, length.toLong(), alt?.let { "Uploading $it" } ?: "Uploading $fileName")?.let { + httpAuth(hash, length, alt?.let { "Uploading $it" } ?: "Uploading $fileName")?.let { requestBuilder.addHeader("Authorization", encodeAuth(it)) } From 4e31a9a5ac8e9fc0924903963587e3f50233664f Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 26 Oct 2025 00:09:17 +0200 Subject: [PATCH 05/19] update buffer to 64kb --- .../vitorpamplona/quartz/utils/sha256/Sha256.jvmAndroid.kt | 4 ++-- .../quartz/utils/sha256/Sha256Hasher.jvmAndroid.kt | 4 ++-- .../quartz/utils/sha256/Sha256Pool.jvmAndroid.kt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256.jvmAndroid.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256.jvmAndroid.kt index 17234cfa9..25fffbde9 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256.jvmAndroid.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256.jvmAndroid.kt @@ -32,12 +32,12 @@ actual fun sha256(data: ByteArray) = pool.hash(data) * This is more efficient than reading the stream twice. * * @param inputStream The input stream to hash - * @param bufferSize Size of chunks to read (default 8KB) + * @param bufferSize Size of chunks to read (default 64KB) * @return Pair of (hash bytes, bytes read count) */ fun sha256StreamWithCount( inputStream: InputStream, - bufferSize: Int = 8192, + bufferSize: Int = 65536, ): Pair { val countingStream = CountingInputStream(inputStream) val hash = pool.hashStream(countingStream, bufferSize) diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Hasher.jvmAndroid.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Hasher.jvmAndroid.kt index 04354963f..aabd5ab00 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Hasher.jvmAndroid.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Hasher.jvmAndroid.kt @@ -37,12 +37,12 @@ class Sha256Hasher { * This avoids loading the entire input into memory at once. * * @param inputStream The input stream to hash - * @param bufferSize Size of chunks to read (default 8KB) + * @param bufferSize Size of chunks to read (default 64KB) * @return SHA256 hash bytes */ fun hashStream( inputStream: InputStream, - bufferSize: Int = 8192, + bufferSize: Int = 65536, ): ByteArray { val buffer = ByteArray(bufferSize) try { diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Pool.jvmAndroid.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Pool.jvmAndroid.kt index 8efa3c65c..86cd0dbf9 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Pool.jvmAndroid.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Pool.jvmAndroid.kt @@ -61,12 +61,12 @@ class Sha256Pool( * This avoids loading the entire input into memory at once. * * @param inputStream The input stream to hash - * @param bufferSize Size of chunks to read (default 8KB) + * @param bufferSize Size of chunks to read (default 64KB) * @return SHA256 hash bytes */ fun hashStream( inputStream: InputStream, - bufferSize: Int = 8192, + bufferSize: Int = 65536, ): ByteArray { val hasher = acquire() try { From 7254dce3a97d0e209b40467ced72333a58620bcf Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 26 Oct 2025 10:25:40 +0100 Subject: [PATCH 06/19] Revert "update buffer to 64kb" This reverts commit 4e31a9a5ac8e9fc0924903963587e3f50233664f. --- .../vitorpamplona/quartz/utils/sha256/Sha256.jvmAndroid.kt | 4 ++-- .../quartz/utils/sha256/Sha256Hasher.jvmAndroid.kt | 4 ++-- .../quartz/utils/sha256/Sha256Pool.jvmAndroid.kt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256.jvmAndroid.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256.jvmAndroid.kt index 25fffbde9..17234cfa9 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256.jvmAndroid.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256.jvmAndroid.kt @@ -32,12 +32,12 @@ actual fun sha256(data: ByteArray) = pool.hash(data) * This is more efficient than reading the stream twice. * * @param inputStream The input stream to hash - * @param bufferSize Size of chunks to read (default 64KB) + * @param bufferSize Size of chunks to read (default 8KB) * @return Pair of (hash bytes, bytes read count) */ fun sha256StreamWithCount( inputStream: InputStream, - bufferSize: Int = 65536, + bufferSize: Int = 8192, ): Pair { val countingStream = CountingInputStream(inputStream) val hash = pool.hashStream(countingStream, bufferSize) diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Hasher.jvmAndroid.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Hasher.jvmAndroid.kt index aabd5ab00..04354963f 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Hasher.jvmAndroid.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Hasher.jvmAndroid.kt @@ -37,12 +37,12 @@ class Sha256Hasher { * This avoids loading the entire input into memory at once. * * @param inputStream The input stream to hash - * @param bufferSize Size of chunks to read (default 64KB) + * @param bufferSize Size of chunks to read (default 8KB) * @return SHA256 hash bytes */ fun hashStream( inputStream: InputStream, - bufferSize: Int = 65536, + bufferSize: Int = 8192, ): ByteArray { val buffer = ByteArray(bufferSize) try { diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Pool.jvmAndroid.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Pool.jvmAndroid.kt index 86cd0dbf9..8efa3c65c 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Pool.jvmAndroid.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Pool.jvmAndroid.kt @@ -61,12 +61,12 @@ class Sha256Pool( * This avoids loading the entire input into memory at once. * * @param inputStream The input stream to hash - * @param bufferSize Size of chunks to read (default 64KB) + * @param bufferSize Size of chunks to read (default 8KB) * @return SHA256 hash bytes */ fun hashStream( inputStream: InputStream, - bufferSize: Int = 65536, + bufferSize: Int = 8192, ): ByteArray { val hasher = acquire() try { From ccf9d4ce8032040733e52a1d1c44f635b770e93f Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 30 Oct 2025 16:48:58 +0100 Subject: [PATCH 07/19] update cs, de, sv, pt translations --- .../src/main/res/values-cs-rCZ/strings.xml | 27 ++++++++++++++++++ .../src/main/res/values-de-rDE/strings.xml | 28 +++++++++++++++++++ .../src/main/res/values-pt-rBR/strings.xml | 28 +++++++++++++++++++ .../src/main/res/values-sv-rSE/strings.xml | 28 +++++++++++++++++++ 4 files changed, 111 insertions(+) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 48986cd9e..69d62085f 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -1082,4 +1082,31 @@ Odeslat Tato zpráva zmizí za %1$d dní Vybrat podepisovatele + + Relaye pro odesílání + Nastavte své veřejné relaye pro odesílání příspěvků + Vytvoření seznamu relayů určených pro přijímání vašeho obsahu je zásadní pro váš zážitek z Nostr a jediný způsob, jak vás mohou vaši sledující najít. + Vložte 1–3 relaye, které přijímají vaše příspěvky. Ujistěte se, že nevyžadují platbu, pokud neplatíte za vložení. + Dobré možnosti jsou:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social + + Relaye pro přijímání + Nastavte své veřejné relaye pro přijímání oznámení + Vytvoření seznamu relayů určených pro přijímání oznámení je zásadní pro váš zážitek z Nostr. + Vložte 1–3 relaye, které přijímají oznámení pro vás. Ujistěte se, že nevyžadují platbu ani web of trust od odesílatele, jinak budete dostávat oznámení pouze od plátců nebo dlouhodobých uživatelů. + Dobré možnosti jsou:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social + + Veřejní členové + Soukromí členové + + Připojit novou NWC peněženku + Peněženka nebyla nalezená + Amethyst nenašel žádné aplikace peněženek, které podporují Nostr Wallet Connect (NWC). +\n\nCo můžete udělat: +\n\t1. Ověřte podporu NWC: Zkontrolujte, že vaše oblíbená peněženka podporuje Nostr Wallet Connect. +\n\t2. Vytvořte připojení: Otevřete svou peněženku a vytvořte nové NWC připojení. +\n\t3. Propojte s Amethyst: Vaše peněženka obvykle nabídne tři možnosti: +\n\t\t3.1 Automatické propojení: Tlačítko pro automatické otevření a připojení k Amethyst. Klikněte na něj, otevřete Amethyst a uložte na nové obrazovce. +\n\t\t3.2 Ruční propojení: URI připojení, které můžete zkopírovat a vložit přes ikonu vložení na této obrazovce. +\n\t\t3.3 QR kód: Peněženka vytvoří QR kód, který můžete naskenovat pomocí tlačítka QR kódu na této obrazovce. + diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index bd0b9b409..4054e6766 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -1087,4 +1087,32 @@ anz der Bedingungen ist erforderlich Senden Diese Nachricht verschwindet in %1$d Tagen Signierer auswählen + + Ausgehende Relays + Richte deine öffentlichen Ausgehenden Relays zum Posten ein + Das Erstellen einer Relay-Liste, die speziell zum Empfangen deiner Inhalte gedacht ist, ist entscheidend für dein Nostr-Erlebnis und der einzige Weg, wie dich deine Follower finden können. + Füge 1–3 Relays hinzu, die deine Beiträge empfangen. Stelle sicher, dass sie keine Zahlung verlangen, wenn du nicht für das Einfügen zahlst. + Gute Optionen sind:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social + + Eingehende Relays + Richte deine öffentlichen Eingehenden Relays zum Empfangen von Benachrichtigungen ein + Das Erstellen einer Relay-Liste, die speziell zum Empfangen von Benachrichtigungen gedacht ist, ist entscheidend für dein Nostr-Erlebnis. + Füge 1–3 Relays hinzu, die Benachrichtigungen für dich empfangen. Stelle sicher, dass sie keine Zahlung oder Web of Trust vom Absender verlangen, sonst erhältst du nur Benachrichtigungen von zahlenden oder langjährigen Nutzern. + Gute Optionen sind:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social + + Öffentliche Mitglieder + Private Mitglieder + + Mit neuer NWC-Wallet verbinden + Wallet nicht gefunden + Amethyst konnte keine Wallet-Apps finden, die Nostr Wallet Connect (NWC) unterstützen. +\n\nWas du tun kannst: +\n\t1. NWC-Unterstützung überprüfen: Stelle sicher, dass deine bevorzugte Wallet-App mit Nostr Wallet Connect kompatibel ist. +\n\t2. Verbindung erstellen: Öffne deine Wallet-App und erstelle eine neue NWC-Verbindung. +\n\t3. Mit Amethyst verknüpfen: Deine Wallet bietet normalerweise drei Optionen: +\n\t\t3.1 Automatische Verknüpfung: Eine Schaltfläche zum automatischen Öffnen und Verbinden mit Amethyst. Klicke darauf, öffne Amethyst und speichere auf dem neuen Bildschirm. +\n\t\t3.2 Manuelle Verknüpfung: Eine Verbindungs-URI, die du kopieren und über das Einfügesymbol auf diesem Bildschirm einfügen kannst. +\n\t\t3.3 QR-Code: Die Wallet erstellt einen QR-Code, den du über die QR-Schaltfläche auf diesem Bildschirm scannen kannst. + + diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index f6790d9c3..888657c23 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -1082,4 +1082,32 @@ Enviar Esta mensagem desaparecerá em %1$d dias Selecionar assinador + + Relays de Saída + Configure seus relays públicos de saída para postar + Criar uma lista de relays especificamente projetada para receber seu conteúdo é essencial para sua experiência no Nostr e a única forma de seus seguidores encontrarem você. + Insira entre 1–3 relays que recebam suas postagens. Certifique-se de que eles não exijam pagamento se você não estiver pagando para inserir. + Boas opções são:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social + + Relays de Entrada + Configure seus relays públicos de entrada para receber notificações + Criar uma lista de relays especificamente projetada para receber notificações é essencial para sua experiência no Nostr. + Insira entre 1–3 relays que recebam notificações para você. Certifique-se de que eles não exijam pagamento ou rede de confiança do remetente, caso contrário você receberá notificações apenas de pagadores ou usuários de longo prazo. + Boas opções são:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social + + Membros Públicos + Membros Privados + + Conectar a nova carteira NWC + Carteira não encontrada + O Amethyst não conseguiu encontrar nenhum aplicativo de carteira que suporte Nostr Wallet Connect (NWC). +\n\nO que você pode fazer: +\n\t1. Verifique o suporte NWC: Confirme se o aplicativo da sua carteira é compatível com o Nostr Wallet Connect. +\n\t2. Criar conexão: Vá até o aplicativo da sua carteira e crie uma nova conexão NWC. +\n\t3. Vincular ao Amethyst: Sua carteira geralmente oferece três opções: +\n\t\t3.1 Link Automático: Um botão para abrir e conectar automaticamente ao Amethyst. Clique nele, abra o Amethyst e salve na nova tela. +\n\t\t3.2 Link Manual: Um URI de conexão que você pode copiar e colar usando o ícone de colar nesta tela. +\n\t\t3.3 Código QR: A carteira gera um código QR que você pode ler usando o botão de código QR nesta tela. + + diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index b2b942706..d4d54d26e 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -1081,4 +1081,32 @@ Skicka Detta meddelande försvinner om %1$d dagar Välj signatör + + Utgående reläer + Ställ in dina offentliga utgående reläer för att kunna posta + Att skapa en relälista specifikt för att ta emot ditt innehåll är avgörande för din Nostr-upplevelse och det enda sättet dina följare kan hitta dig. + Lägg till mellan 1–3 reläer som tar emot dina inlägg. Se till att de inte kräver betalning om du inte betalar för att lägga till dem. + Bra alternativ är:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social + + Inkommande reläer + Ställ in dina offentliga inkommande reläer för att ta emot aviseringar + Att skapa en relälista specifikt för att ta emot aviseringar är avgörande för din Nostr-upplevelse. + Lägg till mellan 1–3 reläer som tar emot aviseringar åt dig. Se till att de inte kräver betalning eller web of trust från avsändaren, annars får du bara aviseringar från betalande eller långsiktiga användare. + Bra alternativ är:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social + + Offentliga medlemmar + Privata medlemmar + + Anslut till ny NWC-plånbok + Plånbok hittades inte + Amethyst kunde inte hitta några plånboksappar som stöder Nostr Wallet Connect (NWC). +\n\nVad du kan göra: +\n\t1. Verifiera NWC-stöd: Kontrollera att din föredragna plånbok stöder Nostr Wallet Connect. +\n\t2. Skapa anslutning: Gå till din plånbok och skapa en ny NWC-anslutning. +\n\t3. Länka till Amethyst: Din plånbok erbjuder vanligtvis tre alternativ: +\n\t\t3.1 Automatisk länk: En knapp för att automatiskt öppna och ansluta till Amethyst. Klicka på den, öppna Amethyst och spara på den nya skärmen. +\n\t\t3.2 Manuell länk: En anslutnings-URI som du kan kopiera och klistra in via klistra-ikonen på den här skärmen. +\n\t\t3.3 QR-kod: Plånboken skapar en QR-kod som du kan skanna med QR-knappens funktion på den här skärmen. + + From c111912d2f9967ff17c886b38337ecfd10fdf334 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Thu, 30 Oct 2025 17:56:08 +0000 Subject: [PATCH 08/19] New Crowdin translations by GitHub Action --- .../src/main/res/values-cs-rCZ/strings.xml | 49 ++++++++---------- .../src/main/res/values-de-rDE/strings.xml | 50 ++++++++----------- .../src/main/res/values-pt-rBR/strings.xml | 50 ++++++++----------- .../src/main/res/values-sv-rSE/strings.xml | 50 ++++++++----------- 4 files changed, 88 insertions(+), 111 deletions(-) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 69d62085f..1828c8a4e 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -459,6 +459,8 @@ %1$s je soukromý člen Přidat jako veřejného člena Přidat jako soukromého člena + Veřejní členové + Soukromí členové Veřejné profily Soukromé profily člen @@ -703,6 +705,16 @@ Vložit ze schránky Neplatné NIP-47 URI URI %1$s není platné přihlašovací URI NIP-47. + Připojit novou NWC peněženku + Peněženka nebyla nalezená + Amethyst nenašel žádné aplikace peněženek, které podporují Nostr Wallet Connect (NWC). +\n\nCo můžete udělat: +\n\t1. Ověřte podporu NWC: Zkontrolujte, že vaše oblíbená peněženka podporuje Nostr Wallet Connect. +\n\t2. Vytvořte připojení: Otevřete svou peněženku a vytvořte nové NWC připojení. +\n\t3. Propojte s Amethyst: Vaše peněženka obvykle nabídne tři možnosti: +\n\t\t3.1 Automatické propojení: Tlačítko pro automatické otevření a připojení k Amethyst. Klikněte na něj, otevřete Amethyst a uložte na nové obrazovce. +\n\t\t3.2 Ruční propojení: URI připojení, které můžete zkopírovat a vložit přes ikonu vložení na této obrazovce. +\n\t\t3.3 QR kód: Peněženka vytvoří QR kód, který můžete naskenovat pomocí tlačítka QR kódu na této obrazovce. Pro rozhraní aplikace Tmavé, světlé nebo systémové téma Automaticky načítat obrázky a GIFy @@ -934,6 +946,16 @@ Vytvoření seznamu relé speciálně navržených pro vyhledávání a označování uživatelů zlepší tyto výsledky. Vložte mezi 1–3 relé, která chcete použít při vyhledávání obsahu nebo označování uživatelů. Ujistěte se, že vaše vybraná relé implementují NIP-50 Dobré možnosti jsou:\n - nostr.wine\n - relay.nostr.band\n - relay.noswhere.com + Relaye pro odesílání + Nastavte své veřejné relaye pro odesílání příspěvků + Vytvoření seznamu relayů určených pro přijímání vašeho obsahu je zásadní pro váš zážitek z Nostr a jediný způsob, jak vás mohou vaši sledující najít. + Vložte 1–3 relaye, které přijímají vaše příspěvky. Ujistěte se, že nevyžadují platbu, pokud neplatíte za vložení. + Dobré možnosti jsou:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social + Relaye pro přijímání + Nastavte své veřejné relaye pro přijímání oznámení + Vytvoření seznamu relayů určených pro přijímání oznámení je zásadní pro váš zážitek z Nostr. + Vložte 1–3 relaye, které přijímají oznámení pro vás. Ujistěte se, že nevyžadují platbu ani web of trust od odesílatele, jinak budete dostávat oznámení pouze od plátců nebo dlouhodobých uživatelů. + Dobré možnosti jsou:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social Nahrát v DM Nastavení relé Veřejná domácí relé @@ -1082,31 +1104,4 @@ Odeslat Tato zpráva zmizí za %1$d dní Vybrat podepisovatele - - Relaye pro odesílání - Nastavte své veřejné relaye pro odesílání příspěvků - Vytvoření seznamu relayů určených pro přijímání vašeho obsahu je zásadní pro váš zážitek z Nostr a jediný způsob, jak vás mohou vaši sledující najít. - Vložte 1–3 relaye, které přijímají vaše příspěvky. Ujistěte se, že nevyžadují platbu, pokud neplatíte za vložení. - Dobré možnosti jsou:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social - - Relaye pro přijímání - Nastavte své veřejné relaye pro přijímání oznámení - Vytvoření seznamu relayů určených pro přijímání oznámení je zásadní pro váš zážitek z Nostr. - Vložte 1–3 relaye, které přijímají oznámení pro vás. Ujistěte se, že nevyžadují platbu ani web of trust od odesílatele, jinak budete dostávat oznámení pouze od plátců nebo dlouhodobých uživatelů. - Dobré možnosti jsou:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social - - Veřejní členové - Soukromí členové - - Připojit novou NWC peněženku - Peněženka nebyla nalezená - Amethyst nenašel žádné aplikace peněženek, které podporují Nostr Wallet Connect (NWC). -\n\nCo můžete udělat: -\n\t1. Ověřte podporu NWC: Zkontrolujte, že vaše oblíbená peněženka podporuje Nostr Wallet Connect. -\n\t2. Vytvořte připojení: Otevřete svou peněženku a vytvořte nové NWC připojení. -\n\t3. Propojte s Amethyst: Vaše peněženka obvykle nabídne tři možnosti: -\n\t\t3.1 Automatické propojení: Tlačítko pro automatické otevření a připojení k Amethyst. Klikněte na něj, otevřete Amethyst a uložte na nové obrazovce. -\n\t\t3.2 Ruční propojení: URI připojení, které můžete zkopírovat a vložit přes ikonu vložení na této obrazovce. -\n\t\t3.3 QR kód: Peněženka vytvoří QR kód, který můžete naskenovat pomocí tlačítka QR kódu na této obrazovce. - diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index 4054e6766..99e04e593 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -465,6 +465,8 @@ anz der Bedingungen ist erforderlich %1$s ist ein privates Mitglied Als öffentliches Mitglied hinzufügen Als privates Mitglied hinzufügen + Öffentliche Mitglieder + Private Mitglieder Öffentliche Profile Private Profile Mitglied @@ -708,6 +710,16 @@ anz der Bedingungen ist erforderlich Aus Zwischenablage einfügen Ungültige NIP-47 URI Die URI %1$s ist keine gültige NIP-47 Anmelde-URI. + Mit neuer NWC-Wallet verbinden + Wallet nicht gefunden + Amethyst konnte keine Wallet-Apps finden, die Nostr Wallet Connect (NWC) unterstützen. +\n\nWas du tun kannst: +\n\t1. NWC-Unterstützung überprüfen: Stelle sicher, dass deine bevorzugte Wallet-App mit Nostr Wallet Connect kompatibel ist. +\n\t2. Verbindung erstellen: Öffne deine Wallet-App und erstelle eine neue NWC-Verbindung. +\n\t3. Mit Amethyst verknüpfen: Deine Wallet bietet normalerweise drei Optionen: +\n\t\t3.1 Automatische Verknüpfung: Eine Schaltfläche zum automatischen Öffnen und Verbinden mit Amethyst. Klicke darauf, öffne Amethyst und speichere auf dem neuen Bildschirm. +\n\t\t3.2 Manuelle Verknüpfung: Eine Verbindungs-URI, die du kopieren und über das Einfügesymbol auf diesem Bildschirm einfügen kannst. +\n\t\t3.3 QR-Code: Die Wallet erstellt einen QR-Code, den du über die QR-Schaltfläche auf diesem Bildschirm scannen kannst. Für die App-Benutzeroberfläche Dunkles, helles oder Systemdesign Bilder und GIFs automatisch laden @@ -939,6 +951,16 @@ anz der Bedingungen ist erforderlich Das Erstellen einer Relaisliste, die speziell für die Suche und Benutzerkennzeichnung entwickelt wurde, wird diese Ergebnisse verbessern. Fügen Sie 1–3 Relais ein, die beim Suchen nach Inhalten oder beim Taggen von Benutzern verwendet werden sollen. Stellen Sie sicher, dass Ihre ausgewählten Relais NIP-50 implementieren Gute Optionen sind:\n - nostr.wine\n - relay.nostr.band\n - relay.noswhere.com + Ausgehende Relays + Richte deine öffentlichen Ausgehenden Relays zum Posten ein + Das Erstellen einer Relay-Liste, die speziell zum Empfangen deiner Inhalte gedacht ist, ist entscheidend für dein Nostr-Erlebnis und der einzige Weg, wie dich deine Follower finden können. + Füge 1–3 Relays hinzu, die deine Beiträge empfangen. Stelle sicher, dass sie keine Zahlung verlangen, wenn du nicht für das Einfügen zahlst. + Gute Optionen sind:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social + Eingehende Relays + Richte deine öffentlichen Eingehenden Relays zum Empfangen von Benachrichtigungen ein + Das Erstellen einer Relay-Liste, die speziell zum Empfangen von Benachrichtigungen gedacht ist, ist entscheidend für dein Nostr-Erlebnis. + Füge 1–3 Relays hinzu, die Benachrichtigungen für dich empfangen. Stelle sicher, dass sie keine Zahlung oder Web of Trust vom Absender verlangen, sonst erhältst du nur Benachrichtigungen von zahlenden oder langjährigen Nutzern. + Gute Optionen sind:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social DM-Upload Relaiseinstellungen Öffentliche Heimrelais @@ -1087,32 +1109,4 @@ anz der Bedingungen ist erforderlich Senden Diese Nachricht verschwindet in %1$d Tagen Signierer auswählen - - Ausgehende Relays - Richte deine öffentlichen Ausgehenden Relays zum Posten ein - Das Erstellen einer Relay-Liste, die speziell zum Empfangen deiner Inhalte gedacht ist, ist entscheidend für dein Nostr-Erlebnis und der einzige Weg, wie dich deine Follower finden können. - Füge 1–3 Relays hinzu, die deine Beiträge empfangen. Stelle sicher, dass sie keine Zahlung verlangen, wenn du nicht für das Einfügen zahlst. - Gute Optionen sind:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social - - Eingehende Relays - Richte deine öffentlichen Eingehenden Relays zum Empfangen von Benachrichtigungen ein - Das Erstellen einer Relay-Liste, die speziell zum Empfangen von Benachrichtigungen gedacht ist, ist entscheidend für dein Nostr-Erlebnis. - Füge 1–3 Relays hinzu, die Benachrichtigungen für dich empfangen. Stelle sicher, dass sie keine Zahlung oder Web of Trust vom Absender verlangen, sonst erhältst du nur Benachrichtigungen von zahlenden oder langjährigen Nutzern. - Gute Optionen sind:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social - - Öffentliche Mitglieder - Private Mitglieder - - Mit neuer NWC-Wallet verbinden - Wallet nicht gefunden - Amethyst konnte keine Wallet-Apps finden, die Nostr Wallet Connect (NWC) unterstützen. -\n\nWas du tun kannst: -\n\t1. NWC-Unterstützung überprüfen: Stelle sicher, dass deine bevorzugte Wallet-App mit Nostr Wallet Connect kompatibel ist. -\n\t2. Verbindung erstellen: Öffne deine Wallet-App und erstelle eine neue NWC-Verbindung. -\n\t3. Mit Amethyst verknüpfen: Deine Wallet bietet normalerweise drei Optionen: -\n\t\t3.1 Automatische Verknüpfung: Eine Schaltfläche zum automatischen Öffnen und Verbinden mit Amethyst. Klicke darauf, öffne Amethyst und speichere auf dem neuen Bildschirm. -\n\t\t3.2 Manuelle Verknüpfung: Eine Verbindungs-URI, die du kopieren und über das Einfügesymbol auf diesem Bildschirm einfügen kannst. -\n\t\t3.3 QR-Code: Die Wallet erstellt einen QR-Code, den du über die QR-Schaltfläche auf diesem Bildschirm scannen kannst. - - diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 888657c23..f5c8e6cc9 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -459,6 +459,8 @@ %1$s é um membro privado Adicionar como membro público Adicionar como membro privado + Membros Públicos + Membros Privados Perfis públicos Perfis privados membro @@ -703,6 +705,16 @@ Colar da área de transferência URI NIP-47 inválido O URI %1$s não é um URI de login NIP-47 válido. + Conectar a nova carteira NWC + Carteira não encontrada + O Amethyst não conseguiu encontrar nenhum aplicativo de carteira que suporte Nostr Wallet Connect (NWC). +\n\nO que você pode fazer: +\n\t1. Verifique o suporte NWC: Confirme se o aplicativo da sua carteira é compatível com o Nostr Wallet Connect. +\n\t2. Criar conexão: Vá até o aplicativo da sua carteira e crie uma nova conexão NWC. +\n\t3. Vincular ao Amethyst: Sua carteira geralmente oferece três opções: +\n\t\t3.1 Link Automático: Um botão para abrir e conectar automaticamente ao Amethyst. Clique nele, abra o Amethyst e salve na nova tela. +\n\t\t3.2 Link Manual: Um URI de conexão que você pode copiar e colar usando o ícone de colar nesta tela. +\n\t\t3.3 Código QR: A carteira gera um código QR que você pode ler usando o botão de código QR nesta tela. Para a interface do aplicativo Tema Escuro, Claro ou Padrão Carregar automaticamente imagens e GIFs @@ -934,6 +946,16 @@ Criar uma lista de relés especificamente projetada para pesquisa e marcação de usuários melhorará esses resultados. Insira entre 1–3 relés para usar ao pesquisar conteúdo ou marcar usuários. Certifique-se de que seus relés escolhidos implementem o NIP-50 Boas opções são:\n - nostr.wine\n - relay.nostr.band\n - relay.noswhere.com + Relays de Saída + Configure seus relays públicos de saída para postar + Criar uma lista de relays especificamente projetada para receber seu conteúdo é essencial para sua experiência no Nostr e a única forma de seus seguidores encontrarem você. + Insira entre 1–3 relays que recebam suas postagens. Certifique-se de que eles não exijam pagamento se você não estiver pagando para inserir. + Boas opções são:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social + Relays de Entrada + Configure seus relays públicos de entrada para receber notificações + Criar uma lista de relays especificamente projetada para receber notificações é essencial para sua experiência no Nostr. + Insira entre 1–3 relays que recebam notificações para você. Certifique-se de que eles não exijam pagamento ou rede de confiança do remetente, caso contrário você receberá notificações apenas de pagadores ou usuários de longo prazo. + Boas opções são:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social Envio de DM Configurações de Relay Relés Públicos de Casa @@ -1082,32 +1104,4 @@ Enviar Esta mensagem desaparecerá em %1$d dias Selecionar assinador - - Relays de Saída - Configure seus relays públicos de saída para postar - Criar uma lista de relays especificamente projetada para receber seu conteúdo é essencial para sua experiência no Nostr e a única forma de seus seguidores encontrarem você. - Insira entre 1–3 relays que recebam suas postagens. Certifique-se de que eles não exijam pagamento se você não estiver pagando para inserir. - Boas opções são:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social - - Relays de Entrada - Configure seus relays públicos de entrada para receber notificações - Criar uma lista de relays especificamente projetada para receber notificações é essencial para sua experiência no Nostr. - Insira entre 1–3 relays que recebam notificações para você. Certifique-se de que eles não exijam pagamento ou rede de confiança do remetente, caso contrário você receberá notificações apenas de pagadores ou usuários de longo prazo. - Boas opções são:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social - - Membros Públicos - Membros Privados - - Conectar a nova carteira NWC - Carteira não encontrada - O Amethyst não conseguiu encontrar nenhum aplicativo de carteira que suporte Nostr Wallet Connect (NWC). -\n\nO que você pode fazer: -\n\t1. Verifique o suporte NWC: Confirme se o aplicativo da sua carteira é compatível com o Nostr Wallet Connect. -\n\t2. Criar conexão: Vá até o aplicativo da sua carteira e crie uma nova conexão NWC. -\n\t3. Vincular ao Amethyst: Sua carteira geralmente oferece três opções: -\n\t\t3.1 Link Automático: Um botão para abrir e conectar automaticamente ao Amethyst. Clique nele, abra o Amethyst e salve na nova tela. -\n\t\t3.2 Link Manual: Um URI de conexão que você pode copiar e colar usando o ícone de colar nesta tela. -\n\t\t3.3 Código QR: A carteira gera um código QR que você pode ler usando o botão de código QR nesta tela. - - diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index d4d54d26e..e326e921c 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -459,6 +459,8 @@ %1$s är en privat medlem Lägg till som offentlig medlem Lägg till som privat medlem + Offentliga medlemmar + Privata medlemmar Offentliga profiler Privata profiler medlem @@ -702,6 +704,16 @@ Klistra in från urklipp Ogiltig NIP-47 URI URI %1$s är inte en giltig NIP-47 inloggnings-URI. + Anslut till ny NWC-plånbok + Plånbok hittades inte + Amethyst kunde inte hitta några plånboksappar som stöder Nostr Wallet Connect (NWC). +\n\nVad du kan göra: +\n\t1. Verifiera NWC-stöd: Kontrollera att din föredragna plånbok stöder Nostr Wallet Connect. +\n\t2. Skapa anslutning: Gå till din plånbok och skapa en ny NWC-anslutning. +\n\t3. Länka till Amethyst: Din plånbok erbjuder vanligtvis tre alternativ: +\n\t\t3.1 Automatisk länk: En knapp för att automatiskt öppna och ansluta till Amethyst. Klicka på den, öppna Amethyst och spara på den nya skärmen. +\n\t\t3.2 Manuell länk: En anslutnings-URI som du kan kopiera och klistra in via klistra-ikonen på den här skärmen. +\n\t\t3.3 QR-kod: Plånboken skapar en QR-kod som du kan skanna med QR-knappens funktion på den här skärmen. För appens gränssnitt Mörkt, Ljust eller Systemtema Ladda automatiskt bilder och GIFs @@ -933,6 +945,16 @@ Att skapa en relälista speciellt utformad för sökning och användartaggning kommer att förbättra dessa resultat. Sätt in mellan 1–3 reläer att använda när du söker efter innehåll eller taggar användare. Se till att dina valda reläer implementerar NIP-50 Bra alternativ är:\n - nostr.wine\n - relay.nostr.band\n - relay.noswhere.com + Utgående reläer + Ställ in dina offentliga utgående reläer för att kunna posta + Att skapa en relälista specifikt för att ta emot ditt innehåll är avgörande för din Nostr-upplevelse och det enda sättet dina följare kan hitta dig. + Lägg till mellan 1–3 reläer som tar emot dina inlägg. Se till att de inte kräver betalning om du inte betalar för att lägga till dem. + Bra alternativ är:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social + Inkommande reläer + Ställ in dina offentliga inkommande reläer för att ta emot aviseringar + Att skapa en relälista specifikt för att ta emot aviseringar är avgörande för din Nostr-upplevelse. + Lägg till mellan 1–3 reläer som tar emot aviseringar åt dig. Se till att de inte kräver betalning eller web of trust från avsändaren, annars får du bara aviseringar från betalande eller långsiktiga användare. + Bra alternativ är:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social DM uppladdning Relä inställningar Offentliga hemreläer @@ -1081,32 +1103,4 @@ Skicka Detta meddelande försvinner om %1$d dagar Välj signatör - - Utgående reläer - Ställ in dina offentliga utgående reläer för att kunna posta - Att skapa en relälista specifikt för att ta emot ditt innehåll är avgörande för din Nostr-upplevelse och det enda sättet dina följare kan hitta dig. - Lägg till mellan 1–3 reläer som tar emot dina inlägg. Se till att de inte kräver betalning om du inte betalar för att lägga till dem. - Bra alternativ är:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social - - Inkommande reläer - Ställ in dina offentliga inkommande reläer för att ta emot aviseringar - Att skapa en relälista specifikt för att ta emot aviseringar är avgörande för din Nostr-upplevelse. - Lägg till mellan 1–3 reläer som tar emot aviseringar åt dig. Se till att de inte kräver betalning eller web of trust från avsändaren, annars får du bara aviseringar från betalande eller långsiktiga användare. - Bra alternativ är:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social - - Offentliga medlemmar - Privata medlemmar - - Anslut till ny NWC-plånbok - Plånbok hittades inte - Amethyst kunde inte hitta några plånboksappar som stöder Nostr Wallet Connect (NWC). -\n\nVad du kan göra: -\n\t1. Verifiera NWC-stöd: Kontrollera att din föredragna plånbok stöder Nostr Wallet Connect. -\n\t2. Skapa anslutning: Gå till din plånbok och skapa en ny NWC-anslutning. -\n\t3. Länka till Amethyst: Din plånbok erbjuder vanligtvis tre alternativ: -\n\t\t3.1 Automatisk länk: En knapp för att automatiskt öppna och ansluta till Amethyst. Klicka på den, öppna Amethyst och spara på den nya skärmen. -\n\t\t3.2 Manuell länk: En anslutnings-URI som du kan kopiera och klistra in via klistra-ikonen på den här skärmen. -\n\t\t3.3 QR-kod: Plånboken skapar en QR-kod som du kan skanna med QR-knappens funktion på den här skärmen. - - From c3c75b46a7a7ef1ddddb9a74a2b66dbbec9db815 Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 30 Oct 2025 16:55:39 +0100 Subject: [PATCH 09/19] optimise imports --- .../main/java/com/vitorpamplona/amethyst/LocalPreferences.kt | 1 - .../account/follows/AccountFollowsLoaderSubAssembler.kt | 1 - .../account/follows/FilterFindFollowMetadataForKey.kt | 1 - .../reqCommand/user/loaders/UserOutboxFinderSubAssembler.kt | 1 - .../reqCommand/user/watchers/FilterUserMetadataForKey.kt | 2 -- .../searchCommand/subassemblies/FilterByAuthor.kt | 2 -- .../vitorpamplona/amethyst/ui/components/RichTextViewer.kt | 2 -- .../amethyst/ui/layouts/DisappearingBottomBar.kt | 1 - .../amethyst/ui/layouts/DisappearingFloatingButton.kt | 2 -- .../discover/nip23LongForm/DiscoverLongFormFeedFilter.kt | 1 - .../ui/screen/loggedIn/lists/followsets/FollowSetScreen.kt | 1 - .../amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt | 1 - .../main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt | 2 -- .../amethyst/commons/richtext/GalleryParserTest.kt | 1 - .../quartz/nip55AndroidSigner/JsonMapperNip55.kt | 1 - .../vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt | 1 - .../quartz/nip01Core/relay/client/pool/PoolCounts.kt | 3 +-- .../quartz/nip06KeyDerivation/Bip32SeedDerivation.kt | 5 ++--- .../com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt | 2 -- .../com/vitorpamplona/quartz/nip59Giftwrap/WrappedEvent.kt | 1 - .../quartz/nip89AppHandlers/definition/AppMetadata.kt | 1 - .../quartz/nip90Dvms/NIP90ContentDiscoveryResponseEvent.kt | 1 - .../kotlin/com/vitorpamplona/quartz/RandomInstanceTest.kt | 1 - .../vitorpamplona/quartz/nip01Core/hints/HintIndexerTest.kt | 4 ---- .../vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt | 1 - .../relay/commands/toClient/CountResultSerializer.kt | 1 - .../nip46RemoteSigner/jackson/BunkerMessageDeserializer.kt | 1 - .../nip46RemoteSigner/jackson/BunkerRequestSerializer.kt | 1 - 28 files changed, 3 insertions(+), 40 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 9328f7d39..7f825ffb8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -66,7 +66,6 @@ import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable import java.io.File -import kotlin.jvm.java // Release mode (!BuildConfig.DEBUG) always uses encrypted preferences // To use plaintext SharedPreferences for debugging, set this to true diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt index e15a6c799..36fc9474c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt @@ -49,7 +49,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch -import kotlin.collections.forEach /** * This downloads the outbox events for all Follows of all users. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/FilterFindFollowMetadataForKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/FilterFindFollowMetadataForKey.kt index 3ed810db1..afd76dec4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/FilterFindFollowMetadataForKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/FilterFindFollowMetadataForKey.kt @@ -30,7 +30,6 @@ import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.utils.mapOfSet -import kotlin.collections.ifEmpty fun pickRelaysToLoadUsers( users: Set, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserOutboxFinderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserOutboxFinderSubAssembler.kt index a540b972b..bb145322d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserOutboxFinderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserOutboxFinderSubAssembler.kt @@ -36,7 +36,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.utils.TimeUtils -import kotlin.collections.ifEmpty class UserOutboxFinderSubAssembler( client: INostrClient, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt index 3f2deb186..a997ab173 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt @@ -33,8 +33,6 @@ import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.utils.mapOfSet -import kotlin.collections.forEach -import kotlin.collections.plus val UserMetadataForKeyKinds = listOf( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAuthor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAuthor.kt index c453f70f2..2aa980512 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAuthor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAuthor.kt @@ -27,8 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.utils.mapOfSet -import kotlin.collections.ifEmpty -import kotlin.collections.plus val MetadataKindList = listOf(MetadataEvent.KIND) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index b9388edba..c44bcaf2b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -92,7 +92,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUse import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav -import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav.nav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor @@ -112,7 +111,6 @@ import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlin.text.Typography.paragraph fun isMarkdown(content: String): Boolean = content.startsWith("> ") || diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBottomBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBottomBar.kt index 835a362a5..3d6f9077c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBottomBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBottomBar.kt @@ -31,7 +31,6 @@ import androidx.compose.foundation.gestures.rememberDraggableState import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.material3.BottomAppBarScrollBehavior import androidx.compose.material3.BottomAppBarState import androidx.compose.material3.ExperimentalMaterial3Api diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingFloatingButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingFloatingButton.kt index 9de17d8f9..5f8836b0b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingFloatingButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingFloatingButton.kt @@ -22,14 +22,12 @@ package com.vitorpamplona.myapplication import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.height import androidx.compose.material3.BottomAppBarScrollBehavior import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.layout -import com.vitorpamplona.myapplication.ResetDisappearingOnResume import kotlin.math.roundToInt @OptIn(ExperimentalMaterial3Api::class) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt index bff05caa9..ff1110fac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.LocalCache.notes import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.filterIntoSet import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetScreen.kt index a3e644730..ba8dcce83 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/followsets/FollowSetScreen.kt @@ -75,7 +75,6 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSet import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav -import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav.nav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt index 92671a5dd..129fe777d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt @@ -24,7 +24,6 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.LocalCache.notes import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.filterIntoSet import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt index 4c75a29de..0aa4a5f27 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt @@ -20,8 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.theme -import android.R.attr.fontFamily -import android.R.id.primary import android.app.Activity import android.app.UiModeManager import android.content.Context diff --git a/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/richtext/GalleryParserTest.kt b/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/richtext/GalleryParserTest.kt index a0b832feb..94d53861c 100644 --- a/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/richtext/GalleryParserTest.kt +++ b/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/richtext/GalleryParserTest.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.commons.richtext -import android.R.attr.text import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList import org.junit.Assert diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/JsonMapperNip55.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/JsonMapperNip55.kt index b42945d9d..eedede150 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/JsonMapperNip55.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/JsonMapperNip55.kt @@ -35,7 +35,6 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.Permission import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.PermissionDeserializer import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.PermissionSerializer import java.io.InputStream -import kotlin.jvm.java object JsonMapperNip55 { val defaultMapper: ObjectMapper = diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt index e5e6d4c98..c9fb6784e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt @@ -35,7 +35,6 @@ import com.vitorpamplona.quartz.nip01Core.metadata.tags.Nip05Tag import com.vitorpamplona.quartz.nip01Core.metadata.tags.PictureTag import com.vitorpamplona.quartz.nip01Core.metadata.tags.PronounsTag import com.vitorpamplona.quartz.nip01Core.metadata.tags.WebsiteTag -import com.vitorpamplona.quartz.nip01Core.metadata.website import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolCounts.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolCounts.kt index 81d7789b1..06ef476b4 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolCounts.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolCounts.kt @@ -33,7 +33,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.utils.cache.LargeCache import kotlinx.coroutines.flow.MutableStateFlow -import kotlin.collections.plus class PoolCounts { private var queries = LargeCache>>() @@ -102,7 +101,7 @@ class PoolCounts { } } - suspend fun syncState( + fun syncState( relay: NormalizedRelayUrl, sync: (Command) -> Unit, ) { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip06KeyDerivation/Bip32SeedDerivation.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip06KeyDerivation/Bip32SeedDerivation.kt index f34a195b8..0975c8e5b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip06KeyDerivation/Bip32SeedDerivation.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip06KeyDerivation/Bip32SeedDerivation.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.quartz.nip06KeyDerivation -import androidx.compose.runtime.key import com.vitorpamplona.quartz.utils.Secp256k1Instance import com.vitorpamplona.quartz.utils.mac.MacInstance @@ -77,9 +76,9 @@ class Bip32SeedDerivation { return ExtendedPrivateKey(key, ir) } - public fun writeInt32BE(n: Int): ByteArray = ByteArray(Int.SIZE_BYTES).also { writeInt32BE(n, it) } + fun writeInt32BE(n: Int): ByteArray = ByteArray(Int.SIZE_BYTES).also { writeInt32BE(n, it) } - public fun writeInt32BE( + fun writeInt32BE( n: Int, bs: ByteArray, off: Int = 0, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt index 8751562e2..fdea6b724 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt @@ -38,8 +38,6 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NSec import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CancellationException -import kotlin.sequences.forEach -import kotlin.text.Regex object Nip19Parser { private val nip19PlusNip46regex: Regex = diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/WrappedEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/WrappedEvent.kt index ea9b6fe9e..6f9f633c0 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/WrappedEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/WrappedEvent.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.quartz.nip59Giftwrap import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey -import kotlinx.serialization.Transient @Immutable open class WrappedEvent( diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppMetadata.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppMetadata.kt index d2cd247e8..44b6d8bb8 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppMetadata.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppMetadata.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.quartz.nip89AppHandlers.definition import androidx.compose.runtime.Stable import com.vitorpamplona.quartz.nip01Core.core.JsonMapper -import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppMetadata import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes import kotlinx.serialization.SerialName diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryResponseEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryResponseEvent.kt index 516cd049a..c4e1500b3 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryResponseEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryResponseEvent.kt @@ -30,7 +30,6 @@ import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes -import kotlinx.serialization.Transient @Immutable class NIP90ContentDiscoveryResponseEvent( diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/RandomInstanceTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/RandomInstanceTest.kt index 921987541..2d92c16d6 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/RandomInstanceTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/RandomInstanceTest.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.quartz import com.vitorpamplona.quartz.utils.RandomInstance -import kotlin.collections.all import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/hints/HintIndexerTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/hints/HintIndexerTest.kt index e362ede01..84f062b18 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/hints/HintIndexerTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/hints/HintIndexerTest.kt @@ -24,10 +24,6 @@ import com.vitorpamplona.quartz.TestResourceLoader import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey -import com.vitorpamplona.quartz.nip01Core.hints.HintIndexerTest.Companion.addresses -import com.vitorpamplona.quartz.nip01Core.hints.HintIndexerTest.Companion.eventIds -import com.vitorpamplona.quartz.nip01Core.hints.HintIndexerTest.Companion.indexer -import com.vitorpamplona.quartz.nip01Core.hints.HintIndexerTest.Companion.keys import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isLocalHost import com.vitorpamplona.quartz.utils.RandomInstance diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt index a57fcf79d..958bd7381 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt @@ -59,7 +59,6 @@ import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor import com.vitorpamplona.quartz.nip59Giftwrap.rumors.jackson.RumorDeserializer import com.vitorpamplona.quartz.nip59Giftwrap.rumors.jackson.RumorSerializer import java.io.InputStream -import kotlin.jvm.java class JacksonMapper { companion object { diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/CountResultSerializer.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/CountResultSerializer.kt index 21d00b3b0..20f94d0b4 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/CountResultSerializer.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/CountResultSerializer.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.quartz.nip01Core.relay.commands.toClient import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.ser.std.StdSerializer -import kotlin.jvm.java class CountResultSerializer : StdSerializer(CountResult::class.java) { override fun serialize( diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/jackson/BunkerMessageDeserializer.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/jackson/BunkerMessageDeserializer.kt index fe1159b11..38584bdfa 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/jackson/BunkerMessageDeserializer.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/jackson/BunkerMessageDeserializer.kt @@ -27,7 +27,6 @@ import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerMessage import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse -import kotlin.jvm.java class BunkerMessageDeserializer : StdDeserializer(BunkerMessage::class.java) { override fun deserialize( diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/jackson/BunkerRequestSerializer.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/jackson/BunkerRequestSerializer.kt index 46c0b52ee..a3f89ea4a 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/jackson/BunkerRequestSerializer.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/jackson/BunkerRequestSerializer.kt @@ -24,7 +24,6 @@ import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.ser.std.StdSerializer import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest -import kotlin.collections.forEach class BunkerRequestSerializer : StdSerializer(BunkerRequest::class.java) { override fun serialize( From bdb263780372d9177d2720befc7b5cf27d805f1c Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Fri, 31 Oct 2025 06:34:49 -0300 Subject: [PATCH 10/19] Fix location being added to note even after deselecting it --- .../loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt | 2 +- .../chats/publicChannels/send/ChannelNewMessageViewModel.kt | 2 +- .../loggedIn/discover/nip99Classifieds/NewProductViewModel.kt | 2 +- .../amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index 6899d7b14..348eb8ffe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -441,7 +441,7 @@ class ChatNewMessageViewModel : val urls = findURLs(message.text) val usedAttachments = iMetaAttachments.filterIsIn(urls.toSet()) val emojis = findEmoji(message.text, accountViewModel.account.emoji.myEmojis.value) - val geoHash = (location?.value as? LocationState.LocationResult.Success)?.geoHash?.toString() + val geoHash = if (wantsToAddGeoHash) (location?.value as? LocationState.LocationResult.Success)?.geoHash?.toString() else null val message = message.text val contentWarningReason = if (wantsToMarkAsSensitive) "" else null diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 8272af2ae..7adeb4abb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -385,7 +385,7 @@ open class ChannelNewMessageViewModel : val emojis = findEmoji(message.text, accountViewModel.account.emoji.myEmojis.value) val channelRelays = channel.relays() - val geoHash = (location?.value as? LocationState.LocationResult.Success)?.geoHash?.toString() + val geoHash = if (wantsToAddGeoHash) (location?.value as? LocationState.LocationResult.Success)?.geoHash?.toString() else null return if (channel is PublicChatChannel) { val replyingToEvent = replyTo.value?.toEventHint() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index ae52a7d7f..71b44c360 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -327,7 +327,7 @@ open class NewProductViewModel : val urls = findURLs(tagger.message) val usedAttachments = iMetaDescription.filterIsIn(urls.toSet()) + productImages.map { it.toIMeta() } - val geoHash = (location?.value as? LocationState.LocationResult.Success)?.geoHash?.toString() + val geoHash = if (wantsToAddGeoHash) (location?.value as? LocationState.LocationResult.Success)?.geoHash?.toString() else null val zapReceiver = if (wantsForwardZapTo) forwardZapTo.value.toZapSplitSetup() else null val localZapRaiserAmount = if (wantsZapraiser) zapRaiserAmount.value else null diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index e4d7d7200..1e48d95fc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -515,7 +515,7 @@ open class ShortNotePostViewModel : val zapReceiver = if (wantsForwardZapTo) forwardZapTo.value.toZapSplitSetup() else null - val geoHash = (location?.value as? LocationState.LocationResult.Success)?.geoHash?.toString() + val geoHash = if (wantsToAddGeoHash) (location?.value as? LocationState.LocationResult.Success)?.geoHash?.toString() else null val localZapRaiserAmount = if (wantsZapRaiser) zapRaiserAmount.value else null val emojis = findEmoji(tagger.message, account.emoji.myEmojis.value) From 1e12decd369d13a7fdc9637e2be4ef1f60629838 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 1 Nov 2025 16:23:35 +0100 Subject: [PATCH 11/19] add VideoSegment and failing tests --- .../commons/richtext/GalleryParserTest.kt | 111 +++++++++++++++--- .../richtext/RichTextParserSegments.kt | 5 + 2 files changed, 101 insertions(+), 15 deletions(-) diff --git a/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/richtext/GalleryParserTest.kt b/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/richtext/GalleryParserTest.kt index a0b832feb..d49ad783c 100644 --- a/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/richtext/GalleryParserTest.kt +++ b/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/richtext/GalleryParserTest.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.commons.richtext -import android.R.attr.text import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList import org.junit.Assert @@ -31,7 +30,7 @@ import org.junit.runner.RunWith class GalleryParserTest { @Test fun testMixedImageAndVideoRenderedIndividually() { - // Test the bug: when mixed image + video, both should be rendered individually + // Test that when image + video are in consecutive paragraphs, both should be rendered individually (not as gallery) val text = "Renfield (2023)\n" + "https://image.tmdb.org/t/p/original/ekfIcBvqfqKbI6m227NFipBNh7O.jpg\n" + @@ -39,19 +38,29 @@ class GalleryParserTest { val state = RichTextParser().parseText(text, EmptyTagList, null) - // Should have 2 image segments (image + video URLs) - Assert.assertEquals(2, state.paragraphs.size) + // Should have 3 paragraphs (text, image, video) + Assert.assertEquals(3, state.paragraphs.size) + // First paragraph is text Assert.assertTrue(state.paragraphs[0] !is ImageGalleryParagraph) Assert.assertEquals(1, state.paragraphs[0].words.size) - Assert.assertTrue(state.paragraphs[1] is ImageGalleryParagraph) + + // Second paragraph is image (rendered individually, not as gallery) + Assert.assertTrue(state.paragraphs[1] !is ImageGalleryParagraph) + Assert.assertEquals(1, state.paragraphs[1].words.size) + Assert.assertTrue(state.paragraphs[1].words[0] is ImageSegment) Assert.assertEquals("https://image.tmdb.org/t/p/original/ekfIcBvqfqKbI6m227NFipBNh7O.jpg", state.paragraphs[1].words[0].segmentText) - Assert.assertEquals("https://archive.org/download/cinema-horror-sci-fi/Renfield.2023.ia.mp4", state.paragraphs[1].words[1].segmentText) + + // Third paragraph is video (rendered individually) + Assert.assertTrue(state.paragraphs[2] !is ImageGalleryParagraph) + Assert.assertEquals(1, state.paragraphs[2].words.size) + Assert.assertTrue(state.paragraphs[2].words[0] is VideoSegment) + Assert.assertEquals("https://archive.org/download/cinema-horror-sci-fi/Renfield.2023.ia.mp4", state.paragraphs[2].words[0].segmentText) } @Test fun testMixedImageAndVideoRenderedIndividuallyDoubled() { - // Test the bug: when mixed image + video, both should be rendered individually + // Test that when image + video are repeated, both should be rendered individually (not as gallery) val text = "Renfield (2023)\n" + "https://image.tmdb.org/t/p/original/ekfIcBvqfqKbI6m227NFipBNh7O.jpg\n" + @@ -62,19 +71,28 @@ class GalleryParserTest { val state = RichTextParser().parseText(text, EmptyTagList, null) - // Should have 2 image segments (image + video URLs) - Assert.assertEquals(4, state.paragraphs.size) + // Should have 6 paragraphs (text, image, video, text, image, video) + Assert.assertEquals(6, state.paragraphs.size) + // First set: text, image, video Assert.assertTrue(state.paragraphs[0] !is ImageGalleryParagraph) Assert.assertEquals(1, state.paragraphs[0].words.size) - Assert.assertTrue(state.paragraphs[1] is ImageGalleryParagraph) + Assert.assertTrue(state.paragraphs[1] !is ImageGalleryParagraph) + Assert.assertTrue(state.paragraphs[1].words[0] is ImageSegment) Assert.assertEquals("https://image.tmdb.org/t/p/original/ekfIcBvqfqKbI6m227NFipBNh7O.jpg", state.paragraphs[1].words[0].segmentText) - Assert.assertEquals("https://archive.org/download/cinema-horror-sci-fi/Renfield.2023.ia.mp4", state.paragraphs[1].words[1].segmentText) Assert.assertTrue(state.paragraphs[2] !is ImageGalleryParagraph) - Assert.assertEquals(1, state.paragraphs[2].words.size) - Assert.assertTrue(state.paragraphs[3] is ImageGalleryParagraph) - Assert.assertEquals("https://image.tmdb.org/t/p/original/ekfIcBvqfqKbI6m227NFipBNh7O.jpg", state.paragraphs[3].words[0].segmentText) - Assert.assertEquals("https://archive.org/download/cinema-horror-sci-fi/Renfield.2023.ia.mp4", state.paragraphs[3].words[1].segmentText) + Assert.assertTrue(state.paragraphs[2].words[0] is VideoSegment) + Assert.assertEquals("https://archive.org/download/cinema-horror-sci-fi/Renfield.2023.ia.mp4", state.paragraphs[2].words[0].segmentText) + + // Second set: text, image, video + Assert.assertTrue(state.paragraphs[3] !is ImageGalleryParagraph) + Assert.assertEquals(1, state.paragraphs[3].words.size) + Assert.assertTrue(state.paragraphs[4] !is ImageGalleryParagraph) + Assert.assertTrue(state.paragraphs[4].words[0] is ImageSegment) + Assert.assertEquals("https://image.tmdb.org/t/p/original/ekfIcBvqfqKbI6m227NFipBNh7O.jpg", state.paragraphs[4].words[0].segmentText) + Assert.assertTrue(state.paragraphs[5] !is ImageGalleryParagraph) + Assert.assertTrue(state.paragraphs[5].words[0] is VideoSegment) + Assert.assertEquals("https://archive.org/download/cinema-horror-sci-fi/Renfield.2023.ia.mp4", state.paragraphs[5].words[0].segmentText) } @Test @@ -141,4 +159,67 @@ class GalleryParserTest { Assert.assertEquals("https://relay.dergigi.com/d6a3e33b101fe219ef251ac6261c10392c2af9918c3c252d4c202016b0b4ec83.jpg", state.paragraphs[1].words[0].segmentText) Assert.assertEquals("https://relay.dergigi.com/d60c9c562912573f214c2b1958cc20bf8913cd718d2ed9e020621e3e3120634b.jpg", state.paragraphs[1].words[1].segmentText) } + + @Test + fun testMixedImageAndVideoInSameParagraph() { + // Test the user's specific example: mixed image and video in same paragraph should render individually + val text = + "The Crow (1994)\n" + + "https://image.tmdb.org/t/p/original/1r7iOhXSbbuUTORNEUOvCUkQ86K.jpg\n" + + "https://archive.org/download/the-crow-1994_20231024/The%20Crow%201994.rar/The%20Crow%201994.1080p.BluRay.x264%20.%20NVEE%2FThe%20Crow%201994.1080p.BluRay.x264%20.%20NVEE.mp4\n" + + "#fantasy #action #thriller #film #movies #kinostr" + val state = RichTextParser().parseText(text, EmptyTagList, null) + + // Should have 4 paragraphs (text, image, video, hashtags) + Assert.assertEquals(4, state.paragraphs.size) + + // First paragraph is text + Assert.assertTrue(state.paragraphs[0] !is ImageGalleryParagraph) + + // Second paragraph is image (rendered individually, not as gallery) + Assert.assertTrue(state.paragraphs[1] !is ImageGalleryParagraph) + Assert.assertTrue(state.paragraphs[1].words[0] is ImageSegment) + Assert.assertEquals("https://image.tmdb.org/t/p/original/1r7iOhXSbbuUTORNEUOvCUkQ86K.jpg", state.paragraphs[1].words[0].segmentText) + + // Third paragraph is video (rendered individually) + Assert.assertTrue(state.paragraphs[2] !is ImageGalleryParagraph) + Assert.assertTrue(state.paragraphs[2].words[0] is VideoSegment) + Assert.assertEquals("https://archive.org/download/the-crow-1994_20231024/The%20Crow%201994.rar/The%20Crow%201994.1080p.BluRay.x264%20.%20NVEE%2FThe%20Crow%201994.1080p.BluRay.x264%20.%20NVEE.mp4", state.paragraphs[2].words[0].segmentText) + + // Fourth paragraph is hashtags + Assert.assertTrue(state.paragraphs[3] !is ImageGalleryParagraph) + } + + @Test + fun testDesperadoExample() { + // Test the exact example provided by the user + val text = + "Desperado (1995)\n" + + "https://media.themoviedb.org/t/p/w440_and_h660_face/e3gwpBeXpvGZsxUya9zNym5QXrw.jpg\n" + + "https://archive.org/download/desperado.-1995.1080p.-blu-ray.x-264.-yify/Desperado.1995.1080p.BluRay.x264.YIFY.mp4\n" + + "#action #crime #film #movies #kinostr" + val state = RichTextParser().parseText(text, EmptyTagList, null) + + // Should have 4 paragraphs (text, image, video, hashtags) + Assert.assertEquals(4, state.paragraphs.size) + + // First paragraph is text + Assert.assertTrue(state.paragraphs[0] !is ImageGalleryParagraph) + Assert.assertEquals("Desperado (1995)", state.paragraphs[0].words[0].segmentText) + + // Second paragraph is image (rendered individually, not as gallery) + Assert.assertTrue(state.paragraphs[1] !is ImageGalleryParagraph) + Assert.assertEquals(1, state.paragraphs[1].words.size) + Assert.assertTrue(state.paragraphs[1].words[0] is ImageSegment) + Assert.assertEquals("https://media.themoviedb.org/t/p/w440_and_h660_face/e3gwpBeXpvGZsxUya9zNym5QXrw.jpg", state.paragraphs[1].words[0].segmentText) + + // Third paragraph is video (rendered individually) + Assert.assertTrue(state.paragraphs[2] !is ImageGalleryParagraph) + Assert.assertEquals(1, state.paragraphs[2].words.size) + Assert.assertTrue(state.paragraphs[2].words[0] is VideoSegment) + Assert.assertEquals("https://archive.org/download/desperado.-1995.1080p.-blu-ray.x-264.-yify/Desperado.1995.1080p.BluRay.x264.YIFY.mp4", state.paragraphs[2].words[0].segmentText) + + // Fourth paragraph is hashtags + Assert.assertTrue(state.paragraphs[3] !is ImageGalleryParagraph) + } } diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt index 4fd05413a..f826220c6 100644 --- a/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt @@ -58,6 +58,11 @@ class ImageSegment( segment: String, ) : Segment(segment) +@Immutable +class VideoSegment( + segment: String, +) : Segment(segment) + @Immutable class LinkSegment( segment: String, From 7c374fb5869c7de9f9127f17329be9cfe1596f2c Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 1 Nov 2025 16:27:28 +0100 Subject: [PATCH 12/19] Parse for video segments and exclude them from image gallery --- .../amethyst/ui/components/RichTextViewer.kt | 6 ++++-- .../amethyst/commons/richtext/GalleryParser.kt | 13 ++++++++++--- .../amethyst/commons/richtext/RichTextParser.kt | 11 +++++++++-- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index b9388edba..ce7e7ea98 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -81,6 +81,7 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextViewerState import com.vitorpamplona.amethyst.commons.richtext.SchemelessUrlSegment import com.vitorpamplona.amethyst.commons.richtext.SecretEmoji import com.vitorpamplona.amethyst.commons.richtext.Segment +import com.vitorpamplona.amethyst.commons.richtext.VideoSegment import com.vitorpamplona.amethyst.commons.richtext.WithdrawSegment import com.vitorpamplona.amethyst.model.HashtagIcon import com.vitorpamplona.amethyst.model.LocalCache @@ -92,7 +93,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUse import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav -import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav.nav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor @@ -112,7 +112,6 @@ import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlin.text.Typography.paragraph fun isMarkdown(content: String): Boolean = content.startsWith("> ") || @@ -416,6 +415,8 @@ private fun RenderWordWithoutPreview( when (word) { // Don't preview Images is ImageSegment -> ClickableUrl(word.segmentText, word.segmentText) + // Don't preview Videos + is VideoSegment -> ClickableUrl(word.segmentText, word.segmentText) is LinkSegment -> ClickableUrl(word.segmentText, word.segmentText) is EmojiSegment -> RenderCustomEmoji(word.segmentText, state) // Don't offer to pay invoices @@ -447,6 +448,7 @@ private fun RenderWordWithPreview( ) { when (word) { is ImageSegment -> ZoomableContentView(word.segmentText, state, accountViewModel) + is VideoSegment -> ZoomableContentView(word.segmentText, state, accountViewModel) is LinkSegment -> LoadUrlPreview(word.segmentText, word.segmentText, callbackUri, accountViewModel) is EmojiSegment -> RenderCustomEmoji(word.segmentText, state) is InvoiceSegment -> MayBeInvoicePreview(word.segmentText, accountViewModel) diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/GalleryParser.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/GalleryParser.kt index 51eec881d..6eb4d9700 100644 --- a/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/GalleryParser.kt +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/GalleryParser.kt @@ -37,6 +37,7 @@ class GalleryParser { paragraph.words.forEach { word -> when (word) { is ImageSegment, is Base64Segment -> imageCount++ + is VideoSegment -> hasNonWhitespaceNonImageContent = true // Videos are not images is RegularTextSegment -> { if (word.segmentText.isNotBlank()) { hasNonWhitespaceNonImageContent = true @@ -146,13 +147,18 @@ class GalleryParser { val word = paragraph.words[i] if (word is ImageSegment || word is Base64Segment) { - // Collect consecutive image/whitespace segments without extra list allocations + // Collect consecutive image/whitespace segments (but not videos) val imageSegments = mutableListOf() var j = i + var hasVideo = false while (j < n) { val seg = paragraph.words[j] when { + seg is VideoSegment -> { + hasVideo = true + break + } seg is ImageSegment || seg is Base64Segment -> imageSegments.add(seg) seg is RegularTextSegment && seg.segmentText.isBlank() -> { /* skip whitespace */ } else -> break @@ -160,8 +166,9 @@ class GalleryParser { j++ } - if (imageSegments.size <= 1) { - currentParagraphSegments.add(word) + // If we found a video, don't create a gallery - render images individually + if (hasVideo || imageSegments.size <= 1) { + currentParagraphSegments.addAll(imageSegments) } else { if (currentParagraphSegments.isNotEmpty()) { resultingParagraphs.add(ParagraphState(currentParagraphSegments.toImmutableList(), paragraph.isRTL)) diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt index edeebed7f..14bc34a4f 100644 --- a/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt @@ -139,9 +139,12 @@ class RichTextParser { val imagesForPager = urlSet.mapNotNull { fullUrl -> createMediaContent(fullUrl, imetas, content, callbackUri) }.associateBy { it.url } + val imageUrls = imagesForPager.filterValues { it is MediaUrlImage }.keys + val videoUrls = imagesForPager.filterValues { it is MediaUrlVideo }.keys + val emojiMap = CustomEmoji.createEmojiMap(tags) - val segments = findTextSegments(content, imagesForPager.keys, urlSet, emojiMap, tags) + val segments = findTextSegments(content, imageUrls, videoUrls, urlSet, emojiMap, tags) val base64Images = segments.map { it.words.filterIsInstance() }.flatten() @@ -164,6 +167,7 @@ class RichTextParser { private fun findTextSegments( content: String, images: Set, + videos: Set, urls: Set, emojis: Map, tags: ImmutableListOfLists, @@ -177,7 +181,7 @@ class RichTextParser { val wordList = paragraph.trimEnd().split(' ') val segments = ArrayList(wordList.size) wordList.forEach { word -> - segments.add(wordIdentifier(word, images, urls, emojis, tags)) + segments.add(wordIdentifier(word, images, videos, urls, emojis, tags)) } paragraphSegments.add(ParagraphState(segments.toPersistentList(), isRTL)) @@ -229,6 +233,7 @@ class RichTextParser { private fun wordIdentifier( word: String, images: Set, + videos: Set, urls: Set, emojis: Map, tags: ImmutableListOfLists, @@ -239,6 +244,8 @@ class RichTextParser { if (images.contains(word)) return ImageSegment(word) + if (videos.contains(word)) return VideoSegment(word) + if (urls.contains(word)) return LinkSegment(word) if (CustomEmoji.fastMightContainEmoji(word, emojis) && emojis.any { word.contains(it.key) }) return EmojiSegment(word) From eb4f0b9af9ae51ad2779ea80849d4ef8d3c6d145 Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Sat, 1 Nov 2025 18:20:58 +0000 Subject: [PATCH 13/19] Update README to mark video capture as complete --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index de0f34551..d69818c00 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ height="70">](https://github.com/vitorpamplona/amethyst/releases) - [ ] Key Migration (NIP-xx/Draft) - [ ] Time-based Sync (NIP-xx/Draft) - [x] Image Capture in the app -- [ ] Video Capture in the app +- [x] Video Capture in the app - [ ] Local Database - [ ] Workspaces - [ ] Infinity Scroll From f62b1667c30611cae86bcc1cc9327fa4938740a3 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Sat, 1 Nov 2025 18:22:14 +0000 Subject: [PATCH 14/19] New Crowdin translations by GitHub Action --- .../src/main/res/values-hi-rIN/strings.xml | 10 +++++ .../src/main/res/values-pl-rPL/strings.xml | 10 +++++ .../src/main/res/values-zh-rCN/strings.xml | 44 ++++++++++++++++++- 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/res/values-hi-rIN/strings.xml b/amethyst/src/main/res/values-hi-rIN/strings.xml index c73a4c1e5..72b71b730 100644 --- a/amethyst/src/main/res/values-hi-rIN/strings.xml +++ b/amethyst/src/main/res/values-hi-rIN/strings.xml @@ -951,6 +951,16 @@ खोज तथा उपयोगकर्ता सूचक जोडने के लिए स्पष्टतः रूपांकित पुनःप्रसारक सूची बनाने से इन परिणामों में शोधन होगा। विषयवस्तु खोजने के लिए अथवा उपयोगकर्ता सूचक जोडने में उपयोग करनें के लिए १ - ३ पुनःप्रसारकों को जोडें। सुनिश्चित करें कि आपके चयनित पुनःप्रसारक निप॰-५० को कार्यान्वित किये हैं। ये अच्छे विकल्प हैं :\n - nostr.wine\n - relay.nostr.band\n - relay.noswhere.com + निर्गतपेटिका पुनःप्रसारक सूची + अपने सार्वजनिक निर्गतपेटिका पुनःप्रसारकों की स्थापना करें पत्र प्रकाशन के लिए + आपके विषयवस्तु प्राप्त करने के लिए पुनःप्रसारक सूची की विशिष्ट रचना आवश्यक है नोस्टर में। तथा आपके अनुचरों के लिए आपको ढूँढने की एकमात्र विधि। + १ - ३ पुनःप्रसारकों को जोडें जो आपके पत्र स्वीकारते हैं। सुनिश्चित करें कि इनके लिए भुगतान अनावश्यक हैं यदि आप भुगतान नहीं कर रहे हैं अभिलेखन के लिए + ये अच्छे विकल्प हैं :\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social + आगतपेटिका पुनःप्रसारक सूची + आपके सार्वजनिक आगतपेटिका पुनःप्रसारकों की स्थापना करें सूचनाएँ प्राप्त करने के लिए + सूचनाएँ प्राप्त करने के लिए पुनःप्रसारक सूची की विशिष्ट रचना आवश्यक है नोस्टर में। + १ - ३ पुनःप्रसारकों को जोडें जो आपके लिए सूचनाएँ स्वीकारते हैं। सुनिश्चित करें कि इनके लिए भुगतान अथवा विश्वासजाल अनुमति अनावश्यक हैं अन्यथा आप केवल भुगतान करनेवालों से अथवा दीर्घकालिक प्रयोक्ताओं से सूचनाएँ प्राप्त करेंगे + ये अच्छे विकल्प हैं :\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social सीधा संदेश आरोहण पुनःप्रसारक स्थापना विकल्प सार्वजनिक मुख्य पुनःप्रसारक diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index 787dcb9eb..967436d49 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -948,6 +948,16 @@ Stworzenie listy transmiterów specjalnie przeznaczonych do wyszukiwania i tagowania użytkowników poprawi te wyniki. Wstaw od 1 do 3 transmiterów, które będą używane podczas wyszukiwania treści lub tagowania użytkowników. Upewnij się, że zaimplementowano NIP-50 Dobre opcje to:\n - nostr.wine\n - relay.nostr.band\n - relay.noswhere.com + Transmitery wysyłające + Skonfiguruj swoje publiczne transmitery wysyłające, aby publikować + Stworzenie listy transmiterów specjalnie zaprojektowanej do odbierania Twoich wpisów ma kluczowe znaczenie dla korzystania z sieci Nostr i jest jedynym sposobem, w jaki Twoi obserwujący mogą Cię znaleźć. + Wstaw od 1 do 3 transmiterów, które odbierają Twoje wpisy. Upewnij się, że nie wymagają one płatności, jeśli nie opłacasz wstawiania wpisów + Dobre opcje to:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social + Transmitery odbiorcze + Skonfiguruj swoje publiczne transmitery skrzynki odbiorczej, aby otrzymywać powiadomienia + Stworzenie listy transmiterów specjalnie zaprojektowanej do odbierania powiadomień ma kluczowe znaczenie dla korzystania z protokołu Nostr. + Wstaw od 1 do 3 transmiterów, które będą odbierać powiadomienia. Upewnij się, że nie wymagają one płatności ani statusu \"web of trust\" od nadawcy, w przeciwnym razie będziesz otrzymywać powiadomienia tylko od płatników lub stałych użytkowników + Dobre opcje to:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social Wgrywanie DM Ustawienia Transmiterów Publiczne transmitery domowe diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index 434f9503b..548230370 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -455,9 +455,21 @@ 似乎你还没有任何关注集。 \n轻按下方刷新,或者轻按“+”按钮新建一个。 + 新建 添加作者到关注集 从列表中添加或删除用户,或用此用户创建一个新列表。 - %1$s 列表的图标 + 关注集的图标 + %1$s 是公开成员 + %1$s 是私密成员 + 添加为公开成员 + 添加为私密成员 + 公开成员 + 私密成员 + 公开的个人资料 + 私密配置文件 + 成员 + 成员 + 无成员 此列表中没有 %1$s 您的关注集 未找到关注集,或者你还没有任何关注集。轻按下方刷新,或使用按钮新建。 @@ -466,10 +478,19 @@ 创建用户新的 %1$s 列表 创建 %1$s 关注集,并添加 %2$s 到其中。 新的 %1$s 列表 + 复制/克隆关注集 + 修改描述 + 此列表没有描述 + 当前描述: + 你可以为下方的克隆集合设置自定义名称、描述。 集合名 + (原始集合名称) 集合描述(可选) + (原始集合描述) 新建集 + 复制/克隆集 重命名集 + 修改 正将集合名称从 改为 默认端口为 9050 @@ -688,6 +709,17 @@ 粘贴自剪贴板 无效的 NIP-47 URI URI %1$s 不是有效的 NIP-47 登录 URI。 + 连接到新的 NWC 钱包 + 未找到钱包 + Amethyst 找不到任何支持 Nostr Wallet Connect (NWC) 的钱包应用. +\n\n你可以做的事情: +\n\t1. 验证 NWC 支持:仔细确认偏好的钱包应用和 Nostr Wallet Connect 兼容。 +\n\t2. 创建连接:前往钱包应用并创建新的 NWC 连接。 +\n\t3.链接到 Amethyst: 你的钱包通常会给你三个选项: +\n\t\t3.1 自动链接:自动打开并连接到 Amethyst 的按钮。如果是这样, 单击那个按钮,打开 Amethyst 并在新屏上按保存。 +\n\t\t3.2 手动链接: 你可以复制的连接 URI,单击这个屏幕上的粘贴图标直接输入它。 +\n\t\t3.3 二维码: 钱包会创建一个你可以从这个屏幕中的二维码按钮那里读取的二维码。 + 用于应用程序界面 暗色、亮色或系统主题 自动加载图像和 GIF @@ -919,6 +951,16 @@ 通过创建专用于关键词和标签检索的中继列表能够改善搜索结果。 设置 1 ~ 3 个支持 NIP-50 的中继,用于关键词和标签检索。 示例:\n - nostr.wine\n - reiny.nostr.band\n - reliy.noswhere.com + 发件箱中继 + 设置您的公共发件箱中继来发布内容 + 创建专为接收您的内容而设计的中继列表对于您的Nostr体验至关重要,也是您的关注者找到您的唯一途径。 + 插入 1-3 个接收你帖子的中继。确保它们不需要付款,如果你没有付费来插入 + 好的选项是:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social + 收件箱中继 + 设置您的公共收件箱中继来接收通知 + 创建专为接收通知而设计的中继列表对于您的 Nostr 体验至关重要。 + 插入 1-3 个为你接受通知的中继。确保它们不需要发送者的付款或 web of trust,否则你只能接收付款人或长期用户的通知 + 好的选项是:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social DM 上传 中继设置 公共时间线中继 From eee1487883bc97c1ce079e876dfc2b705364f207 Mon Sep 17 00:00:00 2001 From: davotoula Date: Wed, 29 Oct 2025 12:08:14 +0100 Subject: [PATCH 15/19] added blurhash to blossom upload --- .../uploads/BlurhashMetadataCalculator.kt | 124 ++++++++++++++++++ .../amethyst/service/uploads/FileHeader.kt | 26 +--- .../service/uploads/MediaUploadResult.kt | 3 + .../service/uploads/UploadOrchestrator.kt | 2 +- .../uploads/blossom/BlossomUploader.kt | 39 ++++-- 5 files changed, 154 insertions(+), 40 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/BlurhashMetadataCalculator.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/BlurhashMetadataCalculator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/BlurhashMetadataCalculator.kt new file mode 100644 index 000000000..c90473a47 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/BlurhashMetadataCalculator.kt @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2025 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.amethyst.service.uploads + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.media.MediaMetadataRetriever +import android.net.Uri +import com.vitorpamplona.amethyst.commons.blurhash.toBlurhash +import com.vitorpamplona.amethyst.service.images.BlurhashWrapper +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.utils.Log + +object BlurhashMetadataCalculator { + fun shouldAttempt(mimeType: String?): Boolean = + mimeType?.let { + it.startsWith("image/", ignoreCase = true) || it.startsWith("video/", ignoreCase = true) + } ?: false + + fun computeFromBytes( + data: ByteArray, + mimeType: String?, + dimPrecomputed: DimensionTag?, + ): Pair = + when { + mimeType?.startsWith("image/", ignoreCase = true) == true -> { + val options = BitmapFactory.Options() + options.inPreferredConfig = Bitmap.Config.ARGB_8888 + val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size, options) + val (blur, dim) = processBitmap(bitmap) + blur to (dim ?: dimPrecomputed) + } + mimeType?.startsWith("video/", ignoreCase = true) == true -> { + val retriever = MediaMetadataRetriever() + try { + retriever.setDataSource(ByteArrayMediaDataSource(data)) + processRetriever(retriever, dimPrecomputed) + } finally { + retriever.release() + } + } + else -> null to dimPrecomputed + } + + fun computeFromUri( + context: Context, + uri: Uri, + mimeType: String?, + dimPrecomputed: DimensionTag? = null, + ): Pair? { + if (!shouldAttempt(mimeType)) return null + + return try { + when { + mimeType?.startsWith("image/", ignoreCase = true) == true -> + context.contentResolver.openInputStream(uri)?.use { stream -> + val options = BitmapFactory.Options() + options.inPreferredConfig = Bitmap.Config.ARGB_8888 + val bitmap = BitmapFactory.decodeStream(stream, null, options) + val (blur, dim) = processBitmap(bitmap) + blur to (dim ?: dimPrecomputed) + } ?: (null to dimPrecomputed) + + mimeType?.startsWith("video/", ignoreCase = true) == true -> { + val retriever = MediaMetadataRetriever() + try { + retriever.setDataSource(context, uri) + processRetriever(retriever, dimPrecomputed) + } finally { + retriever.release() + } + } + else -> null + } + } catch (e: Exception) { + Log.w("BlurhashMetadataCalc", "Failed to compute metadata from uri", e) + null + } + } + + private fun processBitmap(bitmap: Bitmap?): Pair = + if (bitmap != null) { + try { + val blurhash = BlurhashWrapper(bitmap.toBlurhash()) + blurhash to DimensionTag(bitmap.width, bitmap.height) + } finally { + bitmap.recycle() + } + } else { + null to null + } + + private fun processRetriever( + retriever: MediaMetadataRetriever, + dimPrecomputed: DimensionTag?, + ): Pair { + val dim = retriever.prepareDimFromVideo() ?: dimPrecomputed + val blurhash = retriever.getThumbnail()?.toBlurhash()?.let { BlurhashWrapper(it) } + return if (dim?.hasSize() == true) { + blurhash to dim + } else { + blurhash to null + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/FileHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/FileHeader.kt index 29d86b669..cd12d86f7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/FileHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/FileHeader.kt @@ -20,11 +20,7 @@ */ package com.vitorpamplona.amethyst.service.uploads -import android.graphics.Bitmap -import android.graphics.BitmapFactory import android.media.MediaDataSource -import android.media.MediaMetadataRetriever -import com.vitorpamplona.amethyst.commons.blurhash.toBlurhash import com.vitorpamplona.amethyst.service.images.BlurhashWrapper import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag @@ -75,27 +71,7 @@ class FileHeader( val hash = sha256(data).toHexKey() val size = data.size - val (blurHash, dim) = - if (mimeType?.startsWith("image/") == true) { - val opt = BitmapFactory.Options() - opt.inPreferredConfig = Bitmap.Config.ARGB_8888 - val mBitmap = BitmapFactory.decodeByteArray(data, 0, data.size, opt) - Pair(BlurhashWrapper(mBitmap.toBlurhash()), DimensionTag(mBitmap.width, mBitmap.height)) - } else if (mimeType?.startsWith("video/") == true) { - val mediaMetadataRetriever = MediaMetadataRetriever() - mediaMetadataRetriever.setDataSource(ByteArrayMediaDataSource(data)) - - val newDim = mediaMetadataRetriever.prepareDimFromVideo() ?: dimPrecomputed - val blurhash = mediaMetadataRetriever.getThumbnail()?.toBlurhash()?.let { BlurhashWrapper(it) } - - if (newDim?.hasSize() == true) { - Pair(blurhash, newDim) - } else { - Pair(blurhash, null) - } - } else { - Pair(null, null) - } + val (blurHash, dim) = BlurhashMetadataCalculator.computeFromBytes(data, mimeType, dimPrecomputed) Result.success(FileHeader(mimeType, hash, size, dim, blurHash)) } catch (e: Exception) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaUploadResult.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaUploadResult.kt index ad65e1756..b897d5dd9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaUploadResult.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaUploadResult.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.service.uploads +import com.vitorpamplona.amethyst.service.images.BlurhashWrapper import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag @@ -42,4 +43,6 @@ data class MediaUploadResult( val infohash: String? = null, // ipfs link val ipfs: String? = null, + // blurhash value for previews + val blurHash: BlurhashWrapper? = null, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt index 3c087c587..ca4311edd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt @@ -245,7 +245,7 @@ class UploadOrchestrator { hash = verification.hash, size = verification.size.toInt(), dim = uploadResult.dimension, - blurHash = null, // Skip blurhash generation for verification + blurHash = uploadResult.blurHash, ) return finish( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt index 90033b4ca..924b63eec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt @@ -28,6 +28,7 @@ import android.webkit.MimeTypeMap import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.HttpStatusMessages import com.vitorpamplona.amethyst.service.checkNotInMainThread +import com.vitorpamplona.amethyst.service.uploads.BlurhashMetadataCalculator import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -104,25 +105,35 @@ class BlossomUploader { calculateHashAndSize(it) } + val localMetadata = BlurhashMetadataCalculator.computeFromUri(context, uri, myContentType) + val imageInputStream = contentResolver.openInputStream(uri) checkNotNull(imageInputStream) { "Can't open the image input stream" } - return imageInputStream.use { stream -> - upload( - stream, - streamInfo.hash, - streamInfo.size, - fileName, - myContentType, - alt, - sensitiveContent, - serverBaseUrl, - okHttpClient, - httpAuth, - context, + val serverResult = + imageInputStream.use { stream -> + upload( + stream, + streamInfo.hash, + streamInfo.size, + fileName, + myContentType, + alt, + sensitiveContent, + serverBaseUrl, + okHttpClient, + httpAuth, + context, + ) + } + + return localMetadata?.let { (blur, dim) -> + serverResult.copy( + dimension = dim ?: serverResult.dimension, + blurHash = blur ?: serverResult.blurHash, ) - } + } ?: serverResult } fun encodeAuth(event: BlossomAuthorizationEvent): String { From 4b260cfc0788e5cfcb889d51167ecb38be70e992 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 2 Nov 2025 17:38:24 +0100 Subject: [PATCH 16/19] Add blurhash and dim to Nip96Uploader.kt --- .../service/uploads/nip96/Nip96Uploader.kt | 41 ++++++++++++------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt index 1490c1d7d..9e17ed898 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt @@ -30,6 +30,7 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.HttpStatusMessages import com.vitorpamplona.amethyst.service.checkNotInMainThread +import com.vitorpamplona.amethyst.service.uploads.BlurhashMetadataCalculator import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.core.JsonMapper @@ -106,24 +107,36 @@ class Nip96Uploader { val myContentType = contentType ?: contentResolver.getType(uri) val length = size ?: contentResolver.querySize(uri) ?: fileSize(uri) ?: 0 + val localMetadata = BlurhashMetadataCalculator.computeFromUri(context, uri, myContentType) val imageInputStream = contentResolver.openInputStream(uri) checkNotNull(imageInputStream) { "Can't open the image input stream" } - return imageInputStream.use { stream -> - upload( - stream, - length, - myContentType, - alt, - sensitiveContent, - server, - okHttpClient, - onProgress, - httpAuth, - context, - ) - } + val serverResult = + imageInputStream.use { stream -> + upload( + stream, + length, + myContentType, + alt, + sensitiveContent, + server, + okHttpClient, + onProgress, + httpAuth, + context, + ) + } + + val merged = + localMetadata?.let { (blur, dim) -> + serverResult.copy( + dimension = dim ?: serverResult.dimension, + blurHash = blur ?: serverResult.blurHash, + ) + } ?: serverResult + + return merged } suspend fun upload( From 76be7b0d58bfcafef6d39eba760ee992bcbbe148 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 2 Nov 2025 19:45:51 +0100 Subject: [PATCH 17/19] code simplification and deduplication --- .../uploads/BlurhashMetadataCalculator.kt | 45 ++++++++++------- .../service/uploads/UploadOrchestrator.kt | 50 +++++++++---------- .../uploads/blossom/BlossomUploader.kt | 26 +++------- .../service/uploads/nip96/Nip96Uploader.kt | 15 +++--- 4 files changed, 63 insertions(+), 73 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/BlurhashMetadataCalculator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/BlurhashMetadataCalculator.kt index c90473a47..bfd3ab05d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/BlurhashMetadataCalculator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/BlurhashMetadataCalculator.kt @@ -31,10 +31,24 @@ import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag import com.vitorpamplona.quartz.utils.Log object BlurhashMetadataCalculator { - fun shouldAttempt(mimeType: String?): Boolean = - mimeType?.let { - it.startsWith("image/", ignoreCase = true) || it.startsWith("video/", ignoreCase = true) - } ?: false + private fun isImage(mimeType: String?) = mimeType?.startsWith("image/", ignoreCase = true) == true + + private fun isVideo(mimeType: String?) = mimeType?.startsWith("video/", ignoreCase = true) == true + + fun shouldAttempt(mimeType: String?): Boolean = isImage(mimeType) || isVideo(mimeType) + + private fun createBitmapOptions() = + BitmapFactory.Options().apply { + inPreferredConfig = Bitmap.Config.ARGB_8888 + } + + private fun processImage( + bitmap: Bitmap?, + dimPrecomputed: DimensionTag?, + ): Pair { + val (blur, dim) = processBitmap(bitmap) + return blur to (dim ?: dimPrecomputed) + } fun computeFromBytes( data: ByteArray, @@ -42,14 +56,11 @@ object BlurhashMetadataCalculator { dimPrecomputed: DimensionTag?, ): Pair = when { - mimeType?.startsWith("image/", ignoreCase = true) == true -> { - val options = BitmapFactory.Options() - options.inPreferredConfig = Bitmap.Config.ARGB_8888 - val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size, options) - val (blur, dim) = processBitmap(bitmap) - blur to (dim ?: dimPrecomputed) + isImage(mimeType) -> { + val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size, createBitmapOptions()) + processImage(bitmap, dimPrecomputed) } - mimeType?.startsWith("video/", ignoreCase = true) == true -> { + isVideo(mimeType) -> { val retriever = MediaMetadataRetriever() try { retriever.setDataSource(ByteArrayMediaDataSource(data)) @@ -71,16 +82,13 @@ object BlurhashMetadataCalculator { return try { when { - mimeType?.startsWith("image/", ignoreCase = true) == true -> + isImage(mimeType) -> context.contentResolver.openInputStream(uri)?.use { stream -> - val options = BitmapFactory.Options() - options.inPreferredConfig = Bitmap.Config.ARGB_8888 - val bitmap = BitmapFactory.decodeStream(stream, null, options) - val (blur, dim) = processBitmap(bitmap) - blur to (dim ?: dimPrecomputed) + val bitmap = BitmapFactory.decodeStream(stream, null, createBitmapOptions()) + processImage(bitmap, dimPrecomputed) } ?: (null to dimPrecomputed) - mimeType?.startsWith("video/", ignoreCase = true) == true -> { + isVideo(mimeType) -> { val retriever = MediaMetadataRetriever() try { retriever.setDataSource(context, uri) @@ -89,6 +97,7 @@ object BlurhashMetadataCalculator { retriever.release() } } + else -> null } } catch (e: Exception) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt index ca4311edd..8426eca3d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt @@ -232,35 +232,33 @@ class UploadOrchestrator { updateState(0.6, UploadingState.Downloading) // Use streaming verification for memory efficiency with large files - val verification: ImageDownloader.StreamVerification? = ImageDownloader().waitAndVerifyStream(uploadResult.url, okHttpClient) + val verification = + ImageDownloader().waitAndVerifyStream(uploadResult.url, okHttpClient) + ?: return error(R.string.could_not_download_from_the_server) - if (verification != null) { - updateState(0.8, UploadingState.Hashing) + updateState(0.8, UploadingState.Hashing) - // Create FileHeader with hash from streaming verification - // Note: We skip blurhash/dimensions since we already have them from upload - val fileHeader = - FileHeader( - mimeType = uploadResult.type ?: localContentType ?: verification.contentType, - hash = verification.hash, - size = verification.size.toInt(), - dim = uploadResult.dimension, - blurHash = uploadResult.blurHash, - ) - - return finish( - OrchestratorResult.ServerResult( - fileHeader, - uploadResult.url, - uploadResult.magnet, - uploadResult.sha256, - originalContentType, - originalHash, - ), + // Create FileHeader with hash from streaming verification + // Note: We skip blurhash/dimensions since we already have them from upload + val fileHeader = + FileHeader( + mimeType = uploadResult.type ?: localContentType ?: verification.contentType, + hash = verification.hash, + size = verification.size.toInt(), + dim = uploadResult.dimension, + blurHash = uploadResult.blurHash, ) - } else { - return error(R.string.could_not_download_from_the_server) - } + + return finish( + OrchestratorResult.ServerResult( + fileHeader, + uploadResult.url, + uploadResult.magnet, + uploadResult.sha256, + originalContentType, + originalHash, + ), + ) } sealed class OrchestratorResult { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt index 924b63eec..6483ff09e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt @@ -52,20 +52,6 @@ import java.io.InputStream import java.util.Base64 class BlossomUploader { - data class StreamInfo( - val hash: HexKey, - val size: Long, - ) - - /** - * Calculate SHA256 hash and size of a file by streaming it in chunks - * to avoid loading the entire file into memory. - */ - private fun calculateHashAndSize(inputStream: InputStream): StreamInfo { - val (hash, size) = sha256StreamWithCount(inputStream) - return StreamInfo(hash.toHexKey(), size) - } - fun Context.getFileName(uri: Uri): String? = when (uri.scheme) { ContentResolver.SCHEME_CONTENT -> getContentFileName(uri) @@ -100,23 +86,23 @@ class BlossomUploader { val imageInputStreamForHash = contentResolver.openInputStream(uri) checkNotNull(imageInputStreamForHash) { "Can't open the image input stream" } - val streamInfo = - imageInputStreamForHash.use { - calculateHashAndSize(it) + val (hash, size) = + imageInputStreamForHash.use { stream -> + val (hashBytes, totalBytes) = sha256StreamWithCount(stream) + hashBytes.toHexKey() to totalBytes } val localMetadata = BlurhashMetadataCalculator.computeFromUri(context, uri, myContentType) val imageInputStream = contentResolver.openInputStream(uri) - checkNotNull(imageInputStream) { "Can't open the image input stream" } val serverResult = imageInputStream.use { stream -> upload( stream, - streamInfo.hash, - streamInfo.size, + hash, + size, fileName, myContentType, alt, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt index 9e17ed898..3bb80367f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt @@ -128,15 +128,12 @@ class Nip96Uploader { ) } - val merged = - localMetadata?.let { (blur, dim) -> - serverResult.copy( - dimension = dim ?: serverResult.dimension, - blurHash = blur ?: serverResult.blurHash, - ) - } ?: serverResult - - return merged + return localMetadata?.let { (blur, dim) -> + serverResult.copy( + dimension = dim ?: serverResult.dimension, + blurHash = blur ?: serverResult.blurHash, + ) + } ?: serverResult } suspend fun upload( From 942de501482eca2db4ed09364f2a13b056b5179b Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 2 Nov 2025 20:37:20 +0100 Subject: [PATCH 18/19] code simplification and deduplication --- .../service/uploads/ImageDownloader.kt | 127 ++++++++---------- .../service/uploads/MediaUploadResult.kt | 10 +- .../uploads/blossom/BlossomUploader.kt | 13 +- .../service/uploads/nip96/Nip96Uploader.kt | 13 +- 4 files changed, 70 insertions(+), 93 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/ImageDownloader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/ImageDownloader.kt index 4a1f60643..65c2d5960 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/ImageDownloader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/ImageDownloader.kt @@ -50,64 +50,54 @@ class ImageDownloader { * Stream download and calculate hash for verification without loading entire file into memory. * This is memory-efficient for large files (videos, high-res images, etc.) */ - suspend fun waitAndVerifyStream( - imageUrl: String, - okHttpClient: (url: String) -> OkHttpClient, - ): StreamVerification? = + private suspend fun retryWithDelay( + maxAttempts: Int = 15, + delayMs: Long = 1000, + operation: suspend () -> T?, + ): T? = withContext(Dispatchers.IO) { - var verification: StreamVerification? = null + var result: T? = null var tentatives = 0 // Servers are usually not ready, so tries to download it for 15 times/seconds. - while (verification == null && tentatives < 15) { - verification = + while (result == null && tentatives < maxAttempts) { + result = try { - tryStreamAndVerify(imageUrl, okHttpClient) + operation() } catch (e: Exception) { if (e is CancellationException) throw e null } - if (verification == null) { + if (result == null) { tentatives++ - delay(1000) + delay(delayMs) } } - return@withContext verification + return@withContext result } + suspend fun waitAndVerifyStream( + imageUrl: String, + okHttpClient: (url: String) -> OkHttpClient, + ): StreamVerification? = retryWithDelay { tryStreamAndVerify(imageUrl, okHttpClient) } + suspend fun waitAndGetImage( imageUrl: String, okHttpClient: (url: String) -> OkHttpClient, - ): Blob? = - withContext(Dispatchers.IO) { - var imageData: Blob? = null - var tentatives = 0 + ): Blob? = retryWithDelay { tryGetTheImage(imageUrl, okHttpClient) } - // Servers are usually not ready, so tries to download it for 15 times/seconds. - while (imageData == null && tentatives < 15) { - imageData = - try { - tryGetTheImage(imageUrl, okHttpClient) - } catch (e: Exception) { - if (e is CancellationException) throw e - null - } + private data class HttpConnection( + val connection: HttpURLConnection, + val responseCode: Int, + val contentType: String?, + ) - if (imageData == null) { - tentatives++ - delay(1000) - } - } - - return@withContext imageData - } - - private suspend fun tryStreamAndVerify( + private suspend fun openHttpConnection( imageUrl: String, okHttpClient: (url: String) -> OkHttpClient, - ): StreamVerification? = + ): HttpConnection = withContext(Dispatchers.IO) { // TODO: Migrate to OkHttp HttpURLConnection.setFollowRedirects(true) @@ -122,6 +112,7 @@ class ImageDownloader { huc.instanceFollowRedirects = true var responseCode = huc.responseCode + // Handle redirects if (responseCode in 300..400) { val newUrl: String = huc.getHeaderField("Location") @@ -137,24 +128,38 @@ class ImageDownloader { responseCode = huc.responseCode } + return@withContext HttpConnection( + connection = huc, + responseCode = responseCode, + contentType = huc.headerFields.get("Content-Type")?.firstOrNull(), + ) + } + + private suspend fun tryStreamAndVerify( + imageUrl: String, + okHttpClient: (url: String) -> OkHttpClient, + ): StreamVerification? = + withContext(Dispatchers.IO) { + val httpConn = openHttpConnection(imageUrl, okHttpClient) + return@withContext try { - if (responseCode in 200..300) { + if (httpConn.responseCode in 200..300) { val (hash, totalBytes) = - huc.inputStream.use { + httpConn.connection.inputStream.use { sha256StreamWithCount(it) } StreamVerification( hash = hash.toHexKey(), size = totalBytes, - contentType = huc.headerFields.get("Content-Type")?.firstOrNull(), + contentType = httpConn.contentType, ) } else { null } } finally { // Always disconnect to release connection resources - huc.disconnect() + httpConn.connection.disconnect() } } @@ -163,41 +168,19 @@ class ImageDownloader { okHttpClient: (url: String) -> OkHttpClient, ): Blob? = withContext(Dispatchers.IO) { - // TODO: Migrate to OkHttp - HttpURLConnection.setFollowRedirects(true) - var url = URL(imageUrl) - var clientProxy = okHttpClient(imageUrl).proxy - var huc = - if (clientProxy != null) { - url.openConnection(clientProxy) as HttpURLConnection + val httpConn = openHttpConnection(imageUrl, okHttpClient) + + return@withContext try { + if (httpConn.responseCode in 200..300) { + Blob( + httpConn.connection.inputStream.use { it.readBytes() }, + httpConn.contentType, + ) } else { - url.openConnection() as HttpURLConnection + null } - huc.instanceFollowRedirects = true - var responseCode = huc.responseCode - - if (responseCode in 300..400) { - val newUrl: String = huc.getHeaderField("Location") - - // open the new connnection again - url = URL(newUrl) - clientProxy = okHttpClient(newUrl).proxy - huc = - if (clientProxy != null) { - url.openConnection(clientProxy) as HttpURLConnection - } else { - url.openConnection() as HttpURLConnection - } - responseCode = huc.responseCode - } - - return@withContext if (responseCode in 200..300) { - Blob( - huc.inputStream.use { it.readBytes() }, - huc.headerFields.get("Content-Type")?.firstOrNull(), - ) - } else { - null + } finally { + httpConn.connection.disconnect() } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaUploadResult.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaUploadResult.kt index b897d5dd9..d8d36b1b3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaUploadResult.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaUploadResult.kt @@ -45,4 +45,12 @@ data class MediaUploadResult( val ipfs: String? = null, // blurhash value for previews val blurHash: BlurhashWrapper? = null, -) +) { + fun mergeLocalMetadata(localMetadata: Pair?): MediaUploadResult = + localMetadata?.let { (blur, dim) -> + copy( + dimension = dim ?: dimension, + blurHash = blur ?: blurHash, + ) + } ?: this +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt index 6483ff09e..b225f047e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt @@ -97,8 +97,8 @@ class BlossomUploader { val imageInputStream = contentResolver.openInputStream(uri) checkNotNull(imageInputStream) { "Can't open the image input stream" } - val serverResult = - imageInputStream.use { stream -> + return imageInputStream + .use { stream -> upload( stream, hash, @@ -112,14 +112,7 @@ class BlossomUploader { httpAuth, context, ) - } - - return localMetadata?.let { (blur, dim) -> - serverResult.copy( - dimension = dim ?: serverResult.dimension, - blurHash = blur ?: serverResult.blurHash, - ) - } ?: serverResult + }.mergeLocalMetadata(localMetadata) } fun encodeAuth(event: BlossomAuthorizationEvent): String { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt index 3bb80367f..a602af14e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt @@ -112,8 +112,8 @@ class Nip96Uploader { checkNotNull(imageInputStream) { "Can't open the image input stream" } - val serverResult = - imageInputStream.use { stream -> + return imageInputStream + .use { stream -> upload( stream, length, @@ -126,14 +126,7 @@ class Nip96Uploader { httpAuth, context, ) - } - - return localMetadata?.let { (blur, dim) -> - serverResult.copy( - dimension = dim ?: serverResult.dimension, - blurHash = blur ?: serverResult.blurHash, - ) - } ?: serverResult + }.mergeLocalMetadata(localMetadata) } suspend fun upload( From 379e539e897a423e795ea5918923c3390f957196 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Mon, 3 Nov 2025 17:39:48 +0000 Subject: [PATCH 19/19] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-hu-rHU/strings.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index f4b2e1f27..8f65532f8 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -951,6 +951,16 @@ A kifejezetten a kereséshez és a felhasználói címkézéshez tervezett átjátszólista létrehozása javítani fogja ezeket az eredményeket. Adjon hozzá 1–3 átjátszót a tartalom kereséshez vagy a felhasználók címkézéséhez. Győződjön meg arról, hogy a kiválasztott átjátszók alkalmazzák a NIP-50-et Jó választási lehetőségek:\n - nostr.wine\n - relay.nostr.band\n - relay.noswhere.com + Átjátszók a kimenő üzenetkhez + Állítsa be a nyilvános kimenő üzenetek átjátszóit a bejegyzéshez + A tartalom fogadására kifejezetten kialakított átjátszólista létrehozása elengedhetetlen a Nostr élményhez, és ez az egyetlen módja annak, hogy a követői megtalálják Önt. + Adjon meg 1-3 átjátszót, amelyek fogadják az Ön bejegyzéseit. Győződjön meg arról, hogy nem kérnek fizetést, ha Ön nem fizet a használatukért + Jó választási lehetőségek:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social + Átjátszók a bejövő üzenetkhez + Állítsa be a nyilvános bejövő üzenetek átjátszóit az értesítések fogadásához + Az értesítések fogadásához kifejezetten kialakított átjátszólista létrehozása elengedhetetlen a Nostr élményhez. + Adjon meg 1-3 átjátszót, amelyeken keresztül fogadni tudja az értesítéseket. Győződjön meg arról, hogy azok nem igényelnek fizetést vagy bizalmi hálózatot a feladótól, különben csak a fizetősektől vagy a hosszú távú felhasználóktól fog értesítéseket kapni + Jó választási lehetőségek:\n - nos.lol\n - nostr.mom\n - nostr.bitcoiner.social Feltöltés közvetlen üzenetbe Átjátszók beállításai Nyilvános kimenő és saját átjátszók