From e0ec8a741ca021a0838a00f5519c3e124fd9a257 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 22 Jan 2024 11:29:10 -0300 Subject: [PATCH 001/176] remove the need for application package name in the androidManifest for the external signer --- app/src/main/AndroidManifest.xml | 7 +++++-- .../vitorpamplona/amethyst/service/PackageUtils.kt | 12 ++++++++++-- .../amethyst/ui/screen/loggedOff/LoginScreen.kt | 2 +- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b30c56b1d..8b30f6ee1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,8 +3,11 @@ xmlns:tools="http://schemas.android.com/tools"> - - + + + + + diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/PackageUtils.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/PackageUtils.kt index f7767cd8f..75e17676b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/PackageUtils.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/PackageUtils.kt @@ -21,6 +21,8 @@ package com.vitorpamplona.amethyst.service import android.content.Context +import android.content.Intent +import android.net.Uri object PackageUtils { private fun isPackageInstalled( @@ -36,7 +38,13 @@ object PackageUtils { return isPackageInstalled(context, "org.torproject.android") } - fun isAmberInstalled(context: Context): Boolean { - return isPackageInstalled(context, "com.greenart7c3.nostrsigner") + fun isExternalSignerInstalled(context: Context): Boolean { + val intent = + Intent().apply { + action = Intent.ACTION_VIEW + data = Uri.parse("nostrsigner:") + } + val infos = context.packageManager.queryIntentActivities(intent, 0) + return infos.size > 0 } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt index e4ce9040e..079aa53cd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt @@ -458,7 +458,7 @@ fun LoginPage( } } - if (PackageUtils.isAmberInstalled(context)) { + if (PackageUtils.isExternalSignerInstalled(context)) { Box(modifier = Modifier.padding(40.dp, 20.dp, 40.dp, 0.dp)) { Button( enabled = acceptedTerms.value, From c7b2c201198166a47827bc8dbc0b21c37ee406b4 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 24 Jan 2024 17:18:25 -0300 Subject: [PATCH 002/176] recreate httpclient when changing the proxy or timeout --- .../main/java/com/vitorpamplona/amethyst/service/HttpClient.kt | 3 +++ .../java/com/vitorpamplona/amethyst/service/relays/Relay.kt | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/HttpClient.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/HttpClient.kt index 7645f8e1e..64c77e914 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/HttpClient.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/HttpClient.kt @@ -51,6 +51,8 @@ object HttpClient { fun start(proxy: Proxy?) { if (internalProxy != proxy) { + Log.d("HttpClient", "Changing proxy to: ${proxy != null}") + this.defaultHttpClient = null this.internalProxy = proxy this.defaultHttpClient = getHttpClient() } @@ -59,6 +61,7 @@ object HttpClient { fun changeTimeouts(timeout: Duration) { Log.d("HttpClient", "Changing timeout to: $timeout") if (this.defaultTimeout.seconds != timeout.seconds) { + this.defaultHttpClient = null this.defaultTimeout = timeout this.defaultHttpClient = getHttpClient() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt index 9b9c667f0..08617f8fb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt @@ -106,7 +106,7 @@ class Relay( private var connectingBlock = AtomicBoolean() fun connectAndRun(onConnected: (Relay) -> Unit) { - Log.d("Relay", "Relay.connect $url") + Log.d("Relay", "Relay.connect $url hasProxy: ${this.httpClient.proxy != null}") // BRB is crashing OkHttp Deflater object :( if (url.contains("brb.io")) return From 281d86b4bf90eedccc83bf939e50eb396e479516 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 27 Jan 2024 11:58:39 -0500 Subject: [PATCH 003/176] Small refactoring on the HTTP client --- .../amethyst/LocalPreferences.kt | 4 +- .../vitorpamplona/amethyst/ServiceManager.kt | 6 +-- .../amethyst/service/CashuProcessor.kt | 4 +- .../{HttpClient.kt => HttpClientManager.kt} | 47 ++++++++----------- .../service/Nip05NostrAddressVerifier.kt | 2 +- .../service/Nip11RelayInfoRetriever.kt | 2 +- .../amethyst/service/Nip96MediaServers.kt | 2 +- .../amethyst/service/Nip96Uploader.kt | 6 +-- .../amethyst/service/OnlineCheck.kt | 2 +- .../service/lnurl/LightningAddressResolver.kt | 4 +- .../service/notifications/RegisterAccounts.kt | 4 +- .../service/playback/PlaybackService.kt | 10 ++-- .../service/previews/UrlPreviewUtils.kt | 4 +- .../amethyst/service/relays/Relay.kt | 4 +- .../vitorpamplona/amethyst/ui/MainActivity.kt | 6 +-- .../amethyst/ui/actions/ImageSaver.kt | 4 +- .../ui/screen/AccountStateViewModel.kt | 6 +-- .../ui/screen/loggedIn/AccountViewModel.kt | 4 +- 18 files changed, 57 insertions(+), 64 deletions(-) rename app/src/main/java/com/vitorpamplona/amethyst/service/{HttpClient.kt => HttpClientManager.kt} (76%) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 9edba1452..af3201fb3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -40,7 +40,7 @@ import com.vitorpamplona.amethyst.model.ThemeType import com.vitorpamplona.amethyst.model.parseBooleanType import com.vitorpamplona.amethyst.model.parseConnectivityType import com.vitorpamplona.amethyst.model.parseThemeType -import com.vitorpamplona.amethyst.service.HttpClient +import com.vitorpamplona.amethyst.service.HttpClientManager import com.vitorpamplona.amethyst.service.Nip96MediaServers import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.quartz.crypto.KeyPair @@ -570,7 +570,7 @@ object LocalPreferences { val hideNIP24WarningDialog = getBoolean(PrefKeys.HIDE_NIP_24_WARNING_DIALOG, false) val useProxy = getBoolean(PrefKeys.USE_PROXY, false) val proxyPort = getInt(PrefKeys.PROXY_PORT, 9050) - val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort) + val proxy = HttpClientManager.initProxy(useProxy, "127.0.0.1", proxyPort) val showSensitiveContent = if (contains(PrefKeys.SHOW_SENSITIVE_CONTENT)) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt index 9e3fc4a48..7b332b5df 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt @@ -30,7 +30,7 @@ import coil.decode.SvgDecoder import coil.size.Precision import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.service.HttpClient +import com.vitorpamplona.amethyst.service.HttpClientManager import com.vitorpamplona.amethyst.service.NostrAccountDataSource import com.vitorpamplona.amethyst.service.NostrChannelDataSource import com.vitorpamplona.amethyst.service.NostrChatroomDataSource @@ -83,7 +83,7 @@ class ServiceManager { val myAccount = account // Resets Proxy Use - HttpClient.start(account?.proxy) + HttpClientManager.setDefaultProxy(account?.proxy) LocalCache.antiSpam.active = account?.filterSpamFromStrangers ?: true Coil.setImageLoader { Amethyst.instance @@ -96,7 +96,7 @@ class ServiceManager { } add(SvgDecoder.Factory()) } // .logger(DebugLogger()) - .okHttpClient { HttpClient.getHttpClient() } + .okHttpClient { HttpClientManager.getHttpClient() } .precision(Precision.INEXACT) .respectCacheHeaders(false) .build() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt index f19a4cf6a..3762505aa 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt @@ -119,7 +119,7 @@ class CashuProcessor { checkNotInMainThread() try { - val client = HttpClient.getHttpClient() + val client = HttpClientManager.getHttpClient() val url = "$mintAddress/checkfees" // Melt cashu tokens at Mint val factory = Event.mapper.nodeFactory @@ -170,7 +170,7 @@ class CashuProcessor { context: Context, ) { try { - val client = HttpClient.getHttpClient() + val client = HttpClientManager.getHttpClient() val url = token.mint + "/melt" // Melt cashu tokens at Mint val factory = Event.mapper.nodeFactory diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/HttpClient.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/HttpClientManager.kt similarity index 76% rename from app/src/main/java/com/vitorpamplona/amethyst/service/HttpClient.kt rename to app/src/main/java/com/vitorpamplona/amethyst/service/HttpClientManager.kt index 64c77e914..c89b40399 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/HttpClient.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/HttpClientManager.kt @@ -32,14 +32,13 @@ import java.net.Proxy import java.time.Duration import kotlin.properties.Delegates -object HttpClient { - val DEFAULT_TIMEOUT_ON_WIFI = Duration.ofSeconds(10L) - val DEFAULT_TIMEOUT_ON_MOBILE = Duration.ofSeconds(30L) +object HttpClientManager { + val DEFAULT_TIMEOUT_ON_WIFI: Duration = Duration.ofSeconds(10L) + val DEFAULT_TIMEOUT_ON_MOBILE: Duration = Duration.ofSeconds(30L) var proxyChangeListeners = ArrayList<() -> Unit>() - var defaultTimeout = DEFAULT_TIMEOUT_ON_WIFI - - var defaultHttpClient: OkHttpClient? = null + private var defaultTimeout = DEFAULT_TIMEOUT_ON_WIFI + private var defaultHttpClient: OkHttpClient? = null // fires off every time value of the property changes private var internalProxy: Proxy? by @@ -49,29 +48,34 @@ object HttpClient { } } - fun start(proxy: Proxy?) { + fun setDefaultProxy(proxy: Proxy?) { if (internalProxy != proxy) { Log.d("HttpClient", "Changing proxy to: ${proxy != null}") - this.defaultHttpClient = null this.internalProxy = proxy - this.defaultHttpClient = getHttpClient() + + // recreates singleton + this.defaultHttpClient = buildHttpClient(internalProxy, defaultTimeout) } } - fun changeTimeouts(timeout: Duration) { + fun setDefaultTimeout(timeout: Duration) { Log.d("HttpClient", "Changing timeout to: $timeout") if (this.defaultTimeout.seconds != timeout.seconds) { - this.defaultHttpClient = null this.defaultTimeout = timeout - this.defaultHttpClient = getHttpClient() + + // recreates singleton + this.defaultHttpClient = buildHttpClient(internalProxy, defaultTimeout) } } - fun getHttpClient(timeout: Duration): OkHttpClient { - val seconds = if (internalProxy != null) timeout.seconds * 2 else timeout.seconds + private fun buildHttpClient( + proxy: Proxy?, + timeout: Duration, + ): OkHttpClient { + val seconds = if (proxy != null) timeout.seconds * 2 else timeout.seconds val duration = Duration.ofSeconds(seconds) return OkHttpClient.Builder() - .proxy(internalProxy) + .proxy(proxy) .readTimeout(duration) .connectTimeout(duration) .writeTimeout(duration) @@ -94,24 +98,13 @@ object HttpClient { } } - fun getHttpClientForRelays(): OkHttpClient { - if (this.defaultHttpClient == null) { - this.defaultHttpClient = getHttpClient(defaultTimeout) - } - return defaultHttpClient!! - } - fun getHttpClient(): OkHttpClient { if (this.defaultHttpClient == null) { - this.defaultHttpClient = getHttpClient(defaultTimeout) + this.defaultHttpClient = buildHttpClient(internalProxy, defaultTimeout) } return defaultHttpClient!! } - fun getProxy(): Proxy? { - return internalProxy - } - fun initProxy( useProxy: Boolean, hostname: String, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt index fa97b0a93..3498f9518 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt @@ -64,7 +64,7 @@ class Nip05NostrAddressVerifier() { .url(url) .build() - HttpClient.getHttpClient() + HttpClientManager.getHttpClient() .newCall(request) .enqueue( object : Callback { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt index 93815bf0e..07fb77fe5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt @@ -84,7 +84,7 @@ class Nip11Retriever { val request: Request = Request.Builder().header("Accept", "application/nostr+json").url(url).build() - HttpClient.getHttpClient() + HttpClientManager.getHttpClient() .newCall(request) .enqueue( object : Callback { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip96MediaServers.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip96MediaServers.kt index be1fac14f..d024bb748 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip96MediaServers.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip96MediaServers.kt @@ -87,7 +87,7 @@ class Nip96Retriever { .url(baseUrl.removeSuffix("/") + "/.well-known/nostr/nip96.json") .build() - HttpClient.getHttpClient().newCall(request).execute().use { response -> + HttpClientManager.getHttpClient().newCall(request).execute().use { response -> checkNotInMainThread() response.use { val body = it.body.string() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip96Uploader.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip96Uploader.kt index c4552a3a0..e66133d44 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip96Uploader.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip96Uploader.kt @@ -128,7 +128,7 @@ class Nip96Uploader(val account: Account?) { val extension = contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: "" - val client = HttpClient.getHttpClient() + val client = HttpClientManager.getHttpClient() val requestBody: RequestBody val requestBuilder = Request.Builder() @@ -194,7 +194,7 @@ class Nip96Uploader(val account: Account?) { val extension = contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: "" - val client = HttpClient.getHttpClient() + val client = HttpClientManager.getHttpClient() val requestBuilder = Request.Builder() @@ -227,7 +227,7 @@ class Nip96Uploader(val account: Account?) { server: Nip96Retriever.ServerInfo, onProgress: (percentage: Float) -> Unit, ): PartialEvent { - val client = HttpClient.getHttpClient() + val client = HttpClientManager.getHttpClient() var currentResult = result while (!result.processingUrl.isNullOrBlank() && (currentResult.percentage ?: 100) < 100) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt index 937efa8c4..fae525ad6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt @@ -59,7 +59,7 @@ object OnlineChecker { .build() val result = - HttpClient.getHttpClient().newCall(request).execute().use { + HttpClientManager.getHttpClient().newCall(request).execute().use { checkNotInMainThread() it.isSuccessful } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt index 1615a0dab..3816cc4eb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt @@ -24,7 +24,7 @@ import android.content.Context import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.service.HttpClient +import com.vitorpamplona.amethyst.service.HttpClientManager import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.quartz.encoders.LnInvoiceUtil import com.vitorpamplona.quartz.encoders.Lud06 @@ -35,7 +35,7 @@ import java.math.RoundingMode import java.net.URLEncoder class LightningAddressResolver() { - val client = HttpClient.getHttpClient() + val client = HttpClientManager.getHttpClient() fun assembleUrl(lnaddress: String): String? { val parts = lnaddress.split("@") diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt index 53df87b4d..903b94f95 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.service.HttpClient +import com.vitorpamplona.amethyst.service.HttpClientManager import com.vitorpamplona.quartz.events.RelayAuthEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -106,7 +106,7 @@ class RegisterAccounts( .post(body) .build() - val client = HttpClient.getHttpClient() + val client = HttpClientManager.getHttpClient() val isSucess = client.newCall(request).execute().use { it.isSuccessful } } catch (e: java.lang.Exception) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/playback/PlaybackService.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/playback/PlaybackService.kt index 82dfc93b2..ab9f276f9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/playback/PlaybackService.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/playback/PlaybackService.kt @@ -31,7 +31,7 @@ import androidx.media3.exoplayer.source.ProgressiveMediaSource import androidx.media3.session.MediaSession import androidx.media3.session.MediaSessionService import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.service.HttpClient +import com.vitorpamplona.amethyst.service.HttpClientManager @UnstableApi // Extend MediaSessionService class PlaybackService : MediaSessionService() { @@ -42,12 +42,12 @@ class PlaybackService : MediaSessionService() { private var managerLocal: MultiPlayerPlaybackManager? = null fun newHslDataSource(): MediaSource.Factory { - return HlsMediaSource.Factory(OkHttpDataSource.Factory(HttpClient.getHttpClient())) + return HlsMediaSource.Factory(OkHttpDataSource.Factory(HttpClientManager.getHttpClient())) } fun newProgressiveDataSource(): MediaSource.Factory { return ProgressiveMediaSource.Factory( - (applicationContext as Amethyst).videoCache.get(HttpClient.getHttpClient()), + (applicationContext as Amethyst).videoCache.get(HttpClientManager.getHttpClient()), ) } @@ -90,7 +90,7 @@ class PlaybackService : MediaSessionService() { Log.d("Lifetime Event", "PlaybackService.onCreate") // Stop all videos and recreates all managers when the proxy changes. - HttpClient.proxyChangeListeners.add(this@PlaybackService::onProxyUpdated) + HttpClientManager.proxyChangeListeners.add(this@PlaybackService::onProxyUpdated) } private fun onProxyUpdated() { @@ -114,7 +114,7 @@ class PlaybackService : MediaSessionService() { override fun onDestroy() { Log.d("Lifetime Event", "PlaybackService.onDestroy") - HttpClient.proxyChangeListeners.remove(this@PlaybackService::onProxyUpdated) + HttpClientManager.proxyChangeListeners.remove(this@PlaybackService::onProxyUpdated) managerHls?.releaseAppPlayers() managerLocal?.releaseAppPlayers() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreviewUtils.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreviewUtils.kt index ae521cfee..f028f5804 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreviewUtils.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreviewUtils.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.previews -import com.vitorpamplona.amethyst.service.HttpClient +import com.vitorpamplona.amethyst.service.HttpClientManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.MediaType @@ -90,7 +90,7 @@ suspend fun getDocument( ): UrlInfoItem = withContext(Dispatchers.IO) { val request: Request = Request.Builder().url(url).get().build() - HttpClient.getHttpClient().newCall(request).execute().use { + HttpClientManager.getHttpClient().newCall(request).execute().use { if (it.isSuccessful) { val mimeType = it.headers.get("Content-Type")?.toMediaType() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt index 08617f8fb..e8a2b632e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.service.relays import android.util.Log import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.model.RelayBriefInfoCache -import com.vitorpamplona.amethyst.service.HttpClient +import com.vitorpamplona.amethyst.service.HttpClientManager import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.events.Event @@ -62,7 +62,7 @@ class Relay( const val RECONNECTING_IN_SECONDS = 60 * 3 } - private val httpClient = HttpClient.getHttpClientForRelays() + private val httpClient = HttpClientManager.getHttpClient() private var listeners = setOf() private var socket: WebSocket? = null diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 21402b7a6..0303adc68 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -45,7 +45,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.google.accompanist.adaptive.calculateDisplayFeatures import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.ServiceManager -import com.vitorpamplona.amethyst.service.HttpClient +import com.vitorpamplona.amethyst.service.HttpClientManager import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils import com.vitorpamplona.amethyst.ui.components.DEFAULT_MUTED_SETTING @@ -236,9 +236,9 @@ class MainActivity : AppCompatActivity() { if (changedNetwork) { if (isOnMobileData) { - HttpClient.changeTimeouts(HttpClient.DEFAULT_TIMEOUT_ON_MOBILE) + HttpClientManager.setDefaultTimeout(HttpClientManager.DEFAULT_TIMEOUT_ON_MOBILE) } else { - HttpClient.changeTimeouts(HttpClient.DEFAULT_TIMEOUT_ON_WIFI) + HttpClientManager.setDefaultTimeout(HttpClientManager.DEFAULT_TIMEOUT_ON_WIFI) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageSaver.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageSaver.kt index d9e47197f..ad83dc860 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageSaver.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageSaver.kt @@ -30,7 +30,7 @@ import android.provider.MediaStore import android.webkit.MimeTypeMap import androidx.annotation.RequiresApi import com.vitorpamplona.amethyst.BuildConfig -import com.vitorpamplona.amethyst.service.HttpClient +import com.vitorpamplona.amethyst.service.HttpClientManager import okhttp3.Call import okhttp3.Callback import okhttp3.Request @@ -55,7 +55,7 @@ object ImageSaver { onSuccess: () -> Any?, onError: (Throwable) -> Any?, ) { - val client = HttpClient.getHttpClient() + val client = HttpClientManager.getHttpClient() val request = Request.Builder() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index fae3317c5..3d2a3caca 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.ServiceManager import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.service.HttpClient +import com.vitorpamplona.amethyst.service.HttpClientManager import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.Hex @@ -85,7 +85,7 @@ class AccountStateViewModel() : ViewModel() { ) = withContext(Dispatchers.IO) { val parsed = Nip19.uriToRoute(key) val pubKeyParsed = parsed?.hex?.hexToByteArray() - val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort) + val proxy = HttpClientManager.initProxy(useProxy, "127.0.0.1", proxyPort) if (loginWithExternalSigner && pubKeyParsed == null) { throw Exception("Invalid key while trying to login with external signer") @@ -216,7 +216,7 @@ class AccountStateViewModel() : ViewModel() { name: String? = null, ) { viewModelScope.launch(Dispatchers.IO) { - val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort) + val proxy = HttpClientManager.initProxy(useProxy, "127.0.0.1", proxyPort) val keyPair = KeyPair() val account = Account( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index dc7dd7ed1..20561019b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -49,7 +49,7 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.amethyst.service.CashuProcessor import com.vitorpamplona.amethyst.service.CashuToken -import com.vitorpamplona.amethyst.service.HttpClient +import com.vitorpamplona.amethyst.service.HttpClientManager import com.vitorpamplona.amethyst.service.Nip05NostrAddressVerifier import com.vitorpamplona.amethyst.service.Nip11CachedRetriever import com.vitorpamplona.amethyst.service.Nip11Retriever @@ -1042,7 +1042,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View ) { viewModelScope.launch(Dispatchers.IO) { account.proxyPort = portNumber.value.toInt() - account.proxy = HttpClient.initProxy(checked, "127.0.0.1", account.proxyPort) + account.proxy = HttpClientManager.initProxy(checked, "127.0.0.1", account.proxyPort) account.saveable.invalidateData() serviceManager?.forceRestart() } From 2ff090dac1ab8c6864571cfee908d044df3d9e1a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 29 Jan 2024 11:03:15 -0500 Subject: [PATCH 004/176] New NIP available --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 429514296..bd1be6033 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ -Amethyst brings the best social network to your Android phone. Just insert your Nostr private key and start posting. +Amethyst brings the best social network to your Android phone. Just insert your Nostr private key and start posting. [Get it on Obtaininum](https://github.com/vitorpamplona/amethyst/releases) - [ ] Nostr Connect (NIP-46) - [x] Wallet Connect API (NIP-47) - [ ] Proxy Tags (NIP-48, Not applicable) +- [ ] Private key encryption for import/export (NIP-49) - [x] Online Relay Search (NIP-50) - [x] Lists (NIP-51) - [ ] Calendar Events (NIP-52) @@ -103,7 +104,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases) # Development Overview -## Overall Architecture +## Overall Architecture This is a native Android app made with Kotlin and Jetpack Compose. The app uses a modified version of the [nostrpostrlib](https://github.com/Giszmo/NostrPostr/tree/master/nostrpostrlib) to talk to Nostr relays. @@ -186,7 +187,7 @@ openssl base64 < | tr -d '\n' | tee some_signing_key.j 8. Download the `aab` file and upload it to the` PlayStore. # Privacy on Relays & nostr -Your internet protocol (IP) address is exposed to the relays you connect to. If you want to improve your privacy, consider utilizing a service that masks your IP address (e.g. a VPN) from trackers online. +Your internet protocol (IP) address is exposed to the relays you connect to. If you want to improve your privacy, consider utilizing a service that masks your IP address (e.g. a VPN) from trackers online. The relay also learns which public keys you are requesting, meaning your public key will be tied to your IP address. @@ -197,11 +198,11 @@ While the content of direct messages (DMs) is only visible to you and your DM co # Visibility & Permanence of Your Content on nostr ## Information Visibility ## -Content that you share can be shared to other relays. +Content that you share can be shared to other relays. Information that you share publicly is visible to anyone reading from relays that have your information. Your information may also be visible to nostr users who do not share relays with you. ## Information Permanence ## -Information shared on nostr should be assumed permanent for privacy purposes. There is no way to guarantee edit or deletion of any content once posted. +Information shared on nostr should be assumed permanent for privacy purposes. There is no way to guarantee edit or deletion of any content once posted. # Screenshots From 3ed45f282cbb894104638ad4d1d08b4fe14d0d1d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 29 Jan 2024 11:19:56 -0500 Subject: [PATCH 005/176] Fixing NIP number for wraps/seals --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bd1be6033..742dce71d 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases) - [x] Zaps (NIP-57) - [x] Private Zaps - [x] Zap Splits (NIP-57) +- [x] Gift Wraps & Seals (NIP-59) - [x] Zapraiser (NIP-TBD) - [x] Badges (NIP-58) - [ ] Relay List Metadata (NIP-65) @@ -86,7 +87,6 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases) - [x] HTTP Auth (NIP-98) - [x] Classifieds (NIP-99) - [x] Private Messages and Small Groups (NIP-24/Draft) -- [x] Gift Wraps & Seals (NIP-59/Draft) - [x] Versioned Encrypted Payloads (NIP-44/Draft) - [x] Audio Tracks (zapstr.live) (Kind:31337) - [x] Push Notifications (Google and Unified Push) From 14e9a91d40b24070287b525fb30a59e5c192fcca Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 29 Jan 2024 16:28:53 -0500 Subject: [PATCH 006/176] Updates dependencies --- app/build.gradle | 8 ++++---- .../amethyst/ui/components/ClickableRoute.kt | 4 ++-- build.gradle | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index e068e8fa1..6c569abd6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -181,7 +181,7 @@ dependencies { // Adaptive Layout / Two Pane implementation "androidx.compose.material3:material3-window-size-class:${material3_version}" - implementation "com.google.accompanist:accompanist-adaptive:0.32.0" + implementation 'com.google.accompanist:accompanist-adaptive:0.34.0' // Lifecycle @@ -251,7 +251,7 @@ dependencies { playImplementation 'com.google.mlkit:translate:17.0.2' // PushNotifications - playImplementation platform('com.google.firebase:firebase-bom:32.7.0') + playImplementation platform('com.google.firebase:firebase-bom:32.7.1') playImplementation 'com.google.firebase:firebase-messaging-ktx' //PushNotifications(FDroid) @@ -276,8 +276,8 @@ dependencies { testImplementation 'junit:junit:4.13.2' testImplementation 'io.mockk:mockk:1.13.9' - androidTestImplementation 'androidx.test.ext:junit:1.2.0-alpha02' - androidTestImplementation 'androidx.test.ext:junit-ktx:1.2.0-alpha02' + androidTestImplementation 'androidx.test.ext:junit:1.2.0-alpha03' + androidTestImplementation 'androidx.test.ext:junit-ktx:1.2.0-alpha03' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version" debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version" diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt index dab4b350c..3ca69d4a1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt @@ -388,7 +388,7 @@ fun CreateTextWithEmoji( .merge( TextStyle( color = textColor, - textAlign = textAlign, + textAlign = TextAlign.Unspecified, fontWeight = fontWeight, fontSize = fontSize, ), @@ -446,7 +446,7 @@ fun CreateTextWithEmoji( .merge( TextStyle( color = textColor, - textAlign = textAlign, + textAlign = TextAlign.Unspecified, fontWeight = fontWeight, fontSize = fontSize, ), diff --git a/build.gradle b/build.gradle index 288b9cda8..219ce3db4 100644 --- a/build.gradle +++ b/build.gradle @@ -3,14 +3,14 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile buildscript { ext { fragment_version = "1.6.2" - lifecycle_version = '2.6.2' - compose_ui_version = '1.5.4' + lifecycle_version = '2.7.0' + compose_ui_version = '1.6.0' nav_version = '2.7.6' room_version = "2.4.3" - accompanist_version = '0.32.0' + accompanist_version = '0.34.0' coil_version = '2.5.0' vico_version = '1.13.1' - media3_version = '1.2.0' + media3_version = '1.2.1' core_ktx_version = '1.12.0' material3_version = '1.1.2' } From d88eb014b7fc50664ab7a2ab47b2156cc06fb298 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 29 Jan 2024 17:14:31 -0500 Subject: [PATCH 007/176] Adding the exception to the Zap error message. --- .../amethyst/service/lnurl/LightningAddressResolver.kt | 3 ++- app/src/main/res/values/strings.xml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt index 3816cc4eb..0b5f9a5c9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt @@ -101,9 +101,10 @@ class LightningAddressResolver() { context.getString(R.string.error_unable_to_fetch_invoice), context.getString( R.string - .could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct, + .could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct_exception, url, lnaddress, + e.suppressedExceptions.getOrNull(0)?.message ?: e.cause?.message ?: e.message, ), ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ee7f9a7fe..de1131a51 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -625,6 +625,7 @@ Could not assemble LNUrl from Lightning Address \"%1$s\". Check the user\'s setup The receiver\'s lightning service at %1$s is not available. It was calculated from the lightning address \"%2$s\". Error: %3$s. Check if the server is up and if the lightning address is correct Could not resolve %1$s. Check if you are connected, if the server is up and if the lightning address %2$s is correct + Could not resolve %1$s. Check if you are connected, if the server is up and if the lightning address %2$s is correct.\n\nException was: %3$s Could not fetch invoice from %1$s Error Parsing JSON from Lightning Address. Check the user\'s lightning setup Callback URL not found in the User\'s lightning address server configuration From 6638defbb93c91ae22e7e9a9f88f4e67a44f0668 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 29 Jan 2024 18:26:25 -0500 Subject: [PATCH 008/176] Fixes content sensitivity for NIP-54 images. --- .../amethyst/service/CachedRichTextParser.kt | 2 ++ .../amethyst/ui/actions/NewMediaView.kt | 1 + .../amethyst/ui/actions/NewPostView.kt | 7 ++-- .../amethyst/ui/actions/NewPostViewModel.kt | 6 ++-- .../ui/components/SensitivityWarning.kt | 11 +++++- .../ui/components/ZoomableContentView.kt | 36 +++++++++++-------- 6 files changed, 39 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt index c6097c5a4..bc420f8d2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt @@ -103,6 +103,7 @@ class RichTextParser() { hash = frags["x"], blurhash = frags["blurhash"], dim = frags["dim"], + contentWarning = frags["content-warning"], ) } else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) { val frags = Nip44UrlParser().parse(fullUrl) @@ -112,6 +113,7 @@ class RichTextParser() { hash = frags["x"], blurhash = frags["blurhash"], dim = frags["dim"], + contentWarning = frags["content-warning"], ) } else { null diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt index 0fde17743..52f2d77b8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt @@ -279,6 +279,7 @@ fun ImageVideoPost( modifier = Modifier.fillMaxWidth(), ) { SettingSwitchItem( + modifier = Modifier.fillMaxWidth().padding(8.dp), checked = postViewModel.sensitiveContent, onCheckedChange = { postViewModel.sensitiveContent = it }, title = R.string.add_sensitive_content_label, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index 22b8b5a8a..e756677d1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -1716,6 +1716,7 @@ fun ImageVideoDescription( modifier = Modifier.fillMaxWidth(), ) { SettingSwitchItem( + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), checked = sensitiveContent, onCheckedChange = { sensitiveContent = it }, title = R.string.add_sensitive_content_label, @@ -1764,7 +1765,7 @@ fun ImageVideoDescription( @Composable fun SettingSwitchItem( - modifier: Modifier = Modifier, + modifier: Modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), checked: Boolean, onCheckedChange: (Boolean) -> Unit, title: Int, @@ -1774,8 +1775,6 @@ fun SettingSwitchItem( Row( modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp) .toggleable( value = checked, enabled = enabled, @@ -1786,7 +1785,7 @@ fun SettingSwitchItem( ) { Column( modifier = Modifier.weight(1.0f), - verticalArrangement = Arrangement.spacedBy(3.dp), + verticalArrangement = Arrangement.spacedBy(Size5dp), ) { Text( text = stringResource(id = title), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index 7e16320f4..20f7c59ff 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -264,7 +264,7 @@ open class NewPostViewModel() : ViewModel() { } val urls = findURLs(tagger.message) - val usedAttachments = nip94attachments.filter { it.urls().intersect(urls).isNotEmpty() } + val usedAttachments = nip94attachments.filter { it.urls().intersect(urls.toSet()).isNotEmpty() } usedAttachments.forEach { account?.sendHeader(it, relayList, {}) } if (originalNote?.channelHex() != null) { @@ -846,9 +846,7 @@ open class NewPostViewModel() : ViewModel() { alt?.ifBlank { null }?.let { "alt=${URLEncoder.encode(it, "utf-8")}" }, blurHash?.ifBlank { null }?.let { "blurhash=${URLEncoder.encode(it, "utf-8")}" }, x?.ifBlank { null }?.let { "x=${URLEncoder.encode(it, "utf-8")}" }, - sensitiveContent - ?.ifBlank { null } - ?.let { "content-warning=${URLEncoder.encode(it, "utf-8")}" }, + sensitiveContent?.let { "content-warning=${URLEncoder.encode(it, "utf-8")}" }, ) .joinToString("&") diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt index 008cb5792..770a681bc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt @@ -74,8 +74,17 @@ fun SensitivityWarning( accountViewModel: AccountViewModel, content: @Composable () -> Unit, ) { - val hasSensitiveContent = remember(event) { event.isSensitive() ?: false } + val hasSensitiveContent = remember(event) { event.isSensitive() } + SensitivityWarning(hasSensitiveContent, accountViewModel, content) +} + +@Composable +fun SensitivityWarning( + hasSensitiveContent: Boolean, + accountViewModel: AccountViewModel, + content: @Composable () -> Unit, +) { if (hasSensitiveContent) { SensitivityWarning(accountViewModel, content) } else { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index b002f1854..9b89696d9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -161,6 +161,7 @@ class ZoomableUrlImage( val blurhash: String? = null, dim: String? = null, uri: String? = null, + val contentWarning: String? = null, ) : ZoomableUrlContent(url, description, hash, dim, uri) @Immutable @@ -173,6 +174,7 @@ class ZoomableUrlVideo( val artworkUri: String? = null, val authorName: String? = null, val blurhash: String? = null, + val contentWarning: String? = null, ) : ZoomableUrlContent(url, description, hash, dim, uri) @Immutable @@ -265,20 +267,24 @@ fun ZoomableContentView( when (content) { is ZoomableUrlImage -> - UrlImageView(content, mainImageModifier, accountViewModel = accountViewModel) + SensitivityWarning(content.contentWarning != null, accountViewModel) { + UrlImageView(content, mainImageModifier, accountViewModel = accountViewModel) + } is ZoomableUrlVideo -> - VideoView( - videoUri = content.url, - title = content.description, - artworkUri = content.artworkUri, - authorName = content.authorName, - dimensions = content.dim, - blurhash = content.blurhash, - roundedCorner = roundedCorner, - nostrUriCallback = content.uri, - onDialog = { dialogOpen = true }, - accountViewModel = accountViewModel, - ) + SensitivityWarning(content.contentWarning != null, accountViewModel) { + VideoView( + videoUri = content.url, + title = content.description, + artworkUri = content.artworkUri, + authorName = content.authorName, + dimensions = content.dim, + blurhash = content.blurhash, + roundedCorner = roundedCorner, + nostrUriCallback = content.uri, + onDialog = { dialogOpen = true }, + accountViewModel = accountViewModel, + ) + } is ZoomableLocalImage -> LocalImageView(content, mainImageModifier, accountViewModel = accountViewModel) is ZoomableLocalVideo -> @@ -387,13 +393,13 @@ private fun UrlImageView( val myModifier = remember { mainImageModifier.widthIn(max = maxWidth).heightIn(max = maxHeight) - /* Is this necessary? It makes images bleed into other pages + /* Is this necessary? It makes images bleed into other pages .run { aspectRatio(content.dim)?.let { ratio -> this.aspectRatio(ratio, false) } ?: this } - */ + */ } val contentScale = From 370a18f716deb769b0326c23d89732570bd9e460 Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Tue, 30 Jan 2024 13:30:25 +0000 Subject: [PATCH 009/176] Correct GR locale config --- app/build.gradle | 2 +- app/src/main/res/xml/locales_config.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 6c569abd6..1537ccf59 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -26,7 +26,7 @@ android { 'cy-rGB', 'da-rDK', 'de', - 'el-rGB', + 'el-rGR', 'en-rGB', 'eo', 'es', diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml index 23d8f76e2..b1bd71d23 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -6,7 +6,7 @@ - + From 998aa2aed11db303baf073b2983d1a24d6216cd8 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 30 Jan 2024 09:44:48 -0500 Subject: [PATCH 010/176] Adding GitStr --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 742dce71d..89a4dd6c8 100644 --- a/README.md +++ b/README.md @@ -212,6 +212,8 @@ Information shared on nostr should be assumed permanent for privacy purposes. Th # Contributing +You can send patches in Nostr using [GitStr](https://github.com/fiatjaf/gitstr) to naddr1qqyxzmt9w358jum5qyg8v6t5daezumn0wd68yvfwvdhk6qg7waehxw309ahx7um5wgkhqatz9emk2mrvdaexgetj9ehx2ap0qy2hwumn8ghj7un9d3shjtnwdaehgu3wvfnj7q3qgcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqxpqqqpmej720gac + [Issues](https://github.com/vitorpamplona/amethyst/issues) and [pull requests](https://github.com/vitorpamplona/amethyst/pulls) are very welcome. ## Contributors From 312d82fdf6d47fda661ec3ea8d6abf5e8e5ffb8b Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 30 Jan 2024 09:47:31 -0500 Subject: [PATCH 011/176] moving the order or contributing --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 89a4dd6c8..801799c46 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,13 @@ For the Play build: ./gradlew installPlayDebug ``` +## Contributing + +You can send patches in Nostr using [GitStr](https://github.com/fiatjaf/gitstr) to naddr1qqyxzmt9w358jum5qyg8v6t5daezumn0wd68yvfwvdhk6qg7waehxw309ahx7um5wgkhqatz9emk2mrvdaexgetj9ehx2ap0qy2hwumn8ghj7un9d3shjtnwdaehgu3wvfnj7q3qgcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqxpqqqpmej720gac + +[Issues](https://github.com/vitorpamplona/amethyst/issues) and [pull requests](https://github.com/vitorpamplona/amethyst/pulls) here are also very welcome. + + ## How to Deploy 1. Generate a new signing key @@ -210,12 +217,6 @@ Information shared on nostr should be assumed permanent for privacy purposes. Th |-------------------------------------------|----------------------------------------------|-------------------------------------------------|--------------------------------------------------------| | ![Home Feed](./docs/screenshots/home.png) | ![Messages](./docs/screenshots/messages.png) | ![Live Streams](./docs/screenshots/replies.png) | ![Notifications](./docs/screenshots/notifications.png) | -# Contributing - -You can send patches in Nostr using [GitStr](https://github.com/fiatjaf/gitstr) to naddr1qqyxzmt9w358jum5qyg8v6t5daezumn0wd68yvfwvdhk6qg7waehxw309ahx7um5wgkhqatz9emk2mrvdaexgetj9ehx2ap0qy2hwumn8ghj7un9d3shjtnwdaehgu3wvfnj7q3qgcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqxpqqqpmej720gac - -[Issues](https://github.com/vitorpamplona/amethyst/issues) and [pull requests](https://github.com/vitorpamplona/amethyst/pulls) are very welcome. - ## Contributors From e59f676d47c255e889ba00bddae2998392a724d6 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 30 Jan 2024 11:00:33 -0500 Subject: [PATCH 012/176] updates secp256k1 --- quartz/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/build.gradle b/quartz/build.gradle index 2e0d0cf20..c01833fb1 100644 --- a/quartz/build.gradle +++ b/quartz/build.gradle @@ -43,7 +43,7 @@ dependencies { implementation "androidx.compose.runtime:runtime:$compose_ui_version" // Bitcoin secp256k1 bindings to Android - api 'fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.12.0' + api 'fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.13.0' // LibSodium for ChaCha encryption implementation "com.goterl:lazysodium-android:5.1.0@aar" From 1ac990b67c0e1314715dda41e4dcb0ed799f0f63 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 30 Jan 2024 11:20:25 -0500 Subject: [PATCH 013/176] Set account to avoid lateinit property account has not been initialized --- .../com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt | 2 +- .../amethyst/ui/screen/loggedIn/ChatroomListScreen.kt | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index a22826de7..8bd63e6f9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -580,7 +580,7 @@ fun BechLink( ) } } else if (loadedLink?.nip19 != null) { - Row { ClickableRoute(loadedLink?.nip19!!, accountViewModel, nav) } + ClickableRoute(loadedLink?.nip19!!, accountViewModel, nav) } else { val text = remember(word) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt index 4f8965bfc..2c29bcc8d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt @@ -225,6 +225,7 @@ fun ChatroomListScreenOnlyList( val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_RESUME) { + NostrChatroomListDataSource.account = accountViewModel.account NostrChatroomListDataSource.start() } } @@ -303,6 +304,7 @@ fun WatchAccountForListScreen( ) { LaunchedEffect(accountViewModel) { launch(Dispatchers.IO) { + NostrChatroomListDataSource.account = accountViewModel.account NostrChatroomListDataSource.start() knownFeedViewModel.invalidateData(true) newFeedViewModel.invalidateData(true) From 277bf0a603a2b0e4ac600cf31aa024453ce2c71d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 30 Jan 2024 11:51:02 -0500 Subject: [PATCH 014/176] Fixes misalignment of some links to users in the middle of the note. --- .../vitorpamplona/amethyst/ui/components/ClickableRoute.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt index 3ca69d4a1..a69879fae 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt @@ -280,12 +280,10 @@ private fun RenderUserAsClickableText( derivedStateOf { userState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() } } - val addedCharts = remember(nip19) { "${nip19.additionalChars}" } - userDisplayName?.let { CreateClickableTextWithEmoji( clickablePart = it, - suffix = addedCharts, + suffix = nip19.additionalChars, maxLines = 1, route = route, nav = nav, @@ -335,6 +333,7 @@ fun CreateClickableText( ClickableText( text = text, maxLines = maxLines, + style = currentStyle, onClick = { nav(route) }, ) } From 75d703ac0a997d26faff67f9d028148827ae1524 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 30 Jan 2024 09:52:23 -0500 Subject: [PATCH 015/176] Testing GitStr with a Readme patch --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 801799c46..8fc448bb4 100644 --- a/README.md +++ b/README.md @@ -169,11 +169,10 @@ For the Play build: ## Contributing -You can send patches in Nostr using [GitStr](https://github.com/fiatjaf/gitstr) to naddr1qqyxzmt9w358jum5qyg8v6t5daezumn0wd68yvfwvdhk6qg7waehxw309ahx7um5wgkhqatz9emk2mrvdaexgetj9ehx2ap0qy2hwumn8ghj7un9d3shjtnwdaehgu3wvfnj7q3qgcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqxpqqqpmej720gac +You can send patches in Nostr using [GitStr](https://github.com/fiatjaf/gitstr) to [this address](nostr:naddr1qqyxzmt9w358jum5qyg8v6t5daezumn0wd68yvfwvdhk6qg7waehxw309ahx7um5wgkhqatz9emk2mrvdaexgetj9ehx2ap0qy2hwumn8ghj7un9d3shjtnwdaehgu3wvfnj7q3qgcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqxpqqqpmej720gac) [Issues](https://github.com/vitorpamplona/amethyst/issues) and [pull requests](https://github.com/vitorpamplona/amethyst/pulls) here are also very welcome. - ## How to Deploy 1. Generate a new signing key From f2ed2d94c4c969201e838d0054cc36e852dbbbe8 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 30 Jan 2024 12:01:23 -0500 Subject: [PATCH 016/176] Update README.md Fixing Markdown --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8fc448bb4..4ff182488 100644 --- a/README.md +++ b/README.md @@ -169,10 +169,10 @@ For the Play build: ## Contributing -You can send patches in Nostr using [GitStr](https://github.com/fiatjaf/gitstr) to [this address](nostr:naddr1qqyxzmt9w358jum5qyg8v6t5daezumn0wd68yvfwvdhk6qg7waehxw309ahx7um5wgkhqatz9emk2mrvdaexgetj9ehx2ap0qy2hwumn8ghj7un9d3shjtnwdaehgu3wvfnj7q3qgcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqxpqqqpmej720gac) - [Issues](https://github.com/vitorpamplona/amethyst/issues) and [pull requests](https://github.com/vitorpamplona/amethyst/pulls) here are also very welcome. +You can also send patches in Nostr using [GitStr](https://github.com/fiatjaf/gitstr) to `naddr1qqyxzmt9w358jum5qyg8v6t5daezumn0wd68yvfwvdhk6qg7waehxw309ahx7um5wgkhqatz9emk2mrvdaexgetj9ehx2ap0qy2hwumn8ghj7un9d3shjtnwdaehgu3wvfnj7q3qgcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqxpqqqpmej720gac` + ## How to Deploy 1. Generate a new signing key From a72f5ae01fcbd6335042b783f55e13aa393df129 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 30 Jan 2024 13:09:43 -0500 Subject: [PATCH 017/176] Removing unnecessary logs --- .../com/vitorpamplona/amethyst/service/NostrDataSource.kt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt index c672d6cca..74eb9957c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt @@ -248,19 +248,11 @@ abstract class NostrDataSource(val debugName: String) { if (updatedSubscription.toJson() != currentFilters[updatedSubscription.id]) { Client.close(updatedSubscription.id) if (active) { - Log.d( - this@NostrDataSource.javaClass.simpleName, - "Update Filter 1 ${updatedSubscription.id} ${Client.isSubscribed(clientListener)}", - ) Client.sendFilter(updatedSubscription.id, updatedSubscriptionNewFilters) } } else { // hasn't changed, does nothing. if (active) { - Log.d( - this@NostrDataSource.javaClass.simpleName, - "Update Filter 2 ${updatedSubscription.id} ${Client.isSubscribed(clientListener)}", - ) Client.sendFilterOnlyIfDisconnected( updatedSubscription.id, updatedSubscriptionNewFilters, From 5135026e6ac9eb906ff2fe2148e7cca5e4f205df Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Tue, 30 Jan 2024 18:12:06 +0000 Subject: [PATCH 018/176] New Crowdin translations by GitHub Action --- app/src/main/res/values-cs/strings.xml | 1 + app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values-pt-rBR/strings.xml | 1 + app/src/main/res/values-sv-rSE/strings.xml | 1 + 4 files changed, 4 insertions(+) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 2289f235a..fb3ace03e 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -532,6 +532,7 @@ Nepodařilo se sestavit LNUrl z Lightning adresy \"%1$s\". Zkontrolujte nastavení uživatele Služba blesku příjemce v %1$s není k dispozici. Byla vypočtena z adresy blesku \"%2$s\". Chyba: %3$s. Zkontrolujte, zda je server aktivní a zda je adresa blesku správná Nepodařilo se vyřešit %1$s. Zkontrolujte, zda jste připojeni, zda je server aktivní a zda je adresa %2$s správná + Nepodařilo se vyřešit %1$s. Zkontrolujte, zda jste připojeni, zda je server aktivní a zda je adresa %2$s správná.\n\nVýjimka byla: %3$s Nepodařilo se načíst fakturu z %1$s Chyba při analýze JSON z Lightning adresy. Zkontrolujte uživatelovo bleskové nastavení URL adresa zpětného volání nebyla nalezena v konfiguraci serveru pro bleskovou adresu uživatele diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4631cd18b..6a3c3bc45 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -536,6 +536,7 @@ anz der Bedingungen ist erforderlich Konnte keine LNUrl aus der Lightning-Adresse \"%1$s\" zusammenstellen. Überprüfen Sie die Konfiguration des Benutzers Der Lightning-Service des Empfängers unter %1$s ist nicht verfügbar. Er wurde aus der Lightning-Adresse \"%2$s\" berechnet. Fehler: %3$s. Überprüfen Sie, ob der Server online ist und ob die Lightning-Adresse korrekt ist Konnte %1$s nicht auflösen. Überprüfen Sie, ob Sie verbunden sind, ob der Server online ist und ob die Lightning-Adresse %2$s korrekt ist + Konnte %1$s nicht auflösen. Überprüfen Sie, ob Sie verbunden sind, ob der Server online ist und ob die Lightning-Adresse %2$s korrekt ist.\n\nAusnahme war: %3$s Konnte Rechnung nicht von %1$s abholen Fehler beim analysieren von JSON aus der Lightning-Adresse. Überprüfen Sie die Lightning-Konfiguration des Benutzers Callback-URL nicht in der Serverkonfiguration der Lightning-Adresse des Benutzers gefunden diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index ca944e22b..d9dd7d7f6 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -532,6 +532,7 @@ Não foi possível montar o LNUrl a partir do Endereço Lightning \"%1$s\". Verifique a configuração do usuário O serviço Lightning do destinatário em %1$s não está disponível. Foi calculado a partir do endereço Lightning \"%2$s\". Erro: %3$s. Verifique se o servidor está funcionando e se o endereço Lightning está correto Não foi possível resolver %1$s. Verifique se você está conectado, se o servidor está funcionando e se o endereço Lightning %2$s está correto + Não foi possível resolver %1$s. Verifique se você está conectado, se o servidor está funcionando e se o endereço Lightning %2$s está correto.\n\nExceção foi: %3$s Não foi possível obter a fatura de %1$s Erro ao analisar JSON do Endereço Lightning. Verifique a configuração de Lightning do usuário URL de retorno não encontrada na configuração do servidor de endereço Lightning do usuário diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 246a9ab38..e90c19e84 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -530,6 +530,7 @@ Kunde inte montera LNUrl från Lightning Address \"%1$s\". Kontrollera användarens konfiguration Mottagarens Lightning-tjänst på %1$s är inte tillgänglig. Den beräknades från Lightning Address \"%2$s\". Fel: %3$s. Kontrollera om servern är uppe och om Lightning Addressen är korrekt Kunde inte lösa %1$s. Kontrollera om du är ansluten, om servern är uppe och om Lightning Addressen %2$s är korrekt + Kunde inte lösa %1$s. Kontrollera om du är ansluten, om servern är uppe och om Lightning Addressen %2$s är korrekt.\n\nUndantag var: %3$s Kunde inte hämta fakturan från %1$s Fel vid tolkning av JSON från Lightning Address. Kontrollera användarens Lightning-konfiguration Återuppringnings-URL hittades inte i användarens konfiguration för Lightning Address-servern From 21f042fa9576f62ca0159471e7495b221463fd5a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 30 Jan 2024 13:38:41 -0500 Subject: [PATCH 019/176] Fixes secondary buttons theme in the login and signup screens. --- .../vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt | 3 ++- .../vitorpamplona/amethyst/ui/screen/loggedOff/SignUpScreen.kt | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt index 079aa53cd..eeef52c43 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt @@ -49,6 +49,7 @@ import androidx.compose.material3.Checkbox import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -490,7 +491,7 @@ fun LoginPage( Spacer(modifier = Modifier.height(Size20dp)) Box(modifier = Modifier.padding(Size40dp, 0.dp, Size40dp, 0.dp)) { - Button( + OutlinedButton( onClick = onWantsToLogin, shape = RoundedCornerShape(Size35dp), modifier = Modifier.height(50.dp), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/SignUpScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/SignUpScreen.kt index e34a0c7bf..d92932043 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/SignUpScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/SignUpScreen.kt @@ -40,6 +40,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.Checkbox import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -292,7 +293,7 @@ fun SignUpPage( Spacer(modifier = Modifier.height(Size20dp)) Box(modifier = Modifier.padding(Size40dp, 0.dp, Size40dp, 0.dp)) { - Button( + OutlinedButton( onClick = onWantsToLogin, shape = RoundedCornerShape(Size35dp), modifier = Modifier.height(50.dp), From 6b60b1434f0f5a98dbc185997c33c2d7330c7a45 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 30 Jan 2024 13:52:02 -0500 Subject: [PATCH 020/176] Name refactoring --- .../amethyst/ui/screen/loggedOff/LoginOrSignupScreen.kt | 4 ++-- .../amethyst/ui/screen/loggedOff/LoginScreen.kt | 8 ++++---- .../amethyst/ui/screen/loggedOff/SignUpScreen.kt | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginOrSignupScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginOrSignupScreen.kt index 358d42c1d..874eb9337 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginOrSignupScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginOrSignupScreen.kt @@ -39,11 +39,11 @@ fun LoginOrSignupScreen( Crossfade(wantsNewUser, label = "LoginOrSignupScreen") { if (it) { - SignUpPage(accountViewModel = accountViewModel) { + SignUpPage(accountStateViewModel = accountViewModel) { wantsNewUser = false } } else { - LoginPage(accountViewModel = accountViewModel, isFirstLogin = isFirstLogin) { + LoginPage(accountStateViewModel = accountViewModel, isFirstLogin = isFirstLogin) { wantsNewUser = true } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt index eeef52c43..5fb180f8f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt @@ -123,7 +123,7 @@ fun LoginPage() { @OptIn(ExperimentalComposeUiApi::class) @Composable fun LoginPage( - accountViewModel: AccountStateViewModel, + accountStateViewModel: AccountStateViewModel, isFirstLogin: Boolean, onWantsToLogin: () -> Unit, ) { @@ -209,7 +209,7 @@ fun LoginPage( } if (acceptedTerms.value && key.value.text.isNotBlank()) { - accountViewModel.login( + accountStateViewModel.login( key.value.text, useProxy.value, proxyPort.value.toInt(), @@ -331,7 +331,7 @@ fun LoginPage( } if (acceptedTerms.value && key.value.text.isNotBlank()) { - accountViewModel.login(key.value.text, useProxy.value, proxyPort.value.toInt()) { + accountStateViewModel.login(key.value.text, useProxy.value, proxyPort.value.toInt()) { errorMessage = context.getString(R.string.invalid_key) } } @@ -444,7 +444,7 @@ fun LoginPage( } if (acceptedTerms.value && key.value.text.isNotBlank()) { - accountViewModel.login(key.value.text, useProxy.value, proxyPort.value.toInt()) { + accountStateViewModel.login(key.value.text, useProxy.value, proxyPort.value.toInt()) { errorMessage = context.getString(R.string.invalid_key) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/SignUpScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/SignUpScreen.kt index d92932043..e4b03e6d8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/SignUpScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/SignUpScreen.kt @@ -94,7 +94,7 @@ fun SignUpPage() { @Composable fun SignUpPage( - accountViewModel: AccountStateViewModel, + accountStateViewModel: AccountStateViewModel, onWantsToLogin: () -> Unit, ) { val displayName = remember { mutableStateOf(TextFieldValue("")) } @@ -163,7 +163,7 @@ fun SignUpPage( } if (acceptedTerms.value && displayName.value.text.isNotBlank()) { - accountViewModel.login(displayName.value.text, useProxy.value, proxyPort.value.toInt()) { + accountStateViewModel.login(displayName.value.text, useProxy.value, proxyPort.value.toInt()) { errorMessage = context.getString(R.string.invalid_key) } } @@ -273,7 +273,7 @@ fun SignUpPage( } if (acceptedTerms.value && displayName.value.text.isNotBlank()) { - accountViewModel.newKey(useProxy.value, proxyPort.value.toInt(), displayName.value.text) + accountStateViewModel.newKey(useProxy.value, proxyPort.value.toInt(), displayName.value.text) } }, shape = RoundedCornerShape(Size35dp), From eae07e2fe0bb886ec7ef26823c2d9318b2c5b6ce Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 30 Jan 2024 17:27:48 -0500 Subject: [PATCH 021/176] Making sure Cancellation of coroutines stops long processes. --- .../amethyst/service/notifications/PushMessageReceiver.kt | 2 ++ .../service/notifications/PushNotificationUtils.kt | 2 ++ .../java/com/vitorpamplona/amethyst/LocalPreferences.kt | 7 +++++++ .../java/com/vitorpamplona/amethyst/ServiceManager.kt | 2 ++ .../java/com/vitorpamplona/amethyst/model/LocalCache.kt | 5 +++++ .../main/java/com/vitorpamplona/amethyst/model/Note.kt | 4 ++++ .../amethyst/service/CachedRichTextParser.kt | 3 +++ .../com/vitorpamplona/amethyst/service/CashuProcessor.kt | 4 ++++ .../java/com/vitorpamplona/amethyst/service/FileHeader.kt | 3 +++ .../com/vitorpamplona/amethyst/service/LocationUtil.kt | 2 ++ .../amethyst/service/Nip05NostrAddressVerifier.kt | 7 +++++-- .../amethyst/service/Nip11RelayInfoRetriever.kt | 3 +++ .../com/vitorpamplona/amethyst/service/Nip44UrlParser.kt | 2 ++ .../amethyst/service/Nip47WalletConnectParser.kt | 2 ++ .../vitorpamplona/amethyst/service/Nip96MediaServers.kt | 2 ++ .../amethyst/service/NostrSearchEventOrUserDataSource.kt | 2 ++ .../com/vitorpamplona/amethyst/service/OnlineCheck.kt | 2 ++ .../amethyst/service/lnurl/LightningAddressResolver.kt | 4 ++++ .../amethyst/service/notifications/RegisterAccounts.kt | 2 ++ .../amethyst/service/playback/PlaybackClientController.kt | 3 +++ .../amethyst/service/previews/BahaUrlPreview.kt | 2 ++ .../com/vitorpamplona/amethyst/service/relays/Relay.kt | 8 ++++++-- .../java/com/vitorpamplona/amethyst/ui/MainActivity.kt | 2 ++ .../vitorpamplona/amethyst/ui/actions/ImageDownloader.kt | 2 ++ .../com/vitorpamplona/amethyst/ui/actions/ImageSaver.kt | 4 ++++ .../vitorpamplona/amethyst/ui/actions/NewMediaModel.kt | 2 ++ .../com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt | 2 ++ .../vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt | 2 ++ .../vitorpamplona/amethyst/ui/actions/NewPollClosing.kt | 2 ++ .../amethyst/ui/actions/NewPollConsensusThreshold.kt | 2 ++ .../com/vitorpamplona/amethyst/ui/actions/NewPostView.kt | 2 ++ .../vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt | 4 ++++ .../amethyst/ui/actions/NewUserMetadataViewModel.kt | 2 ++ .../amethyst/ui/actions/UrlUserTagTransformation.kt | 2 ++ .../vitorpamplona/amethyst/ui/components/CashuRedeem.kt | 3 +++ .../amethyst/ui/components/ClickableEmail.kt | 2 ++ .../amethyst/ui/components/InvoicePreview.kt | 2 ++ .../amethyst/ui/components/MarkdownParser.kt | 3 +++ .../amethyst/ui/components/MediaCompressor.kt | 2 ++ .../com/vitorpamplona/amethyst/ui/components/VideoView.kt | 2 ++ .../amethyst/ui/components/ZoomableContentView.kt | 2 ++ .../amethyst/ui/navigation/AccountSwitchBottomSheet.kt | 2 ++ .../amethyst/ui/note/UpdateZapAmountDialog.kt | 3 +++ .../com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt | 3 +++ .../com/vitorpamplona/amethyst/ui/qrcode/QrCodeScanner.kt | 2 ++ .../com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt | 2 ++ .../amethyst/ui/screen/AccountStateViewModel.kt | 2 ++ .../amethyst/ui/screen/loggedIn/AccountViewModel.kt | 2 ++ .../amethyst/ui/screen/loggedIn/ConnectOrbotDialog.kt | 4 +++- .../amethyst/ui/screen/loggedOff/LoginScreen.kt | 2 ++ .../amethyst/service/lang/LanguageTranslatorService.kt | 7 +++++-- .../service/notifications/PushNotificationUtils.kt | 2 ++ 52 files changed, 141 insertions(+), 7 deletions(-) diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt index 951f6e8c6..a10656414 100644 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt @@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrC import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateZapChannel import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.GiftWrapEvent +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -60,6 +61,7 @@ class PushMessageReceiver : MessagingReceiver() { try { parseMessage(messageStr)?.let { receiveIfNew(it) } } catch (e: Exception) { + if (e is CancellationException) throw e Log.d(TAG, "Message could not be parsed: ${e.message}") } } diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt index 6a25767b0..7405779aa 100644 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.notifications import android.util.Log import com.vitorpamplona.amethyst.AccountInfo +import kotlinx.coroutines.CancellationException object PushNotificationUtils { var hasInit: Boolean = false @@ -36,6 +37,7 @@ object PushNotificationUtils { RegisterAccounts(accounts).go(pushHandler.getSavedEndpoint()) } } catch (e: Exception) { + if (e is CancellationException) throw e Log.d("Amethyst-OSSPushUtils", "Failed to get endpoint.") } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index af3201fb3..b553a33d2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -53,6 +53,7 @@ import com.vitorpamplona.quartz.events.LnZapEvent import com.vitorpamplona.quartz.signers.ExternalSignerLauncher import com.vitorpamplona.quartz.signers.NostrSignerExternal import com.vitorpamplona.quartz.signers.NostrSignerInternal +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.sync.Mutex @@ -368,6 +369,7 @@ object LocalPreferences { return try { getString(PrefKeys.SHARED_SETTINGS, "{}")?.let { Event.mapper.readValue(it) } } catch (e: Throwable) { + if (e is CancellationException) throw e Log.w( "LocalPreferences", "Unable to decode shared preferences: ${getString(PrefKeys.SHARED_SETTINGS, null)}", @@ -510,6 +512,7 @@ object LocalPreferences { } ?: Nip96MediaServers.DEFAULT[0] } catch (e: Exception) { + if (e is CancellationException) throw e Log.w("LocalPreferences", "Failed to decode saved File Server", e) e.printStackTrace() Nip96MediaServers.DEFAULT[0] @@ -521,6 +524,7 @@ object LocalPreferences { Event.mapper.readValue(it) } } catch (e: Throwable) { + if (e is CancellationException) throw e Log.w( "LocalPreferences", "Error Decoding Zap Payment Request Server ${getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)}", @@ -541,6 +545,7 @@ object LocalPreferences { } } } catch (e: Throwable) { + if (e is CancellationException) throw e Log.w( "LocalPreferences", "Error Decoding Contact List ${getString(PrefKeys.LATEST_CONTACT_LIST, null)}", @@ -556,6 +561,7 @@ object LocalPreferences { } ?: mapOf() } catch (e: Throwable) { + if (e is CancellationException) throw e Log.w( "LocalPreferences", "Error Decoding Language Preferences ${getString(PrefKeys.LANGUAGE_PREFS, null)}", @@ -588,6 +594,7 @@ object LocalPreferences { } ?: mapOf() } catch (e: Throwable) { + if (e is CancellationException) throw e Log.w( "LocalPreferences", "Error Decoding Last Read per route ${getString(PrefKeys.LAST_READ_PER_ROUTE, null)}", diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt index 7b332b5df..4cf478185 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt @@ -51,6 +51,7 @@ import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.quartz.encoders.bechToBytes import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull import com.vitorpamplona.quartz.encoders.toHexKey +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -126,6 +127,7 @@ class ServiceManager { try { it.npub.bechToBytes().toHexKey() } catch (e: Exception) { + if (e is CancellationException) throw e null } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 2122b7a99..7c530058f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -98,6 +98,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow @@ -1397,6 +1398,7 @@ object LocalCache { try { Nip19.uriToRoute(text)?.hex ?: Hex.decode(text).toHexKey() } catch (e: Exception) { + if (e is CancellationException) throw e null } @@ -1446,6 +1448,7 @@ object LocalCache { try { Nip19.uriToRoute(text)?.hex ?: Hex.decode(text).toHexKey() } catch (e: Exception) { + if (e is CancellationException) throw e null } @@ -1738,6 +1741,7 @@ object LocalCache { try { event.checkSignature() } catch (e: Exception) { + if (e is CancellationException) throw e Log.w("Event failed retest ${event.kind}", (e.message ?: "") + event.toJson()) } false @@ -1831,6 +1835,7 @@ object LocalCache { } } } catch (e: Exception) { + if (e is CancellationException) throw e e.printStackTrace() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 5d2a49f9a..4fae595d6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -63,6 +63,7 @@ import com.vitorpamplona.quartz.events.WrappedEvent import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.containsAny +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import java.math.BigDecimal @@ -644,6 +645,7 @@ open class Note(val idHex: String) { try { LnInvoiceUtil.getAmountInSats(invoice) } catch (e: java.lang.Exception) { + if (e is CancellationException) throw e null } @@ -694,6 +696,7 @@ open class Note(val idHex: String) { try { BigDecimal(it.event?.content()) } catch (e: Exception) { + if (e is CancellationException) throw e null // do nothing if it can't convert to bigdecimal } @@ -709,6 +712,7 @@ open class Note(val idHex: String) { try { BigDecimal(it.event?.content()) } catch (e: Exception) { + if (e is CancellationException) throw e null // do nothing if it can't convert to bigdecimal } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt index bc420f8d2..6f3046af9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt @@ -42,6 +42,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toImmutableSet +import kotlinx.coroutines.CancellationException import java.util.regex.Pattern @Immutable @@ -285,6 +286,7 @@ class RichTextParser() { } } } catch (e: Exception) { + if (e is CancellationException) throw e Log.w("Tag Parser", "Couldn't link tag $word", e) } @@ -299,6 +301,7 @@ class RichTextParser() { } } } catch (e: Exception) { + if (e is CancellationException) throw e Log.e("Hashtag Parser", "Couldn't link hashtag $word", e) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt index 3762505aa..ced0fa315 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt @@ -32,6 +32,7 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import java.util.Base64 +import kotlin.coroutines.cancellation.CancellationException @Immutable data class CashuToken( @@ -59,6 +60,7 @@ class CashuProcessor { return GenericLoadable.Loaded(CashuToken(cashuToken, mint, totalAmount, proofs)) } catch (e: Exception) { + if (e is CancellationException) throw e return GenericLoadable.Error("Could not parse this cashu token") } } @@ -154,6 +156,7 @@ class CashuProcessor { } } } catch (e: Exception) { + if (e is CancellationException) throw e onError( context.getString(R.string.cashu_successful_redemption), context.getString(R.string.cashu_failed_redemption_explainer_error_msg, e.message), @@ -211,6 +214,7 @@ class CashuProcessor { } } } catch (e: Exception) { + if (e is CancellationException) throw e onError( context.getString(R.string.cashu_successful_redemption), context.getString(R.string.cashu_failed_redemption_explainer_error_msg, e.message), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/FileHeader.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/FileHeader.kt index f763482d0..de73fc0f5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/FileHeader.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/FileHeader.kt @@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.ui.actions.ImageDownloader import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.toHexKey import io.trbl.blurhash.BlurHash +import kotlinx.coroutines.CancellationException import java.io.IOException import kotlin.math.roundToInt @@ -59,6 +60,7 @@ class FileHeader( onError(null) } } catch (e: Exception) { + if (e is CancellationException) throw e Log.e("ImageDownload", "Couldn't download image from server: ${e.message}") onError(e.message) } @@ -174,6 +176,7 @@ class FileHeader( onReady(FileHeader(mimeType, hash, size, dim, blurHash)) } catch (e: Exception) { + if (e is CancellationException) throw e Log.e("ImageDownload", "Couldn't convert image in to File Header: ${e.message}") onError(e.message) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/LocationUtil.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/LocationUtil.kt index f634b08a6..67ce8bc03 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/LocationUtil.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/LocationUtil.kt @@ -29,6 +29,7 @@ import android.location.LocationManager import android.os.HandlerThread import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.MutableStateFlow class LocationUtil(context: Context) { @@ -105,6 +106,7 @@ class ReverseGeoLocationUtil { .joinToString(", ") } } catch (e: Exception) { + if (e is CancellationException) throw e e.printStackTrace() return null } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt index 3498f9518..4ba8c34ac 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.vitorpamplona.amethyst.BuildConfig +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.Call @@ -96,7 +97,8 @@ class Nip05NostrAddressVerifier() { } }, ) - } catch (e: java.lang.Exception) { + } catch (e: Exception) { + if (e is CancellationException) throw e onError("Could not resolve '$url': ${e.message}") } } @@ -122,7 +124,8 @@ class Nip05NostrAddressVerifier() { val nip05url = try { mapper.readTree(it.lowercase()) - } catch (t: Throwable) { + } catch (e: Throwable) { + if (e is CancellationException) throw e onError("Error Parsing JSON from Lightning Address. Check the user's lightning setup") null } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt index 07fb77fe5..db98738df 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.service import android.util.Log import android.util.LruCache import com.vitorpamplona.amethyst.model.RelayInformation +import kotlinx.coroutines.CancellationException import okhttp3.Call import okhttp3.Callback import okhttp3.Request @@ -102,6 +103,7 @@ class Nip11Retriever { onError(dirtyUrl, ErrorCode.FAIL_WITH_HTTP_STATUS, it.code.toString()) } } catch (e: Exception) { + if (e is CancellationException) throw e Log.e( "RelayInfoFail", "Resulting Message from Relay $dirtyUrl in not parseable: $body", @@ -122,6 +124,7 @@ class Nip11Retriever { }, ) } catch (e: Exception) { + if (e is CancellationException) throw e Log.e("RelayInfoFail", "Invalid URL $dirtyUrl", e) onError(dirtyUrl, ErrorCode.FAIL_TO_ASSEMBLE_URL, e.message) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip44UrlParser.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip44UrlParser.kt index a3297527f..d203c9d8d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip44UrlParser.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip44UrlParser.kt @@ -22,12 +22,14 @@ package com.vitorpamplona.amethyst.service import java.net.URI import java.net.URLDecoder +import kotlin.coroutines.cancellation.CancellationException class Nip44UrlParser { fun parse(url: String): Map { return try { fragments(URI(url)) } catch (e: Exception) { + if (e is CancellationException) throw e emptyMap() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip47WalletConnectParser.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip47WalletConnectParser.kt index 075b537cc..d7186895d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip47WalletConnectParser.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip47WalletConnectParser.kt @@ -24,6 +24,7 @@ import android.net.Uri import com.vitorpamplona.amethyst.model.Nip47URI import com.vitorpamplona.quartz.encoders.decodePublicKey import com.vitorpamplona.quartz.encoders.toHexKey +import kotlinx.coroutines.CancellationException // Rename to the corect nip number when ready. object Nip47WalletConnectParser { @@ -42,6 +43,7 @@ object Nip47WalletConnectParser { try { decodePublicKey(pubkey).toHexKey() } catch (e: Exception) { + if (e is CancellationException) throw e throw IllegalArgumentException("Hostname is not a valid Nostr Pubkey") } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip96MediaServers.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip96MediaServers.kt index d024bb748..f45d59b9f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip96MediaServers.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip96MediaServers.kt @@ -24,6 +24,7 @@ import android.util.Log import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import kotlinx.coroutines.CancellationException import okhttp3.Request object Nip96MediaServers { @@ -100,6 +101,7 @@ class Nip96Retriever { ) } } catch (e: Exception) { + if (e is CancellationException) throw e Log.e("RelayInfoFail", "Resulting Message from $baseUrl in not parseable: $body", e) throw e } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt index a4a3a39ec..64804f126 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt @@ -46,6 +46,7 @@ import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.events.PinListEvent import com.vitorpamplona.quartz.events.PollNoteEvent import com.vitorpamplona.quartz.events.TextNoteEvent +import kotlin.coroutines.cancellation.CancellationException object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") { private var searchString: String? = null @@ -67,6 +68,7 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") { Nip19.uriToRoute(mySearchString)?.hex ?: isAStraightHex } catch (e: Exception) { + if (e is CancellationException) throw e null } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt index fae525ad6..cc6e4ed71 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt @@ -25,6 +25,7 @@ import android.util.LruCache import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.BuildConfig import okhttp3.Request +import kotlin.coroutines.cancellation.CancellationException @Immutable data class OnlineCheckResult(val timeInMs: Long, val online: Boolean) @@ -66,6 +67,7 @@ object OnlineChecker { checkOnlineCache.put(url, OnlineCheckResult(System.currentTimeMillis(), result)) result } catch (e: Exception) { + if (e is CancellationException) throw e checkOnlineCache.put(url, OnlineCheckResult(System.currentTimeMillis(), false)) Log.e("LiveActivities", "Failed to check streaming url $url", e) false diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt index 0b5f9a5c9..fb471d86d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt @@ -33,6 +33,7 @@ import okhttp3.Request import java.math.BigDecimal import java.math.RoundingMode import java.net.URLEncoder +import kotlin.coroutines.cancellation.CancellationException class LightningAddressResolver() { val client = HttpClientManager.getHttpClient() @@ -96,6 +97,7 @@ class LightningAddressResolver() { } } } catch (e: Exception) { + if (e is CancellationException) throw e e.printStackTrace() onError( context.getString(R.string.error_unable_to_fetch_invoice), @@ -184,6 +186,7 @@ class LightningAddressResolver() { try { mapper.readTree(lnAddressJson) } catch (t: Throwable) { + if (t is CancellationException) throw t onError( context.getString(R.string.error_unable_to_fetch_invoice), context.getString( @@ -219,6 +222,7 @@ class LightningAddressResolver() { try { mapper.readTree(it) } catch (t: Throwable) { + if (t is CancellationException) throw t onError( context.getString(R.string.error_unable_to_fetch_invoice), context.getString( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt index 903b94f95..8d0bac6ee 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt @@ -27,6 +27,7 @@ import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.HttpClientManager import com.vitorpamplona.quartz.events.RelayAuthEvent +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.MediaType.Companion.toMediaType @@ -110,6 +111,7 @@ class RegisterAccounts( val isSucess = client.newCall(request).execute().use { it.isSuccessful } } catch (e: java.lang.Exception) { + if (e is CancellationException) throw e val tag = if (BuildConfig.FLAVOR == "play") { "FirebaseMsgService" diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/playback/PlaybackClientController.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/playback/PlaybackClientController.kt index 94499d3c9..13616eeac 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/playback/PlaybackClientController.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/playback/PlaybackClientController.kt @@ -28,6 +28,7 @@ import android.util.LruCache import androidx.media3.session.MediaController import androidx.media3.session.SessionToken import com.google.common.util.concurrent.MoreExecutors +import kotlinx.coroutines.CancellationException object PlaybackClientController { val cache = LruCache(1) @@ -62,12 +63,14 @@ object PlaybackClientController { try { onReady(controllerFuture.get()) } catch (e: Exception) { + if (e is CancellationException) throw e Log.e("Playback Client", "Failed to load Playback Client for $videoUri", e) } }, MoreExecutors.directExecutor(), ) } catch (e: Exception) { + if (e is CancellationException) throw e Log.e("Playback Client", "Failed to load Playback Client for $videoUri", e) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/previews/BahaUrlPreview.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/previews/BahaUrlPreview.kt index aedb40df6..e794100cb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/previews/BahaUrlPreview.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/previews/BahaUrlPreview.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.service.previews +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -29,6 +30,7 @@ class BahaUrlPreview(val url: String, var callback: IUrlPreviewCallback?) { try { fetch(timeOut) } catch (t: Throwable) { + if (t is CancellationException) throw t callback?.onFailed(t) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt index e8a2b632e..fed6828b6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt @@ -31,6 +31,7 @@ import com.vitorpamplona.quartz.events.EventInterface import com.vitorpamplona.quartz.events.RelayAuthEvent import com.vitorpamplona.quartz.events.bytesUsedInMemory import com.vitorpamplona.quartz.utils.TimeUtils +import kotlinx.coroutines.CancellationException import okhttp3.Request import okhttp3.Response import okhttp3.WebSocket @@ -128,6 +129,8 @@ class Relay( socket = httpClient.newWebSocket(request, RelayListener(onConnected)) } catch (e: Exception) { + if (e is CancellationException) throw e + errorCounter++ markConnectionAsClosed() Log.e("Relay", "Relay Invalid $url") @@ -167,8 +170,9 @@ class Relay( try { processNewRelayMessage(text) - } catch (t: Throwable) { - t.printStackTrace() + } catch (e: Throwable) { + if (e is CancellationException) throw e + e.printStackTrace() text.chunked(2000) { chunked -> listeners.forEach { it.onError(this@Relay, "", Error("Problem with $chunked")) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 0303adc68..85fef8da5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -64,6 +64,7 @@ import com.vitorpamplona.quartz.events.ChannelMetadataEvent import com.vitorpamplona.quartz.events.CommunityDefinitionEvent import com.vitorpamplona.quartz.events.LiveActivitiesEvent import com.vitorpamplona.quartz.events.PrivateDmEvent +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -341,6 +342,7 @@ fun uriToRoute(uri: String?): String? { Route.Home.base + "?nip47=" + encodedUri } } catch (e: Exception) { + if (e is CancellationException) throw e null } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageDownloader.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageDownloader.kt index e37758f29..674108bf4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageDownloader.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageDownloader.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.actions +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay import java.net.HttpURLConnection import java.net.URL @@ -57,6 +58,7 @@ class ImageDownloader { null } } catch (e: Exception) { + if (e is CancellationException) throw e tentatives++ delay(1000) null diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageSaver.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageSaver.kt index ad83dc860..11a989a60 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageSaver.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageSaver.kt @@ -31,6 +31,7 @@ import android.webkit.MimeTypeMap import androidx.annotation.RequiresApi import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.service.HttpClientManager +import kotlinx.coroutines.CancellationException import okhttp3.Call import okhttp3.Callback import okhttp3.Request @@ -102,6 +103,7 @@ object ImageSaver { } onSuccess() } catch (e: Exception) { + if (e is CancellationException) throw e e.printStackTrace() onError(e) } @@ -138,6 +140,7 @@ object ImageSaver { } onSuccess() } catch (e: Exception) { + if (e is CancellationException) throw e e.printStackTrace() onError(e) } @@ -176,6 +179,7 @@ object ImageSaver { outputStream.use { contentSource.readAll(it.sink()) } } catch (e: Exception) { + if (e is CancellationException) throw e contentResolver.delete(uri, null, null) throw e } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt index 985915881..703938397 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt @@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.service.Nip96MediaServers import com.vitorpamplona.amethyst.service.Nip96Uploader import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.ui.components.MediaCompressor +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -148,6 +149,7 @@ open class NewMediaModel : ViewModel() { context, ) } catch (e: Exception) { + if (e is CancellationException) throw e isUploadingImage = false uploadingPercentage.value = 0.00f uploadingDescription.value = null diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt index 52f2d77b8..847131e4f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt @@ -72,6 +72,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.theme.placeholderText import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -231,6 +232,7 @@ fun ImageVideoPost( try { bitmap = resolver.loadThumbnail(it, Size(1200, 1000), null) } catch (e: Exception) { + if (e is CancellationException) throw e Log.w("NewPostView", "Couldn't create thumbnail, but the video can be uploaded", e) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt index 8299421e9..86f531645 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt @@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.Nip19 import com.vitorpamplona.quartz.encoders.bechToBytes import com.vitorpamplona.quartz.encoders.toNpub +import kotlinx.coroutines.CancellationException class NewMessageTagger( var message: String, @@ -196,6 +197,7 @@ class NewMessageTagger( ) // no way to know when they address ends and dirt begins } } catch (e: Exception) { + if (e is CancellationException) throw e e.printStackTrace() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollClosing.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollClosing.kt index 39f16194c..7f6145daf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollClosing.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollClosing.kt @@ -43,6 +43,7 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel import com.vitorpamplona.amethyst.ui.theme.placeholderText +import kotlinx.coroutines.CancellationException @Composable fun NewPollClosing(pollViewModel: NewPostViewModel) { @@ -58,6 +59,7 @@ fun NewPollClosing(pollViewModel: NewPostViewModel) { pollViewModel.closedAt = int } } catch (e: Exception) { + if (e is CancellationException) throw e pollViewModel.isValidClosedAt.value = false } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollConsensusThreshold.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollConsensusThreshold.kt index 271c0f346..51c1f65c8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollConsensusThreshold.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollConsensusThreshold.kt @@ -43,6 +43,7 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel import com.vitorpamplona.amethyst.ui.theme.placeholderText +import kotlinx.coroutines.CancellationException @Composable fun NewPollConsensusThreshold(pollViewModel: NewPostViewModel) { @@ -58,6 +59,7 @@ fun NewPollConsensusThreshold(pollViewModel: NewPostViewModel) { pollViewModel.consensusThreshold = int } } catch (e: Exception) { + if (e is CancellationException) throw e pollViewModel.isValidConsensusThreshold.value = false } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index e756677d1..2429a2990 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -179,6 +179,7 @@ import com.vitorpamplona.quartz.events.ClassifiedsEvent import com.vitorpamplona.quartz.events.toImmutableListOfLists import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -1674,6 +1675,7 @@ fun ImageVideoDescription( try { bitmap = resolver.loadThumbnail(uri, Size(1200, 1000), null) } catch (e: Exception) { + if (e is CancellationException) throw e onError("Unable to load thumbnail") Log.w("NewPostView", "Couldn't create thumbnail, but the video can be uploaded", e) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index 20f7c59ff..1bc8a5fbd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -63,6 +63,7 @@ import com.vitorpamplona.quartz.events.PrivateDmEvent import com.vitorpamplona.quartz.events.TextNoteEvent import com.vitorpamplona.quartz.events.ZapSplitSetup import com.vitorpamplona.quartz.events.findURLs +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.BufferOverflow @@ -476,6 +477,7 @@ open class NewPostViewModel() : ViewModel() { ) } } catch (e: Exception) { + if (e is CancellationException) throw e Log.e( "ImageUploader", "Failed to upload ${e.message}", @@ -942,6 +944,7 @@ open class NewPostViewModel() : ViewModel() { valueMinimum = int } } catch (e: Exception) { + if (e is CancellationException) throw e } } else { valueMinimum = null @@ -960,6 +963,7 @@ open class NewPostViewModel() : ViewModel() { valueMaximum = int } } catch (e: Exception) { + if (e is CancellationException) throw e } } else { valueMaximum = null diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt index 1824dbbc5..86695c9b6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt @@ -37,6 +37,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch +import kotlin.coroutines.cancellation.CancellationException class NewUserMetadataViewModel : ViewModel() { private lateinit var account: Account @@ -197,6 +198,7 @@ class NewUserMetadataViewModel : ViewModel() { } } } catch (e: Exception) { + if (e is CancellationException) throw e onUploading(false) viewModelScope.launch { imageUploadingError.emit("Failed to upload the image / video") diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagTransformation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagTransformation.kt index 32218b39a..f239f4af0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagTransformation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagTransformation.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.text.style.TextDecoration import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.encoders.decodePublicKey import com.vitorpamplona.quartz.encoders.toHexKey +import kotlin.coroutines.cancellation.CancellationException import kotlin.math.roundToInt data class RangesChanges(val original: TextRange, val modified: TextRange) @@ -113,6 +114,7 @@ fun buildAnnotatedStringWithUrlHighlighting( word } } catch (e: Exception) { + if (e is CancellationException) throw e // if it can't parse the key, don't try to change. builderBefore.append("$word ") builderAfter.append("$word ") diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt index d71e64e82..b318a4a3f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt @@ -83,6 +83,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.SmallishBorder import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.subtleBorder +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -238,6 +239,7 @@ fun CashuPreview( startActivity(context, intent, null) } catch (e: Exception) { + if (e is CancellationException) throw e toast("Cashu", context.getString(R.string.cashu_no_wallet_found)) } }, @@ -354,6 +356,7 @@ fun CashuPreviewNew( startActivity(context, intent, null) } catch (e: Exception) { + if (e is CancellationException) throw e toast("Cashu", context.getString(R.string.cashu_no_wallet_found)) } }, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableEmail.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableEmail.kt index 7faddad2f..841e35864 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableEmail.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableEmail.kt @@ -30,6 +30,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.AnnotatedString +import kotlinx.coroutines.CancellationException @Composable fun ClickableEmail(email: String) { @@ -58,6 +59,7 @@ fun Context.sendMail( } catch (e: ActivityNotFoundException) { // TODO: Handle case where no email app is available } catch (t: Throwable) { + if (t is CancellationException) throw t // TODO: Handle potential other type of exceptions } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt index 3f1213cee..5ed03955b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt @@ -58,6 +58,7 @@ import com.vitorpamplona.amethyst.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.subtleBorder import com.vitorpamplona.quartz.encoders.LnInvoiceUtil +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.text.NumberFormat @@ -79,6 +80,7 @@ fun LoadValueFromInvoice( try { NumberFormat.getInstance().format(LnInvoiceUtil.getAmountInSats(myInvoice)) } catch (e: Exception) { + if (e is CancellationException) throw e e.printStackTrace() null } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/MarkdownParser.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/MarkdownParser.kt index eabab7c90..e3518a55b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/MarkdownParser.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/MarkdownParser.kt @@ -27,6 +27,7 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.startsWithNIP19Scheme import com.vitorpamplona.quartz.encoders.Nip19 import com.vitorpamplona.quartz.events.ImmutableListOfLists +import kotlinx.coroutines.CancellationException class MarkdownParser { private fun getDisplayNameAndNIP19FromTag( @@ -39,6 +40,7 @@ class MarkdownParser { matcher.find() Pair(matcher.group(1)?.toInt(), matcher.group(2) ?: "") } catch (e: Exception) { + if (e is CancellationException) throw e Log.w("Tag Parser", "Couldn't link tag $tag", e) Pair(null, null) } @@ -161,6 +163,7 @@ class MarkdownParser { hashtagMatcher.find() Pair(hashtagMatcher.group(1), hashtagMatcher.group(2)) } catch (e: Exception) { + if (e is CancellationException) throw e Log.e("Hashtag Parser", "Couldn't link hashtag $word", e) Pair(null, null) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/MediaCompressor.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/MediaCompressor.kt index 0e9d5377c..176e5c14e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/MediaCompressor.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/MediaCompressor.kt @@ -33,6 +33,7 @@ import com.abedelazizshe.lightcompressorlibrary.config.Configuration import com.vitorpamplona.amethyst.service.checkNotInMainThread import id.zelory.compressor.Compressor import id.zelory.compressor.constraint.default +import kotlinx.coroutines.CancellationException import java.io.File import java.io.FileOutputStream import java.util.UUID @@ -115,6 +116,7 @@ class MediaCompressor { } onReady(compressedImageFile.toUri(), contentType, compressedImageFile.length()) } catch (e: Exception) { + if (e is CancellationException) throw e e.printStackTrace() onReady(uri, contentType, null) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt index ddd58eede..530c95c08 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt @@ -106,6 +106,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size75dp import com.vitorpamplona.amethyst.ui.theme.VolumeBottomIconSize import com.vitorpamplona.amethyst.ui.theme.imageModifier import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay @@ -356,6 +357,7 @@ fun GetMediaItem( null } } catch (e: Exception) { + if (e is CancellationException) throw e null }, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index 9b89696d9..504136bbb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -131,6 +131,7 @@ import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.toHexKey import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -650,6 +651,7 @@ fun aspectRatio(dim: String?): Float? { width / height } } catch (e: Exception) { + if (e is CancellationException) throw e null } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt index 61b3bd706..82d50cf33 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt @@ -80,6 +80,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.quartz.encoders.decodePublicKey import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.events.toImmutableListOfLists +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -166,6 +167,7 @@ fun DisplayAccount( decodePublicKey(acc.npub).toHexKey(), ) } catch (e: Exception) { + if (e is CancellationException) throw e null } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt index bcd0ce310..2d5a0fd78 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt @@ -103,6 +103,7 @@ import com.vitorpamplona.quartz.encoders.decodePublicKey import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.events.LnZapEvent import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CancellationException class UpdateZapAmountViewModel(val account: Account) : ViewModel() { var nextAmount by mutableStateOf(TextFieldValue("")) @@ -149,6 +150,7 @@ class UpdateZapAmountViewModel(val account: Account) : ViewModel() { try { decodePublicKey(walletConnectPubkey.text.trim()).toHexKey() } catch (e: Exception) { + if (e is CancellationException) throw e null } @@ -168,6 +170,7 @@ class UpdateZapAmountViewModel(val account: Account) : ViewModel() { try { unverifiedPrivKey?.let { decodePublicKey(it).toHexKey() } } catch (e: Exception) { + if (e is CancellationException) throw e null } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt index 15b5cec1c..94ab5037b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt @@ -88,6 +88,7 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.events.LnZapEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CancellationException class ZapOptionstViewModel : ViewModel() { private var account: Account? = null @@ -107,6 +108,7 @@ class ZapOptionstViewModel : ViewModel() { return try { customAmount.text.trim().toLongOrNull() } catch (e: Exception) { + if (e is CancellationException) throw e null } } @@ -446,6 +448,7 @@ fun payViaIntent( ContextCompat.startActivity(context, intent, null) } catch (e: Exception) { + if (e is CancellationException) throw e if (e.message != null) { onError(context.getString(R.string.no_wallet_found_with_error, e.message!!)) } else { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeScanner.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeScanner.kt index fec70d41c..abca33706 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeScanner.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeScanner.kt @@ -30,6 +30,7 @@ import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanOptions import com.vitorpamplona.amethyst.R import com.vitorpamplona.quartz.encoders.Nip19 +import kotlinx.coroutines.CancellationException @Composable fun NIP19QrCodeScanner(onScan: (String?) -> Unit) { @@ -51,6 +52,7 @@ fun NIP19QrCodeScanner(onScan: (String?) -> Unit) { onScan(null) } } catch (e: Throwable) { + if (e is CancellationException) throw e Log.e("NIP19 Scanner", "Error parsing $it", e) // QR can be anything, do not throw errors. onScan(null) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt index 5555a2932..29134685d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt @@ -52,6 +52,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.MainScreen import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginOrSignupScreen import com.vitorpamplona.quartz.signers.NostrSignerExternal +import kotlinx.coroutines.CancellationException @Composable fun AccountScreen( @@ -143,6 +144,7 @@ fun LoggedInPage( activity.prepareToLaunchSigner() launcher.launch(it) } catch (e: Exception) { + if (e is CancellationException) throw e Log.e("Signer", "Error opening Signer app", e) accountViewModel.toast( R.string.error_opening_external_signer, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index 3d2a3caca..484943d4a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -40,6 +40,7 @@ import com.vitorpamplona.quartz.encoders.toNpub import com.vitorpamplona.quartz.signers.ExternalSignerLauncher import com.vitorpamplona.quartz.signers.NostrSignerExternal import com.vitorpamplona.quartz.signers.NostrSignerInternal +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -204,6 +205,7 @@ class AccountStateViewModel() : ViewModel() { try { loginAndStartUI(key, useProxy, proxyPort, loginWithExternalSigner, packageName) } catch (e: Exception) { + if (e is CancellationException) throw e Log.e("Login", "Could not sign in", e) onError() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 20561019b..36671804d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -87,6 +87,7 @@ import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.channels.BufferOverflow @@ -1102,6 +1103,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View val myCover = context.imageLoader.execute(request).drawable onReady(myCover) } catch (e: Exception) { + if (e is CancellationException) throw e Log.e("VideoView", "Fail to load cover $thumbUri", e) onError(e.message) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ConnectOrbotDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ConnectOrbotDialog.kt index 64b2536c4..9fe64bdf7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ConnectOrbotDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ConnectOrbotDialog.kt @@ -54,6 +54,7 @@ import com.vitorpamplona.amethyst.ui.actions.CloseButton import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.RichTextDefaults import com.vitorpamplona.amethyst.ui.theme.placeholderText +import kotlinx.coroutines.CancellationException @Composable fun ConnectOrbotDialog( @@ -83,7 +84,8 @@ fun ConnectOrbotDialog( onPost = { try { Integer.parseInt(portNumber.value) - } catch (_: Exception) { + } catch (e: Exception) { + if (e is CancellationException) throw e onError(toastMessage) return@UseOrbotButton } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt index 5fb180f8f..3fec84763 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt @@ -101,6 +101,7 @@ import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.signers.ExternalSignerLauncher import com.vitorpamplona.quartz.signers.SignerType +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.util.UUID @@ -173,6 +174,7 @@ fun LoginPage( activity.prepareToLaunchSigner() launcher.launch(it) } catch (e: Exception) { + if (e is CancellationException) throw e Log.e("Signer", "Error opening Signer app", e) scope.launch(Dispatchers.Main) { Toast.makeText( diff --git a/app/src/play/java/com/vitorpamplona/amethyst/service/lang/LanguageTranslatorService.kt b/app/src/play/java/com/vitorpamplona/amethyst/service/lang/LanguageTranslatorService.kt index 4d9bcc839..3c020b888 100644 --- a/app/src/play/java/com/vitorpamplona/amethyst/service/lang/LanguageTranslatorService.kt +++ b/app/src/play/java/com/vitorpamplona/amethyst/service/lang/LanguageTranslatorService.kt @@ -33,6 +33,7 @@ import com.google.mlkit.nl.translate.TranslatorOptions import com.linkedin.urls.detection.UrlDetector import com.linkedin.urls.detection.UrlDetectorOptions import com.vitorpamplona.amethyst.service.checkNotInMainThread +import kotlinx.coroutines.CancellationException import java.util.concurrent.Executors import java.util.regex.Pattern @@ -161,7 +162,8 @@ object LanguageTranslatorService { val short = "A$counter" counter++ returningList.put(short, tag) - } catch (_: Exception) { + } catch (e: Exception) { + if (e is CancellationException) throw e } } return returningList @@ -177,7 +179,8 @@ object LanguageTranslatorService { val short = "A$counter" counter++ returningList.put(short, lnInvoice) - } catch (_: Exception) { + } catch (e: Exception) { + if (e is CancellationException) throw e } } return returningList diff --git a/app/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt b/app/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt index cd1aadf00..03721e0b0 100644 --- a/app/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt +++ b/app/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.service.notifications import android.util.Log import com.google.firebase.messaging.FirebaseMessaging import com.vitorpamplona.amethyst.AccountInfo +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.tasks.await @@ -38,6 +39,7 @@ object PushNotificationUtils { try { RegisterAccounts(accounts).go(FirebaseMessaging.getInstance().token.await()) } catch (e: Exception) { + if (e is CancellationException) throw e Log.e("Firebase token", "failed to get firebase token", e) } } From 145b0d8f0a82c0ef90077b521f19fe88789ad03d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 31 Jan 2024 14:26:58 -0500 Subject: [PATCH 022/176] v0.83.8 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 1537ccf59..e97e7b847 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 34 - versionCode 348 - versionName "0.83.7" + versionCode 349 + versionName "0.83.8" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From ed75489975bf6417401a3482a2cbb2810fb95783 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Wed, 31 Jan 2024 19:29:03 +0000 Subject: [PATCH 023/176] New Crowdin translations by GitHub Action --- app/src/main/res/values-nl/strings.xml | 1 + app/src/main/res/values-sr-rSP/strings.xml | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 9b79a7b50..238b124b3 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -532,6 +532,7 @@ Kon LNUrl niet samenbrengen van Lightning Adress \"%1$s\". Controleer de instellingen van de gebruiker De Lightning service van de ontvanger bij %1$s is niet beschikbaar. Het werd berekend op basis van het Lightning Adress \"%2$s\". Fout: %3$s. Controleer of de server klaar is en of het Lightning Adress juist is Kan %1$s niet oplossen. Controleer of u verbonden bent, of de server online is en of het Lightning Adress %2$s juist is + Kon %1$s niet oplossen. Controleer of u verbonden bent, of de server online is en of het Lightning Adress %2$s juist is. \n\nUitzondering was: %3$s Kan invoice niet ophalen van %1$s Fout bij het verwerken van JSON uit Lightning Adress. Controleer de Lightning setup van de gebruiker Callback URL niet gevonden in de Lightning Adress configuratie van de gebruiker diff --git a/app/src/main/res/values-sr-rSP/strings.xml b/app/src/main/res/values-sr-rSP/strings.xml index 0264665e9..25197aa21 100644 --- a/app/src/main/res/values-sr-rSP/strings.xml +++ b/app/src/main/res/values-sr-rSP/strings.xml @@ -11,4 +11,26 @@ Референтни догађај није пронађен Није могуће дешифровати поруку Групна слика + Експлицитни садржај + Спам + лажно представљање + Незаконито понашање + Непознат + Икона релеја + Непознати аутор + Копирај текст + Копирај ИД аутора + Копирај ИД белешке + Емитовања + Захтевај брисање + Блокирај / Пријави + + Пријави нежељену пошту/превару + Пријавите лажно представљање + Пријавите експлицитни садржај + Пријавите незаконито понашање + Користите јавни кључ и јавни кључеви су само за читање. Пријавите се са приватним кључем да бисте могли да одговорите + Користите јавни кључ и јавни кључеви су само за читање. Пријавите се са приватним кључем да бисте могли да појачате постове + Користите јавни кључ и јавни кључеви су само за читање. Пријавите се са приватним кључем да бисте лајковали постове + Нема подешавања износа Зап. Дуго притисните да бисте променили From 34d373c293db368173b66b71bba0c90fe584b389 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 31 Jan 2024 16:05:00 -0500 Subject: [PATCH 024/176] Updates to Android Studio Hedgehog | 2023.1.1 Patch 2 --- .idea/kotlinc.xml | 2 +- app/build.gradle | 3 ++- benchmark/build.gradle | 2 +- build.gradle | 14 +++++++------- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index f8467b458..8d81632f8 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index e97e7b847..8a41b62d7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -141,7 +141,8 @@ android { } composeOptions { - kotlinCompilerExtensionVersion "1.5.3" + // Should match compose version : https://developer.android.com/jetpack/androidx/releases/compose-kotlin + kotlinCompilerExtensionVersion "1.5.8" } packagingOptions { diff --git a/benchmark/build.gradle b/benchmark/build.gradle index 61f8934fe..79f7fc1f5 100644 --- a/benchmark/build.gradle +++ b/benchmark/build.gradle @@ -52,7 +52,7 @@ dependencies { androidTestImplementation 'androidx.test:runner:1.5.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.benchmark:benchmark-junit4:1.2.2' + androidTestImplementation 'androidx.benchmark:benchmark-junit4:1.2.3' androidTestImplementation project(path: ':quartz') // Add your dependencies here. Note that you cannot benchmark code diff --git a/build.gradle b/build.gradle index 219ce3db4..a4d9e0b29 100644 --- a/build.gradle +++ b/build.gradle @@ -20,13 +20,13 @@ buildscript { } plugins { - id 'com.android.application' version '8.2.1' apply false - id 'com.android.library' version '8.2.1' apply false - id 'org.jetbrains.kotlin.android' version '1.9.10' apply false - id 'org.jetbrains.kotlin.jvm' version '1.9.10' apply false - id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.10' apply false - id 'androidx.benchmark' version '1.1.1' apply false - id 'com.diffplug.spotless' version '6.22.0' apply false + id 'com.android.application' version '8.2.2' apply false + id 'com.android.library' version '8.2.2' apply false + id 'org.jetbrains.kotlin.android' version '1.9.22' apply false + id 'org.jetbrains.kotlin.jvm' version '1.9.22' apply false + id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.22' apply false + id 'androidx.benchmark' version '1.2.3' apply false + id 'com.diffplug.spotless' version '6.25.0' apply false } subprojects { From 54155a3c30bbf59849b7f42b0b518a92c0552696 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 1 Feb 2024 15:56:13 -0500 Subject: [PATCH 025/176] Includes the product in the first message of the marketplace. --- .../amethyst/ui/navigation/AppNavigation.kt | 7 ++- .../amethyst/ui/navigation/RouteMaker.kt | 3 +- .../amethyst/ui/screen/ThreadFeedView.kt | 61 ++++++++++++------- 3 files changed, 46 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index ddc18b822..35a453b64 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -74,6 +74,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.VideoScreen import com.vitorpamplona.amethyst.ui.uriToRoute import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import java.net.URLDecoder @Composable fun AppNavigation( @@ -288,9 +289,13 @@ fun AppNavigation( route.route, route.arguments, content = { + val decodedMessage = + it.arguments?.getString("message")?.let { + URLDecoder.decode(it, "utf-8") + } ChatroomScreen( roomId = it.arguments?.getString("id"), - draftMessage = it.arguments?.getString("message"), + draftMessage = decodedMessage, accountViewModel = accountViewModel, nav = nav, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt index 8b2150edc..bddb662c0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt @@ -75,7 +75,8 @@ fun routeToMessage( val withKey = ChatroomKey(persistentSetOf(user)) accountViewModel.account.userProfile().createChatroom(withKey) return if (draftMessage != null) { - "Room/${withKey.hashCode()}?message=$draftMessage" + val encodedMessage = URLEncoder.encode(draftMessage, "utf-8") + "Room/${withKey.hashCode()}?message=$encodedMessage" } else { "Room/${withKey.hashCode()}" } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index 80dd11153..84eec451d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -59,6 +59,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -284,26 +285,27 @@ fun Modifier.drawReplyLevel( color: Color, selected: Color, ): Modifier = - this.drawBehind { - val paddingDp = 2 - val strokeWidthDp = 2 - val levelWidthDp = strokeWidthDp + 1 + this + .drawBehind { + val paddingDp = 2 + val strokeWidthDp = 2 + val levelWidthDp = strokeWidthDp + 1 - val padding = paddingDp.dp.toPx() - val strokeWidth = strokeWidthDp.dp.toPx() - val levelWidth = levelWidthDp.dp.toPx() + val padding = paddingDp.dp.toPx() + val strokeWidth = strokeWidthDp.dp.toPx() + val levelWidth = levelWidthDp.dp.toPx() - repeat(level) { - this.drawLine( - if (it == level - 1) selected else color, - Offset(padding + it * levelWidth, 0f), - Offset(padding + it * levelWidth, size.height), - strokeWidth = strokeWidth, - ) + repeat(level) { + this.drawLine( + if (it == level - 1) selected else color, + Offset(padding + it * levelWidth, 0f), + Offset(padding + it * levelWidth, size.height), + strokeWidth = strokeWidth, + ) + } + + return@drawBehind } - - return@drawBehind - } .padding(start = (2 + (level * 3)).dp) @OptIn(ExperimentalFoundationApi::class) @@ -353,11 +355,14 @@ fun NoteMaster( ) } else { Column( - modifier.fillMaxWidth().padding(top = 10.dp), + modifier + .fillMaxWidth() + .padding(top = 10.dp), ) { Row( modifier = - Modifier.padding(start = 12.dp, end = 12.dp) + Modifier + .padding(start = 12.dp, end = 12.dp) .clickable(onClick = { note.author?.let { nav("User/${it.pubkeyHex}") } }), ) { NoteAuthorPicture( @@ -443,7 +448,8 @@ fun NoteMaster( Row( modifier = - Modifier.padding(horizontal = 12.dp) + Modifier + .padding(horizontal = 12.dp) .combinedClickable( onClick = {}, onLongClick = { popupExpanded = true }, @@ -669,7 +675,9 @@ private fun RenderClassifiedsReaderForThread( } Row( - Modifier.padding(start = 20.dp, end = 20.dp, bottom = 5.dp, top = 15.dp).fillMaxWidth(), + Modifier + .padding(start = 20.dp, end = 20.dp, bottom = 5.dp, top = 15.dp) + .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { Icon( @@ -686,7 +694,9 @@ private fun RenderClassifiedsReaderForThread( Row( modifier = - Modifier.padding(start = 10.dp, end = 10.dp, bottom = 5.dp, top = 5.dp).fillMaxWidth(), + Modifier + .padding(start = 10.dp, end = 10.dp, bottom = 5.dp, top = 5.dp) + .fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { @@ -703,6 +713,7 @@ private fun RenderClassifiedsReaderForThread( } var message by remember { mutableStateOf(TextFieldValue(msg)) } + val scope = rememberCoroutineScope() TextField( value = message, @@ -725,7 +736,11 @@ private fun RenderClassifiedsReaderForThread( isActive = message.text.isNotBlank(), modifier = EditFieldTrailingIconModifier, ) { - note.author?.let { nav(routeToMessage(it, msg, accountViewModel)) } + scope.launch(Dispatchers.IO) { + note.author?.let { + nav(routeToMessage(it, note.toNostrUri() + "\n\n" + msg, accountViewModel)) + } + } } }, colors = From eea727570c43608d66e0b862b737f4aa5077cc5c Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Thu, 1 Feb 2024 20:57:56 +0000 Subject: [PATCH 026/176] New Crowdin translations by GitHub Action --- app/src/main/res/values-ar/strings.xml | 51 +++++ app/src/main/res/values-es-rES/strings.xml | 174 +++++++++++++++++ app/src/main/res/values-es-rMX/strings.xml | 3 +- app/src/main/res/values-es-rUS/strings.xml | 5 +- app/src/main/res/values-uk/strings.xml | 208 +++++++++++++++++++++ 5 files changed, 438 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index ed1e090c5..fd4b04a0f 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -85,9 +85,12 @@ الرسائل الخاصة الرسائل العامة الموجز العام + البحث أضف Relay إسم العرض إسم العرض الخاص بي + اسمي المميز + مرحبا بك! اسم المستخدم اسم المستخدم الخاص بي ْعَنِّي @@ -126,6 +129,7 @@ إرسال رسالة مباشرة القيام بتحرير metadata للمستخدم متابعة + إرجاع المتابعة رفع الحظر نسخ معرف المستخدم إلغاء حظر عن مستخدم @@ -140,7 +144,14 @@ شروط الاستخدام يجب قبول شروط الإستخدام المفتاح مطلوب + يجب ادخال اسم تسجيل دخول + التسجيل + انشاء حساب + كيف ينبغي أن ندعوك؟ + ليس لديك حساب؟ + هل لديك حساب nostr بالفعل؟ + أنشئ حسابًا جديدًا إنشاء مفتاح جديد بإنتظار التحديثات جاري تحميل الحساب @@ -249,6 +260,7 @@ إزالة من المفضلات الخاصة إزالة من المفضلات العامة خدمة Wallet Connect + السماح لمفتاحك السري بدفع ال zaps بدون الحاجة الى لخروج من التطبيق (دع مفتاحك السري ٱمن و استخدم خادم خاص إن امكن) خادم Wallet Connect كلمة سر خدمة Wallet Connect إظهار كلمة السر @@ -293,6 +305,7 @@ المستقبل والجمهور لا يعرفان من الذي قام بإرسال الدفع لا يوجد أثر في نوستر، فقط على شبكة الLightning خادم الملفات + user@ او Lnaddres المرحلات الخاصة بك (NIP-95) إعداد Tor/Orbot الاتصال من خلال إعدادات Orbot الخاص بك @@ -303,6 +316,9 @@ قائمة المتابعة جميع المتابعات العالمي + قائمة الحسابات المكتومة + الرسائل الخاصة + التنبيه عند وصول رسالة خاصة من طرف %1$s ل %1$s تنبيه: @@ -347,6 +363,8 @@ المصادقة الدفع Cashu Token + تسخ الtoken + نسخ الtoken من المحفظة تسجيل الخروج سوف يؤدي الى جميع المعلومات المحلية الخاصة بك. تأكد من وجود نسخة احتياطية لمفاتحك السرية لتجنب فقدان حسابك. هل تريد الاستمرار؟ العلامات المُتابعة @@ -416,6 +434,12 @@ موافق فشل الوصول إلى %1$s: %2$s + الصفحة الرئيسية + البحث + خطأ في فتح تطبيق الموقِّع (signer app) + الكلمات المخفية + إخفاء كلمة أو جملة جديدة + حدد خيارا %1$s sats أرسلت الى محفظتك (الرسوم: sats %2$s ) تعذر جلب الinvoice من خوادم المستلم @@ -436,4 +460,31 @@ لاستخدام الإشعارات حمل أي تطبيق يدعم ال [Unified Push](https://unifiedpush.org/) مثل [Nfty](https://ntfy.sh/). بعد تثبيتك التطبيق قم باختياره من الإعدادات + العنوان + iPhone 13 + الشرط + الفئة + الموقع الجغرافيّ + كالجديد + مستعمل، لكن لا تظهر عليه أي ٱثار للاستعمال + بحالة جيدة + لديه بعض علامات الاستخدام السطحية + جيد + لا يزال في شكل مقبول وعملي + ملابس + اكسسوارات + إلكترونيات + أثاث + كتب + حيوانات أليفة + الرياضة + اللياقة البدنية + فن + الحرف + الصفحة الرئيسية + المكتب + طعام + أخرى + فشل في تحميل الوسائط + خطأ في التحميل: %1$s diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 5951c0b6d..2227e3d4f 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -6,10 +6,13 @@ Escanear el QR Mostrar de todos modos Post marcado como inapropiado por + El evento se está cargando o no se puede encontrar en la lista de relés Imagen del canal Evento referenciado no encontrado + No se pudo desencriptar el mensaje Imagen de grupo Contenido explícito + Spam Suplantación de identidad Comportamiento ilegal Desconocido @@ -19,6 +22,8 @@ Copiar PubKey del usuario Copiar ID de la nota Transmisión + Solicitar eliminación + Bloquear / Reportar Reportar Spam / Estafa Reportar suplantación de identidad @@ -29,8 +34,14 @@ Inicie sesión con una clave privada para dar me gusta a los posts Falta la configuración de Zaps por defecto. Mantén pulsado unos segundos para cambiarla Inicie sesión con una clave privada para poder enviar Zaps + Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder seguir a otros usuarios. + Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder dejar de seguir a otros usuarios. + Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder ocultar una palabra o frase. + Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder mostrar una palabra o frase. + Zaps Total vistas Impulsar + Impulsada Cita Nueva cantidad en Sats Añadir @@ -38,6 +49,8 @@ " y " "en canal " Banner de perfil + El pago se ha realizado correctamente + Error al analizar el mensaje de error " Siguiendo" " Seguidores" Perfil @@ -67,6 +80,7 @@ No se pudo cargar la imagen Dirección del transmisor Publicaciones + Bytes Errores Tus noticias Fuente de mensajes privados @@ -76,6 +90,8 @@ Anadir transmisor Nombre para mostrar Mi nombre para mostrar + Avestruz Maravillosa + ¡Te damos la bienvenida, Avestruz! Nombre de usuario Mi nombre de usuario Sobre mí @@ -114,6 +130,7 @@ Enviar un mensaje directo Edita los metadatos del usuario Seguir + Seguir también Desbloquear Copiar ID de usuario Desbloquear usuario @@ -128,9 +145,17 @@ términos de uso Se requiere la aceptación de los términos Se requiere la clave + Se requiere un nombre Acceso + Registrarse + Crear cuenta + ¿Cómo debemos llamarte? + ¿No tienes una cuenta de Nostr? + ¿Ya tienes una cuenta de Nostr? + Crear una cuenta nueva Generar una nueva clave Cargando el tablón… + Cargando cuenta "Error al cargar las respuestas: " Intentar otra vez Tablón vacío @@ -148,13 +173,18 @@ Chat público publicaciones recibidas Eliminar + Automático traducido de a Mostrar en %1$s primero Traducir siempre a %1$s Nunca traducir desde %1$s + Dirección de Nostr nunca ahora + h + m + d Desnudos Blasfemias / Discurso de odio Denuncia discurso de odio @@ -164,9 +194,18 @@ Marcar los nuevos como leídos Marcar todos como leídos Claves de respaldo + ## Consejos sobre la protección y la copia de seguridad de las claves + \n\nTu cuenta está protegida por una clave secreta. Esta clave es una cadena aleatoria y larga que empieza por **nsec1**. Cualquiera que tenga acceso a tu clave secreta puede publicar contenido usando tu identidad. + \n\n- **No** guardes la clave secreta en ningún sitio web o software en los que no confíes. + \n- Los desarrolladores de Amethyst **nunca** te pedirán la clave secreta. + \n- **Sí** puedes realizar una copia de seguridad de la clave secreta para recuperar la cuenta. Para ello, te recomendamos usar un gestor de contraseñas. + Clave secreta (nsec) copiada al portapapeles Copiar mi clave secreta Autenticación fallida + El sistema biométrico no pudo autenticar al propietario de este teléfono + El sistema biométrico no pudo autenticar al propietario de este teléfono. Error: %1$s + Error "Credo por %1$s" "Imagen del Badge para %1$s" Has recibido un nuevo Badge @@ -175,6 +214,15 @@ \@npub del autor copiado al portapapeles ID de nota copiada (@note1) al portapapeles Seleccionar texto + "<Unable to decrypt private message>\n\nTe citaron en una conversación privada o encriptada entre %1$s y %2$s." + Agregar cuenta nueva + Cuentas + Seleccionar cuenta + Agregar cuenta nueva + Cuenta activa + Tiene clave privada + Solo de lectura, sin clave privada + Volver Selecciona Compartir enlace del navegador Compartir @@ -186,4 +234,130 @@ Seguir Solicitar eliminación Amethyst solicitará que se elimine su nota de los relays a los que está conectado actualmente. No hay garantía de que su nota se elimine permanentemente de esos relays, o de otros relays donde pueda almacenarse. + Suplantación de identidad malintencionada + Todos los reportes publicados serán visibles públicamente. + Motivo + Selecciona un motivo… + Publicar reporte + Bloquear y reportar + Bloquear + Marcadores + Marcadores privados + Marcadores públicos + Agregar a marcadores privados + Agregar a marcadores públicos + Eliminar de marcadores privados + Eliminar de marcadores públicos + Servicio de conexión a monedero + Autoriza a un secreto de Nostr para pagar zaps sin salir de la app. Mantén el secreto seguro y usa un relé privado si es posible. + Clave pública de conexión a monedero + Relé de conexión a monedero + Secreto de conexión a monedero + Clave secreta de conexión a monedero + clave privada nsec / hex + Destinar cantidad en sats + Publicar encuesta + Campos obligatorios: + Destinatarios de zaps + Descripción de encuesta principal… + Opción %s + Descripción de opción de encuesta + Campos opcionales: + Zap mínimo + Zap máximo + Consenso + (0–100)% + Cerrar después de + días + No se puede votar + Encuesta cerrada para votos nuevos + Monto del zap + Solo se permite un voto por usuario en este tipo de encuesta + "Buscando evento %1$s" + Agregar un mensaje público + Agregar un mensaje privado + Agregar un mensaje con factura + ¡Gracias por tu trabajo! + Crear y añadir + Los autores de encuestas no pueden votar en ellas. + ¿Qué significa esto? + Este contenido es el mismo desde la publicación + Este contenido ha cambiado. Es posible que el autor no haya visto o aprobado el cambio. + Añadir imagen + Añadir vídeo + Añadir documento + Añadir al mensaje + Descripción del contenido + Un bote azul en una playa de arena blanca al atardecer + Tipo de zap + Tipo de zap para todas las opciones + Público + Todos pueden ver la transacción y el mensaje + Privado + El remitente y el destinatario pueden verse entre sí y leer el mensaje + Non-Zap + No hay rastro en Nostr, solo en Lightning + Lista de silenciados + ## Conéctate a través de Tor con Orbot + \n\n1. Instala [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android). + \n2. Inicia Orbot. + \n3. En Orbot, comprueba el puerto Socks. La opción predeterminada es 9050. + \n4. Si es necesario, cambia el puerto en Orbot. + \n5. Configura el puerto Socks en la pantalla. + \n6. Presiona y activa el botón para usar Orbot como proxy. + + Puerto Socks de Orbot + Número de puerto inválido + Usar Orbot + Desconectar Tor/Orbot + Mensajes privados + Te notifica cuando llega un mensaje privado + Zaps recibidos + Te notifica cuando alguien te zapea + %1$s sats + De %1$s + para %1$s + Notificar: + Unirse a la conversación + Zapraiser + Añade una meta de sats para recaudar por esta publicación. Los clientes compatibles pueden mostrarla como una barra de progreso para incentivar las donaciones. + Cantidad objetivo en sats + Zapraiser en %1$s. %2$s sats hasta la meta + Relé de lectura + Relé de escritura + Se ha producido un error al intentar obtener información sobre el relé de %1$s + Propietario + Versión + Software + Contacto + NIP compatibles + Tasas de admisión + URL de pagos + Limitaciones + Canjear + Enviar a monedero de zaps + Abrir en monedero de Cashu + Copiar token + La dirección de Lightning no se ha configurado + Token copiado al portapapeles + EN VIVO + FUERA DE LÍNEA + TERMINÓ + PROGRAMADO + La transmisión en vivo está fuera de línea + La transmisión en vivo ha terminado + Al cerrar la sesión se borra toda tu información local. Asegúrate de tener una copia de seguridad de tus claves privadas para que no pierdas la cuenta. ¿Quieres continuar? + Etiquetas seguidas + Relés + Oscuro + Preferencias de la aplicación + Idioma + Tema + Vista previa de imagen + Reproducción de vídeo + Vista previa de URL + Desplazamiento inmersivo + Ocultar barras de navegación al desplazarse + Cargar imagen + Spammers diff --git a/app/src/main/res/values-es-rMX/strings.xml b/app/src/main/res/values-es-rMX/strings.xml index 0cc233fe2..1c00aa805 100644 --- a/app/src/main/res/values-es-rMX/strings.xml +++ b/app/src/main/res/values-es-rMX/strings.xml @@ -367,7 +367,7 @@ Zapraiser Agrega una meta de sats para recaudar por esta publicación. Los clientes compatibles pueden mostrarla como una barra de progreso para incentivar las donaciones. Cantidad objetivo en sats - Zapraiser a %1$s. %2$s sats hasta la meta + Zapraiser en %1$s. %2$s sats hasta la meta Relé de lectura Relé de escritura Se produjo un error al intentar obtener información sobre el relé de %1$s @@ -532,6 +532,7 @@ No se pudo ensamblar la LNUrl desde la dirección de Lightning \"%1$s\". Comprueba la configuración del usuario. El servicio de Lightning del destinatario en %1$s no está disponible. Se calculó a partir de la dirección de Lightning \"%2$s\". Error: %3$s. Comprueba si el servidor está activo y si la dirección de Lightning es correcta. No se pudo resolver %1$s. Comprueba si tienes conexión, si el servidor está activo y si la dirección de Lightning \"%2$s\" es correcta. + No se pudo resolver %1$s. Comprueba si tienes conexión, si el servidor está activo y si la dirección de Lightning \"%2$s\" es correcta..\n\nLa excepción fue: %3$s No se pudo obtener la factura de %1$s Error al analizar JSON desde la dirección de Lightning. Comprueba la configuración de Lightning del usuario. No se encontró la URL de devolución de llamada en la configuración del servidor de la dirección de Lightning del usuario. diff --git a/app/src/main/res/values-es-rUS/strings.xml b/app/src/main/res/values-es-rUS/strings.xml index cea4db84d..126a537c5 100644 --- a/app/src/main/res/values-es-rUS/strings.xml +++ b/app/src/main/res/values-es-rUS/strings.xml @@ -367,7 +367,7 @@ Zapraiser Agrega una meta de sats para recaudar por esta publicación. Los clientes compatibles pueden mostrarla como una barra de progreso para incentivar las donaciones. Cantidad objetivo en sats - Zapraiser a %1$s. %2$s sats hasta la meta + Zapraiser en %1$s. %2$s sats hasta la meta Relé de lectura Relé de escritura Se produjo un error al intentar obtener información sobre el relé de %1$s @@ -429,7 +429,7 @@ Idioma Tema Vista previa de imagen - Reproducción de video + Reproducción de vídeo Vista previa de URL Desplazamiento inmersivo Ocultar barras de navegación al desplazarse @@ -532,6 +532,7 @@ No se pudo ensamblar la LNUrl desde la dirección de Lightning \"%1$s\". Comprueba la configuración del usuario. El servicio de Lightning del destinatario en %1$s no está disponible. Se calculó a partir de la dirección de Lightning \"%2$s\". Error: %3$s. Comprueba si el servidor está activo y si la dirección de Lightning es correcta. No se pudo resolver %1$s. Comprueba si tienes conexión, si el servidor está activo y si la dirección de Lightning \"%2$s\" es correcta. + No se pudo resolver %1$s. Comprueba si tienes conexión, si el servidor está activo y si la dirección de Lightning \"%2$s\" es correcta..\n\nLa excepción fue: %3$s No se pudo obtener la factura de %1$s Error al analizar JSON desde la dirección de Lightning. Comprueba la configuración de Lightning del usuario. No se encontró la URL de devolución de llamada en la configuración del servidor de la dirección de Lightning del usuario. diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 962303166..b25a4a71e 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -33,9 +33,14 @@ Увійдіть з приватним ключем щоб лайкати дописи Не налаштовані запи. Натисніть та утримуйте для налаштування Увійдіть з приватним ключем щоб запати + Ви використовуєте відкритий ключ, і відкриті ключі доступні лише для читання. Увійдіть за допомогою закритого ключа, щоб мати можливість стежити + Ви використовуєте відкритий ключ, і відкриті ключі доступні лише для читання. Увійдіть за допомогою закритого ключа, щоб мати можливість скасувати підписку + Ви використовуєте відкритий ключ, і відкриті ключі доступні лише для читання. Увійдіть за допомогою закритого ключа, щоб мати можливість приховати слово або речення + Ви використовуєте відкритий ключ, і відкриті ключі доступні лише для читання. Увійдіть за допомогою закритого ключа, щоб мати можливість показати слово або речення Запи Перегляди Просувати + підсилено Цитувати Нова сума в sat Додати @@ -43,6 +48,8 @@ " та " "в каналі " Банер профілю + Успішно сплачено + Помилка при обробці повідомлення про помилку " Підписок" " Підписників" Профіль @@ -135,9 +142,17 @@ умови користування Треба прийняти умови користування Треба ввести ключ + Необхідно вказати ім\'я Увійти + Реєстрація + Створити обліковий запис + Як ми можемо до вас звертатися? + Не маєте облікового запису Nostr? + Вже маєте обліковий запис Nostr? + Створити новий обліковий запис Згенерувати ключ Завантаження стрічки + Завантаження облікового запису "Не вдалося завантажити відповіді: " Повторити Стрічка порожня. @@ -161,6 +176,7 @@ Показати спершу на %1$s Завжди перекладати на %1$s Не переводити з %1$s + Адреса Nostr ніколи зараз год @@ -184,6 +200,8 @@ Приватний ключ (nsec) скопійовано Скопіювати мій приватний ключ Помилка входу + Не вдалося виконати біометрику для автентифікації власника цього телефону + Не вдалося виконати біометрику для автентифікації власника цього телефону Помилка "Створено %1$s" "Картинка значка-нагороди %1$s" @@ -260,8 +278,11 @@ Мінімальний зап Максимальний зап Консенсус + (0–100)% Закриття після днів + Неможливо проголосувати + Опитування закрито для нових голосів Сума запу У цьому опитуванні можна голосувати тільки один раз "Пошук події %1$s" @@ -271,6 +292,7 @@ Дякую всім вам за роботу! Створити та Додати Автор опитування не може голосувати + Що це значить? Фото не змінилося після публікації Фото змінено. Автор міг не помітити зміну Додати фото @@ -293,8 +315,24 @@ LnAddress або @User Ваші релеї (NIP-95) Файл зберігається на ваших релеях. Новий NIP: може не працювати в інших клієнтах + Налаштування Tor/Orbot + Підключення через налаштування Orbot + Від\'єднатися від вашого Orbot/Tor? + Ваші дані буде негайно перенесено у звичайну мережу + Так + Ні + Слідкуйте за списком Всі підписки Глобально + ## Підключіться через Tor з Orbot + \n\n1. Установіть [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android) + \n2. Запустіть Orbot + \n3. В Orbot перевірте порт Socks. За умовчанням використовується 9050 + \n4. Якщо необхідно, змініть порт в Orbot + \n5. Налаштуйте порт Socks на цьому екрані + \n6. Натисніть кнопку «Активувати», щоб використовувати Orbot як проксі + Невірний номер порту + Відключити Tor/Orbot Особисті повідомлення Сповіщення про вхідні повідомлення Запи @@ -302,4 +340,174 @@ %1$s sat Від %1$s за %1$s + Сповіщення: + Долучитися до розмови + ID користувача або групи + Створити + Долучитися + Сьогодні + Попередження про контент + Цей пост містить чутливий контент, який деякі люди можуть знайти образливим + Завжди приховувати чутливий контент + Завжди показувати чутливий контент + Завжди показувати попередження про контент + Рекомендовані: + Фільтрувати спам від незнайомців + Попереджати, коли публікації містять звіти про ваші підписки + Новий символ реакції + Для цього користувача не вибрано жодного типу реакції. Довге натискання на кнопку серця, щоб змінити + Додає цільову суму сат, яку потрібно отримати для цієї посади. Клієнти, які підтримують, можуть відображати це як індикатор прогресу, щоб стимулювати пожертвування + Власник + Версія + Програмне забезпечення + Підтримувані NIP + Плата за вступ + Посилання на оплату + Обмеження + Країни + Мови + Теги + Політика публікації + Довжина повідомлення + Підписки + Фільтри + Довжина Id підписки + Мінімальний префікс + Максимальна кількість тегів події + Тривалість контенту + Мінімальний PoW + Оплата + Надіслати до Zap Wallet + Токен скопійовано до буферу обміну + НАЖИВО + ОФЛАЙН + ЗАВЕРШЕНО + ЗАПЛАНОВАНО + Пряма трансляція Офлайн + Пряму трансляцію завершено + Вихід з облікового запису видаляє всю вашу локальну інформацію. Переконайтеся, що ваші особисті ключі були збережені, щоб уникнути втрати вашого облікового запису. Ви хочете продовжити? + Відстежувані теги + Торгівельний майданчик + Наживо + Спільнота + Чати + Затверджені публікації + Ця група не має опису або правил. Зверніться до автора, щоб додати один + Ця спільнота не має опису або правил. Зверніться до автора, щоб додати один + Чутливий контент + Додає попередження про чутливий контент перед показом цього контенту + Налаштування + Завжди + Тільки через Wi-Fi + Ніколи + Система + Світла + Темна + Налаштування додатку + Мова + Оформлення + Попередній перегляд + Відтворення відео + Перегляд URL-адреси + Розширена прокрутка + Приховати Nav панель під час прокручування + Завантажити зображення + Спам + Звук вимкнено. Натисніть, щоб увімкнути звук + Звук увімкнено. Натисніть, щоб вимкнути звук + Пошук локальних і віддалених записів + Адресу Nostr було верифіковано + Перевірка адреси Nostr + Вибрати/Скасувати все + За замовчуванням + Виберіть ретранслятор, щоб продовжити + Переслати Zaps до: + Підтримання клієнтів переведе його на LNAddress або профіль користувача нижче замість ваших + Надати місцеперебування як + Нова функція + Активувати + Тема бесіди + "\@User1, @User2, @User3" + Учасники цієї групи + Пояснення для учасників + Зміна назви нових цілей. + Вставити з буферу + Для інтерфейсу додатку + Темна, світла або системна тема + Автоматично завантажувати зображення і GIF + Автоматично відтворювати відео та GIF-зображення + Попередній перегляд URL + Під час завантаження зображень + Скопіювати в буфер + Скопіювати URL в буфер обміну + Копіювати ідентифікатор нотатки в буфер обміну + Створено в + Правила + Увійти з Amber + Оновіть свій статус + Помилка при обробці повідомлення про помилку + Не вдалося надіслати файл Zap + Написати користувачу + ОК + Не вдалося зв’язатися з %1$s: %2$s + Не вдалося зв’язатися з %1$s: %2$s + Не вдалося обробити результат від %1$s: %2$s + %1$s помилка з кодом %2$s + Активно для: + Головна + DMs + Чати + Глобально + Пошук + Знайти та додати користувача + Ім\'я користувача або ім\'я для відображення + 25 + Сплачено + Гаманець: %1$s + Помилка при відкритті додатка для входу + Додаток для входу не знайдено. Перевірте чи не було видалено додаток + Заявку на вхід відхилено + Приховати нове слово або речення + Зображення профілю + Показувати зображення профілю + Вибрати опцію + Не вдалося сплатити рахунок + Не вдалося зняти + Не вдалося налаштувати Wallet Connect + Помилка аналізу рядка підключення NIP-47. Перевірте, чи це правильно, у вашого постачальника Wallet: %1$s. Помилка: %2$s + Помилка аналізу рядка підключення NIP-47. Перевірте, чи це правильно, у вашого постачальника Wallet: %1$s. + Mint надав таке повідомлення про помилку: %1$s + Cashu токени вже витрачені. + Отримано Cashu + %1$s sats були відправлені в ваш гаманець. (Fees: %2$s sats) + У системі не знайдено сумісний гаманець Cashu + Не вдалося отримати рахунок з серверів отримувача + Постачальник підключення гаманця повернув наступну помилку: %1$s + Не вдалося підключитися до Tor + Це абсолютно нова одиниця, в оригінальній коробці + Як нове + Він використовується, але немає ознак використання + Добре + Він має декілька поверхових знаків використання + Непогано + Він все ще знаходиться у прийнятній і функціональній формі + Одяг + Аксесуари + Електроніка + Меблі + Колекційні елементи + Книги + Домашні тварини + Спорт + Фітнес + Мистецтво + Ремесла + Інше + Не вдалося завантажити фото + Не вдалося відкрити стиснутий файл + Помилка стискання медіафайлу: %1$s + Помилка завантаження: %1$s + Сервер не задав URL після завантаження + Не вдалося завантажити завантажені медіафайли з сервера + Не вдалося підготувати локальний файл для завантаження: %1$s From e56377f8c3f8e8e53844d1cc56786f3072876e42 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 1 Feb 2024 18:31:28 -0500 Subject: [PATCH 027/176] Adds support for NIP-29 in public messages and new DMs. NIP-54 stays in NIP-54 --- README.md | 1 + .../vitorpamplona/amethyst/model/Account.kt | 14 +- .../amethyst/service/CachedRichTextParser.kt | 37 ++-- .../amethyst/ui/actions/NewPostViewModel.kt | 190 ++++-------------- .../amethyst/ui/components/RichTextViewer.kt | 7 +- .../ui/note/ChatroomMessageCompose.kt | 14 +- .../ui/screen/loggedIn/ChannelScreen.kt | 7 + .../ui/screen/loggedIn/ChatroomScreen.kt | 6 + .../vitorpamplona/quartz/encoders/Nip29.kt | 76 +++++++ .../vitorpamplona/quartz/encoders/Nip54.kt | 38 +++- .../quartz/events/ChannelMessageEvent.kt | 7 +- .../quartz/events/ChatMessageEvent.kt | 9 + .../quartz/events/FileHeaderEvent.kt | 22 +- .../events/LiveActivitiesChatMessageEvent.kt | 7 +- .../quartz/events/NIP24Factory.kt | 2 + .../quartz/events/PollNoteEvent.kt | 7 +- .../quartz/events/PrivateDmEvent.kt | 28 ++- .../quartz/events/TextNoteEvent.kt | 7 +- 18 files changed, 277 insertions(+), 202 deletions(-) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip29.kt rename app/src/main/java/com/vitorpamplona/amethyst/service/Nip44UrlParser.kt => quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip54.kt (65%) diff --git a/README.md b/README.md index 4ff182488..abab32af1 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases) - [ ] Delegated Event Signing (NIP-26, Will not implement) - [x] Text Note References (NIP-27) - [x] Public Chats (NIP-28) +- [x] Inline Metadata (NIP-29) - [x] Custom Emoji (NIP-30) - [x] Event kind summaries (NIP-31) - [ ] Labeling (NIP-32) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 5ad77ecac..8ef1ab90c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1325,7 +1325,7 @@ class Account( directMentions: Set, relayList: List? = null, geohash: String? = null, - nip94attachments: List? = null, + nip94attachments: List? = null, ) { if (!isWriteable()) return @@ -1381,7 +1381,7 @@ class Account( zapRaiserAmount: Long? = null, relayList: List? = null, geohash: String? = null, - nip94attachments: List? = null, + nip94attachments: List? = null, ) { if (!isWriteable()) return @@ -1428,7 +1428,7 @@ class Account( wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null, - nip94attachments: List? = null, + nip94attachments: List? = null, ) { if (!isWriteable()) return @@ -1461,7 +1461,7 @@ class Account( wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null, - nip94attachments: List? = null, + nip94attachments: List? = null, ) { if (!isWriteable()) return @@ -1495,6 +1495,7 @@ class Account( wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null, + nip94attachments: List? = null, ) { sendPrivateMessage( message, @@ -1505,6 +1506,7 @@ class Account( wantsToMarkAsSensitive, zapRaiserAmount, geohash, + nip94attachments, ) } @@ -1517,6 +1519,7 @@ class Account( wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null, + nip94attachments: List? = null, ) { if (!isWriteable()) return @@ -1533,6 +1536,7 @@ class Account( markAsSensitive = wantsToMarkAsSensitive, zapRaiserAmount = zapRaiserAmount, geohash = geohash, + nip94attachments = nip94attachments, signer = signer, advertiseNip18 = false, ) { @@ -1551,6 +1555,7 @@ class Account( wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null, + nip94attachments: List? = null, ) { if (!isWriteable()) return @@ -1567,6 +1572,7 @@ class Account( markAsSensitive = wantsToMarkAsSensitive, zapRaiserAmount = zapRaiserAmount, geohash = geohash, + nip94attachments = nip94attachments, signer = signer, ) { broadcastPrivately(it) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt index 6f3046af9..e0f944138 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt @@ -34,6 +34,9 @@ import com.vitorpamplona.amethyst.ui.components.imageExtensions import com.vitorpamplona.amethyst.ui.components.removeQueryParamsForExtensionComparison import com.vitorpamplona.amethyst.ui.components.tagIndex import com.vitorpamplona.amethyst.ui.components.videoExtensions +import com.vitorpamplona.quartz.encoders.Nip29 +import com.vitorpamplona.quartz.encoders.Nip54 +import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.ImmutableListOfLists import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap @@ -94,27 +97,33 @@ val HTTPRegex = .toRegex(RegexOption.IGNORE_CASE) class RichTextParser() { - fun parseMediaUrl(fullUrl: String): ZoomableUrlContent? { + fun parseMediaUrl( + fullUrl: String, + tags: ImmutableListOfLists, + ): ZoomableUrlContent? { val removedParamsFromUrl = removeQueryParamsForExtensionComparison(fullUrl) return if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) { - val frags = Nip44UrlParser().parse(fullUrl) + val frags = Nip54().parse(fullUrl) + val tags = Nip29().parse(fullUrl, tags.lists) + ZoomableUrlImage( url = fullUrl, - description = frags["alt"], - hash = frags["x"], - blurhash = frags["blurhash"], - dim = frags["dim"], - contentWarning = frags["content-warning"], + description = frags[FileHeaderEvent.ALT] ?: tags[FileHeaderEvent.ALT], + hash = frags[FileHeaderEvent.HASH] ?: tags[FileHeaderEvent.HASH], + blurhash = frags[FileHeaderEvent.BLUR_HASH] ?: tags[FileHeaderEvent.BLUR_HASH], + dim = frags[FileHeaderEvent.DIMENSION] ?: tags[FileHeaderEvent.DIMENSION], + contentWarning = frags["content-warning"] ?: tags["content-warning"], ) } else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) { - val frags = Nip44UrlParser().parse(fullUrl) + val frags = Nip54().parse(fullUrl) + val tags = Nip29().parse(fullUrl, tags.lists) ZoomableUrlVideo( url = fullUrl, - description = frags["alt"], - hash = frags["x"], - blurhash = frags["blurhash"], - dim = frags["dim"], - contentWarning = frags["content-warning"], + description = frags[FileHeaderEvent.ALT] ?: tags[FileHeaderEvent.ALT], + hash = frags[FileHeaderEvent.HASH] ?: tags[FileHeaderEvent.HASH], + blurhash = frags[FileHeaderEvent.BLUR_HASH] ?: tags[FileHeaderEvent.BLUR_HASH], + dim = frags[FileHeaderEvent.DIMENSION] ?: tags[FileHeaderEvent.DIMENSION], + contentWarning = frags["content-warning"] ?: tags["content-warning"], ) } else { null @@ -146,7 +155,7 @@ class RichTextParser() { } val imagesForPager = - urlSet.mapNotNull { fullUrl -> parseMediaUrl(fullUrl) }.associateBy { it.url } + urlSet.mapNotNull { fullUrl -> parseMediaUrl(fullUrl, tags) }.associateBy { it.url } val imageList = imagesForPager.values.toList() val emojiMap = diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index 1bc8a5fbd..e137d1cfd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -71,7 +71,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.launch -import java.net.URLEncoder enum class UserSuggestionAnchor { MAIN_MESSAGE, @@ -89,6 +88,7 @@ open class NewPostViewModel() : ViewModel() { var pTags by mutableStateOf?>(null) var eTags by mutableStateOf?>(null) + var imetaTags = mutableStateListOf>() var nip94attachments by mutableStateOf>(emptyList()) var nip95attachments by @@ -266,44 +266,46 @@ open class NewPostViewModel() : ViewModel() { val urls = findURLs(tagger.message) val usedAttachments = nip94attachments.filter { it.urls().intersect(urls.toSet()).isNotEmpty() } - usedAttachments.forEach { account?.sendHeader(it, relayList, {}) } + // Doesn't send as nip94 yet because we don't know if it makes sense. + // usedAttachments.forEach { account?.sendHeader(it, relayList, {}) } if (originalNote?.channelHex() != null) { if (originalNote is AddressableEvent && originalNote?.address() != null) { account?.sendLiveMessage( - tagger.message, - originalNote?.address()!!, - tagger.eTags, - tagger.pTags, - zapReceiver, - wantsToMarkAsSensitive, - localZapRaiserAmount, - geoHash, + message = tagger.message, + toChannel = originalNote?.address()!!, + replyTo = tagger.eTags, + mentions = tagger.pTags, + zapReceiver = zapReceiver, + wantsToMarkAsSensitive = wantsToMarkAsSensitive, + zapRaiserAmount = localZapRaiserAmount, + geohash = geoHash, nip94attachments = usedAttachments, ) } else { account?.sendChannelMessage( - tagger.message, - tagger.channelHex!!, - tagger.eTags, - tagger.pTags, - zapReceiver, - wantsToMarkAsSensitive, - localZapRaiserAmount, - geoHash, + message = tagger.message, + toChannel = tagger.channelHex!!, + replyTo = tagger.eTags, + mentions = tagger.pTags, + zapReceiver = zapReceiver, + wantsToMarkAsSensitive = wantsToMarkAsSensitive, + zapRaiserAmount = localZapRaiserAmount, + geohash = geoHash, nip94attachments = usedAttachments, ) } } else if (originalNote?.event is PrivateDmEvent) { account?.sendPrivateMessage( - tagger.message, - originalNote!!.author!!, - originalNote!!, - tagger.pTags, - zapReceiver, - wantsToMarkAsSensitive, - localZapRaiserAmount, - geoHash, + message = tagger.message, + toUser = originalNote!!.author!!, + replyingTo = originalNote!!, + mentions = tagger.pTags, + zapReceiver = zapReceiver, + wantsToMarkAsSensitive = wantsToMarkAsSensitive, + zapRaiserAmount = localZapRaiserAmount, + geohash = geoHash, + nip94attachments = usedAttachments, ) } else if (originalNote?.event is ChatMessageEvent) { val receivers = @@ -324,6 +326,7 @@ open class NewPostViewModel() : ViewModel() { zapReceiver = zapReceiver, zapRaiserAmount = localZapRaiserAmount, geohash = geoHash, + nip94attachments = usedAttachments, ) } else if (!dmUsers.isNullOrEmpty()) { if (nip24 || dmUsers.size > 1) { @@ -337,6 +340,7 @@ open class NewPostViewModel() : ViewModel() { zapReceiver = zapReceiver, zapRaiserAmount = localZapRaiserAmount, geohash = geoHash, + nip94attachments = usedAttachments, ) } else { account?.sendPrivateMessage( @@ -348,6 +352,7 @@ open class NewPostViewModel() : ViewModel() { zapReceiver = zapReceiver, zapRaiserAmount = localZapRaiserAmount, geohash = geoHash, + nip94attachments = usedAttachments, ) } } else { @@ -461,21 +466,12 @@ open class NewPostViewModel() : ViewModel() { onProgress = {}, ) - if (!isPrivate) { - createNIP94Record( - uploadingResult = result, - localContentType = contentType, - alt = alt, - sensitiveContent = sensitiveContent, - ) - } else { - noNIP94( - uploadingResult = result, - localContentType = contentType, - alt = alt, - sensitiveContent = sensitiveContent, - ) - } + createNIP94Record( + uploadingResult = result, + localContentType = contentType, + alt = alt, + sensitiveContent = sensitiveContent, + ) } catch (e: Exception) { if (e is CancellationException) throw e Log.e( @@ -508,6 +504,7 @@ open class NewPostViewModel() : ViewModel() { urlPreview = null isUploadingImage = false pTags = null + imetaTags.clear() wantsDirectMessage = false @@ -703,21 +700,6 @@ open class NewPostViewModel() : ViewModel() { contentToAddUrl == null } - fun includePollHashtagInMessage( - include: Boolean, - hashtag: String, - ) { - if (include) { - updateMessage(TextFieldValue(message.text + " $hashtag")) - } else { - updateMessage( - TextFieldValue( - message.text.replace(" $hashtag", "").replace(hashtag, ""), - ), - ) - } - } - suspend fun createNIP94Record( uploadingResult: Nip96Uploader.PartialEvent, localContentType: String?, @@ -751,25 +733,13 @@ open class NewPostViewModel() : ViewModel() { mimeType = remoteMimeType ?: localContentType, dimPrecomputed = dim, onReady = { header: FileHeader -> - account?.createHeader(imageUrl, magnet, header, alt, sensitiveContent, originalHash) { - event, - -> + account?.createHeader(imageUrl, magnet, header, alt, sensitiveContent, originalHash) { event -> isUploadingImage = false nip94attachments = nip94attachments + event - val contentWarning = if (sensitiveContent) "" else null + message = TextFieldValue( - message.text + - "\n" + - addInlineMetadataAsNIP54( - imageUrl, - header.dim, - header.mimeType, - alt, - header.blurHash, - header.hash, - contentWarning, - ), + message.text + "\n" + imageUrl, ) urlPreview = findUrlInMessage() } @@ -781,84 +751,6 @@ open class NewPostViewModel() : ViewModel() { ) } - suspend fun noNIP94( - uploadingResult: Nip96Uploader.PartialEvent, - localContentType: String?, - alt: String?, - sensitiveContent: Boolean, - ) { - // Images don't seem to be ready immediately after upload - val imageUrl = uploadingResult.tags?.firstOrNull { it.size > 1 && it[0] == "url" }?.get(1) - val remoteMimeType = - uploadingResult.tags?.firstOrNull { it.size > 1 && it[0] == "m" }?.get(1)?.ifBlank { null } - val dim = - uploadingResult.tags?.firstOrNull { it.size > 1 && it[0] == "dim" }?.get(1)?.ifBlank { null } - - if (imageUrl.isNullOrBlank()) { - Log.e("ImageDownload", "Couldn't download image from server") - cancel() - isUploadingImage = false - viewModelScope.launch { imageUploadingError.emit("Server failed to return a url") } - return - } - - FileHeader.prepare( - fileUrl = imageUrl, - mimeType = remoteMimeType ?: localContentType, - dimPrecomputed = dim, - onReady = { header: FileHeader -> - isUploadingImage = false - val contentWarning = if (sensitiveContent) "" else null - message = - TextFieldValue( - message.text + - "\n" + - addInlineMetadataAsNIP54( - imageUrl, - header.dim, - header.mimeType, - alt, - header.blurHash, - header.hash, - contentWarning, - ), - ) - urlPreview = findUrlInMessage() - }, - onError = { - isUploadingImage = false - viewModelScope.launch { imageUploadingError.emit("Failed to upload the image / video") } - }, - ) - } - - fun addInlineMetadataAsNIP54( - imageUrl: String, - dim: String?, - m: String?, - alt: String?, - blurHash: String?, - x: String?, - sensitiveContent: String?, - ): String { - val extension = - listOfNotNull( - m?.ifBlank { null }?.let { "m=${URLEncoder.encode(it, "utf-8")}" }, - dim?.ifBlank { null }?.let { "dim=${URLEncoder.encode(it, "utf-8")}" }, - alt?.ifBlank { null }?.let { "alt=${URLEncoder.encode(it, "utf-8")}" }, - blurHash?.ifBlank { null }?.let { "blurhash=${URLEncoder.encode(it, "utf-8")}" }, - x?.ifBlank { null }?.let { "x=${URLEncoder.encode(it, "utf-8")}" }, - sensitiveContent?.let { "content-warning=${URLEncoder.encode(it, "utf-8")}" }, - ) - .joinToString("&") - - return if (imageUrl.contains("#")) { - "$imageUrl&$extension" - } else { - "$imageUrl#$extension" - } - } - fun createNIP95Record( bytes: ByteArray, mimeType: String?, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 8bd63e6f9..05586e242 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -103,6 +103,7 @@ import com.vitorpamplona.amethyst.ui.theme.markdownStyle import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.amethyst.ui.uriToRoute import com.vitorpamplona.quartz.encoders.Nip19 +import com.vitorpamplona.quartz.events.EmptyTagList import com.vitorpamplona.quartz.events.ImmutableListOfLists import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -184,7 +185,7 @@ private fun RenderRegular( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - val state by remember(content) { mutableStateOf(CachedRichTextParser.parseText(content, tags)) } + val state by remember(content, tags) { mutableStateOf(CachedRichTextParser.parseText(content, tags)) } val currentTextStyle = LocalTextStyle.current val currentTextColor = LocalContentColor.current @@ -416,8 +417,8 @@ private fun RenderContentAsMarkdown( onMediaCompose = { title, destination -> ZoomableContentView( content = - remember(destination) { - RichTextParser().parseMediaUrl(destination) ?: ZoomableUrlImage(url = destination) + remember(destination, tags) { + RichTextParser().parseMediaUrl(destination, tags ?: EmptyTagList) ?: ZoomableUrlImage(url = destination) }, roundedCorner = true, accountViewModel = accountViewModel, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index 842f65f86..08dca663f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -78,6 +78,7 @@ import com.vitorpamplona.amethyst.ui.theme.ChatPaddingInnerQuoteModifier import com.vitorpamplona.amethyst.ui.theme.ChatPaddingModifier import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.Font12SP +import com.vitorpamplona.amethyst.ui.theme.HalfTopPadding import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeightChat import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size15Modifier @@ -619,19 +620,18 @@ private fun RenderRegularTextNote( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - val tags = remember(note.event) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList } - val modifier = remember { Modifier.padding(top = 5.dp) } - LoadDecryptedContentOrNull(note = note, accountViewModel = accountViewModel) { eventContent -> if (eventContent != null) { SensitivityWarning( note = note, accountViewModel = accountViewModel, ) { + val tags = remember(note.event) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList } + TranslatableRichTextViewer( - content = eventContent!!, + content = eventContent, canPreview = canPreview, - modifier = modifier, + modifier = HalfTopPadding, tags = tags, backgroundColor = backgroundBubbleColor, accountViewModel = accountViewModel, @@ -642,8 +642,8 @@ private fun RenderRegularTextNote( TranslatableRichTextViewer( content = stringResource(id = R.string.could_not_decrypt_the_message), canPreview = true, - modifier = modifier, - tags = tags, + modifier = HalfTopPadding, + tags = EmptyTagList, backgroundColor = backgroundBubbleColor, accountViewModel = accountViewModel, nav = nav, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index a3b3d692d..82e2ef638 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -156,6 +156,7 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.events.EmptyTagList import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_LIVE import com.vitorpamplona.quartz.events.Participant +import com.vitorpamplona.quartz.events.findURLs import com.vitorpamplona.quartz.events.toImmutableListOfLists import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -300,6 +301,10 @@ fun ChannelScreen( dao = accountViewModel, ) tagger.run() + + val urls = findURLs(tagger.message) + val usedAttachments = newPostModel.nip94attachments.filter { it.urls().intersect(urls.toSet()).isNotEmpty() } + if (channel is PublicChatChannel) { accountViewModel.account.sendChannelMessage( message = tagger.message, @@ -307,6 +312,7 @@ fun ChannelScreen( replyTo = tagger.eTags, mentions = tagger.pTags, wantsToMarkAsSensitive = false, + nip94attachments = usedAttachments, ) } else if (channel is LiveActivitiesChannel) { accountViewModel.account.sendLiveMessage( @@ -315,6 +321,7 @@ fun ChannelScreen( replyTo = tagger.eTags, mentions = tagger.pTags, wantsToMarkAsSensitive = false, + nip94attachments = usedAttachments, ) } newPostModel.message = TextFieldValue("") diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt index d045b83f1..c205ff87c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt @@ -115,6 +115,7 @@ import com.vitorpamplona.amethyst.ui.theme.StdPadding import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.events.ChatMessageEvent import com.vitorpamplona.quartz.events.ChatroomKey +import com.vitorpamplona.quartz.events.findURLs import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.Dispatchers @@ -324,6 +325,9 @@ fun ChatroomScreen( // LAST ROW PrivateMessageEditFieldRow(newPostModel, isPrivate = true, accountViewModel) { scope.launch(Dispatchers.IO) { + val urls = findURLs(newPostModel.message.text) + val usedAttachments = newPostModel.nip94attachments.filter { it.urls().intersect(urls.toSet()).isNotEmpty() } + if (newPostModel.nip24 || room.users.size > 1 || replyTo.value?.event is ChatMessageEvent) { accountViewModel.account.sendNIP24PrivateMessage( message = newPostModel.message.text, @@ -331,6 +335,7 @@ fun ChatroomScreen( replyingTo = replyTo.value, mentions = null, wantsToMarkAsSensitive = false, + nip94attachments = usedAttachments, ) } else { accountViewModel.account.sendPrivateMessage( @@ -339,6 +344,7 @@ fun ChatroomScreen( replyingTo = replyTo.value, mentions = null, wantsToMarkAsSensitive = false, + nip94attachments = usedAttachments, ) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip29.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip29.kt new file mode 100644 index 000000000..6457ac843 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip29.kt @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2023 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.encoders + +import com.vitorpamplona.quartz.events.FileHeaderEvent + +class Nip29 { + companion object { + private const val IMETA = "imeta" + } + + fun convertFromFileHeader(header: FileHeaderEvent): Array? { + val myUrl = header.url() ?: return null + return createTag( + myUrl, + header.tags, + ) + } + + fun createTag( + imageUrl: String, + tags: Array>, + ): Array { + return arrayOf( + IMETA, + "url $imageUrl", + ) + + tags.mapNotNull { + if (it.isNotEmpty() && it[0] != "url") { + if (it.size > 1) { + "${it[0]} ${it[1]}" + } else { + "${it[0]}}" + } + } else { + null + } + } + } + + fun parse( + imageUrl: String, + tags: Array>, + ): Map { + return tags.firstOrNull { + it.size > 1 && it[0] == IMETA && it[1] == "url $imageUrl" + }?.let { tagList -> + tagList.associate { tag -> + val parts = tag.split(" ", limit = 2) + when (parts.size) { + 2 -> parts[0] to parts[1] + 1 -> parts[0] to "" + else -> "" to "" + } + } + } ?: emptyMap() + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip44UrlParser.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip54.kt similarity index 65% rename from app/src/main/java/com/vitorpamplona/amethyst/service/Nip44UrlParser.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip54.kt index d203c9d8d..1994cf71a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip44UrlParser.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip54.kt @@ -18,13 +18,47 @@ * 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 +package com.vitorpamplona.quartz.encoders +import com.vitorpamplona.quartz.events.FileHeaderEvent import java.net.URI import java.net.URLDecoder +import java.net.URLEncoder import kotlin.coroutines.cancellation.CancellationException -class Nip44UrlParser { +class Nip54 { + fun convertFromFileHeader(header: FileHeaderEvent): String? { + val myUrl = header.url() ?: return null + return createUrl( + myUrl, + header.tags, + ) + } + + fun createUrl( + imageUrl: String, + tags: Array>, + ): String { + val extension = + tags.mapNotNull { + if (it.isNotEmpty() && it[0] != "url") { + if (it.size > 1) { + "${it[0]}=${URLEncoder.encode(it[1], "utf-8")}" + } else { + "${it[0]}}=" + } + } else { + null + } + }.joinToString("&") + + return if (imageUrl.contains("#")) { + "$imageUrl&$extension" + } else { + "$imageUrl#$extension" + } + } + fun parse(url: String): Map { return try { fragments(URI(url)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMessageEvent.kt index 265709839..ce9de89e0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMessageEvent.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.events import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.encoders.Nip29 import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.utils.TimeUtils @@ -58,7 +59,7 @@ class ChannelMessageEvent( markAsSensitive: Boolean, zapRaiserAmount: Long?, geohash: String? = null, - nip94attachments: List? = null, + nip94attachments: List? = null, onReady: (ChannelMessageEvent) -> Unit, ) { val tags = @@ -77,7 +78,9 @@ class ChannelMessageEvent( geohash?.let { tags.addAll(geohashMipMap(it)) } nip94attachments?.let { it.forEach { - // tags.add(arrayOf("nip94", it.toJson())) + Nip29().convertFromFileHeader(it)?.let { + tags.add(it) + } } } tags.add( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt index 77a62b9b6..e17753029 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.events import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.encoders.Nip29 import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableSet @@ -80,6 +81,7 @@ class ChatMessageEvent( geohash: String? = null, signer: NostrSigner, createdAt: Long = TimeUtils.now(), + nip94attachments: List? = null, onReady: (ChatMessageEvent) -> Unit, ) { val tags = mutableListOf>() @@ -95,6 +97,13 @@ class ChatMessageEvent( zapRaiserAmount?.let { tags.add(arrayOf("zapraiser", "$it")) } geohash?.let { tags.addAll(geohashMipMap(it)) } subject?.let { tags.add(arrayOf("subject", it)) } + nip94attachments?.let { + it.forEach { + Nip29().convertFromFileHeader(it)?.let { + tags.add(it) + } + } + } // tags.add(arrayOf("alt", alt)) signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/FileHeaderEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/FileHeaderEvent.kt index 0c7a8fa0b..1d760fe14 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/FileHeaderEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/FileHeaderEvent.kt @@ -62,17 +62,17 @@ class FileHeaderEvent( const val KIND = 1063 const val ALT_DESCRIPTION = "Verifiable file url" - private const val URL = "url" - private const val ENCRYPTION_KEY = "aes-256-gcm" - private const val MIME_TYPE = "m" - private const val FILE_SIZE = "size" - private const val DIMENSION = "dim" - private const val HASH = "x" - private const val MAGNET_URI = "magnet" - private const val TORRENT_INFOHASH = "i" - private const val BLUR_HASH = "blurhash" - private const val ORIGINAL_HASH = "ox" - private const val ALT = "alt" + const val URL = "url" + const val ENCRYPTION_KEY = "aes-256-gcm" + const val MIME_TYPE = "m" + const val FILE_SIZE = "size" + const val DIMENSION = "dim" + const val HASH = "x" + const val MAGNET_URI = "magnet" + const val TORRENT_INFOHASH = "i" + const val BLUR_HASH = "blurhash" + const val ORIGINAL_HASH = "ox" + const val ALT = "alt" fun create( url: String, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesChatMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesChatMessageEvent.kt index d2349f15a..efeb2441e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesChatMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesChatMessageEvent.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.events import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.encoders.Nip29 import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.utils.TimeUtils @@ -70,7 +71,7 @@ class LiveActivitiesChatMessageEvent( markAsSensitive: Boolean, zapRaiserAmount: Long?, geohash: String? = null, - nip94attachments: List? = null, + nip94attachments: List? = null, onReady: (LiveActivitiesChatMessageEvent) -> Unit, ) { val content = message @@ -90,7 +91,9 @@ class LiveActivitiesChatMessageEvent( geohash?.let { tags.addAll(geohashMipMap(it)) } nip94attachments?.let { it.forEach { - // tags.add(arrayOf("nip94", it.toJson())) + Nip29().convertFromFileHeader(it)?.let { + tags.add(it) + } } } tags.add(arrayOf("alt", ALT)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP24Factory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP24Factory.kt index 4fddd1925..a99a3e2d6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP24Factory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP24Factory.kt @@ -77,6 +77,7 @@ class NIP24Factory { markAsSensitive: Boolean = false, zapRaiserAmount: Long? = null, geohash: String? = null, + nip94attachments: List? = null, onReady: (Result) -> Unit, ) { val senderPublicKey = signer.pubKey @@ -92,6 +93,7 @@ class NIP24Factory { markAsSensitive = markAsSensitive, zapRaiserAmount = zapRaiserAmount, geohash = geohash, + nip94attachments = nip94attachments, ) { senderMessage -> createWraps(senderMessage, to.plus(senderPublicKey).toSet(), signer) { wraps -> onReady( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/PollNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/PollNoteEvent.kt index 3d9a5dc57..13d90d82a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/PollNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/PollNoteEvent.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.events import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.encoders.Nip29 import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.utils.TimeUtils @@ -78,7 +79,7 @@ class PollNoteEvent( markAsSensitive: Boolean, zapRaiserAmount: Long?, geohash: String? = null, - nip94attachments: List? = null, + nip94attachments: List? = null, onReady: (PollNoteEvent) -> Unit, ) { val tags = mutableListOf>() @@ -104,7 +105,9 @@ class PollNoteEvent( geohash?.let { tags.addAll(geohashMipMap(it)) } nip94attachments?.let { it.forEach { - // tags.add(arrayOf("nip94", it.toJson())) + Nip29().convertFromFileHeader(it)?.let { + tags.add(it) + } } } tags.add(arrayOf("alt", ALT)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/PrivateDmEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/PrivateDmEvent.kt index d5475ca39..dd1d619cb 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/PrivateDmEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/PrivateDmEvent.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.encoders.Hex import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexValidator +import com.vitorpamplona.quartz.encoders.Nip54 import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.persistentSetOf @@ -122,14 +123,24 @@ class PrivateDmEvent( markAsSensitive: Boolean, zapRaiserAmount: Long?, geohash: String? = null, + nip94attachments: List? = null, onReady: (PrivateDmEvent) -> Unit, ) { - val message = + var message = msg + nip94attachments?.forEach { + val myUrl = it.url() + if (myUrl != null) { + message = message.replace(myUrl, Nip54().createUrl(myUrl, it.tags)) + } + } + + message = if (advertiseNip18) { - NIP_18_ADVERTISEMENT + NIP_18_ADVERTISEMENT + message } else { - "" - } + msg + message + } + val tags = mutableListOf>() publishedRecipientPubKey?.let { tags.add(arrayOf("p", publishedRecipientPubKey)) } replyTos?.forEach { tags.add(arrayOf("e", it)) } @@ -142,6 +153,15 @@ class PrivateDmEvent( } zapRaiserAmount?.let { tags.add(arrayOf("zapraiser", "$it")) } geohash?.let { tags.addAll(geohashMipMap(it)) } + /* Privacy issue: DO NOT ADD THESE TO THE TAGS. + nip94attachments?.let { + it.forEach { + Nip29().convertFromFileHeader(it)?.let { + tags.add(it) + } + } + }*/ + tags.add(arrayOf("alt", ALT)) signer.nip04Encrypt(message, recipientPubKey) { content -> diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt index 7abb7b242..ac97f781a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt @@ -25,6 +25,7 @@ import com.linkedin.urls.detection.UrlDetector import com.linkedin.urls.detection.UrlDetectorOptions import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.encoders.Nip29 import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.utils.TimeUtils @@ -55,7 +56,7 @@ class TextNoteEvent( root: String?, directMentions: Set, geohash: String? = null, - nip94attachments: List? = null, + nip94attachments: List? = null, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (TextNoteEvent) -> Unit, @@ -106,7 +107,9 @@ class TextNoteEvent( geohash?.let { tags.addAll(geohashMipMap(it)) } nip94attachments?.let { it.forEach { - // tags.add(arrayOf("nip94", it.toJson())) + Nip29().convertFromFileHeader(it)?.let { + tags.add(it) + } } } From 316c32f076b0351ad7d3b1e03bd35d1947260609 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 1 Feb 2024 18:38:25 -0500 Subject: [PATCH 028/176] v0.83.9 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 8a41b62d7..11868fee2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 34 - versionCode 349 - versionName "0.83.8" + versionCode 350 + versionName "0.83.9" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From d3d19b497b52d5bafc8dae0dcf57afed6ea7b62d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 1 Feb 2024 19:20:26 -0500 Subject: [PATCH 029/176] Renames from NIP-29 to 92 --- README.md | 2 +- .../vitorpamplona/amethyst/service/CachedRichTextParser.kt | 6 +++--- .../vitorpamplona/quartz/encoders/{Nip29.kt => Nip92.kt} | 2 +- .../com/vitorpamplona/quartz/events/ChannelMessageEvent.kt | 4 ++-- .../com/vitorpamplona/quartz/events/ChatMessageEvent.kt | 4 ++-- .../quartz/events/LiveActivitiesChatMessageEvent.kt | 4 ++-- .../java/com/vitorpamplona/quartz/events/PollNoteEvent.kt | 4 ++-- .../java/com/vitorpamplona/quartz/events/PrivateDmEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/TextNoteEvent.kt | 4 ++-- 9 files changed, 16 insertions(+), 16 deletions(-) rename quartz/src/main/java/com/vitorpamplona/quartz/encoders/{Nip29.kt => Nip92.kt} (99%) diff --git a/README.md b/README.md index abab32af1..83bfc49a8 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,6 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases) - [ ] Delegated Event Signing (NIP-26, Will not implement) - [x] Text Note References (NIP-27) - [x] Public Chats (NIP-28) -- [x] Inline Metadata (NIP-29) - [x] Custom Emoji (NIP-30) - [x] Event kind summaries (NIP-31) - [ ] Labeling (NIP-32) @@ -82,6 +81,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases) - [x] Highlights (NIP-84) - [x] Recommended Application Handlers (NIP-89) - [ ] Data Vending Machine (NIP-90) +- [x] Inline Metadata (NIP-92) - [x] Verifiable file URLs (NIP-94) - [x] Binary Blobs (NIP-95) - [x] HTTP File Storage Integration (NIP-96 Draft) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt index e0f944138..70816cd69 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt @@ -34,8 +34,8 @@ import com.vitorpamplona.amethyst.ui.components.imageExtensions import com.vitorpamplona.amethyst.ui.components.removeQueryParamsForExtensionComparison import com.vitorpamplona.amethyst.ui.components.tagIndex import com.vitorpamplona.amethyst.ui.components.videoExtensions -import com.vitorpamplona.quartz.encoders.Nip29 import com.vitorpamplona.quartz.encoders.Nip54 +import com.vitorpamplona.quartz.encoders.Nip92 import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.ImmutableListOfLists import kotlinx.collections.immutable.ImmutableList @@ -104,7 +104,7 @@ class RichTextParser() { val removedParamsFromUrl = removeQueryParamsForExtensionComparison(fullUrl) return if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) { val frags = Nip54().parse(fullUrl) - val tags = Nip29().parse(fullUrl, tags.lists) + val tags = Nip92().parse(fullUrl, tags.lists) ZoomableUrlImage( url = fullUrl, @@ -116,7 +116,7 @@ class RichTextParser() { ) } else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) { val frags = Nip54().parse(fullUrl) - val tags = Nip29().parse(fullUrl, tags.lists) + val tags = Nip92().parse(fullUrl, tags.lists) ZoomableUrlVideo( url = fullUrl, description = frags[FileHeaderEvent.ALT] ?: tags[FileHeaderEvent.ALT], diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip29.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip92.kt similarity index 99% rename from quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip29.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip92.kt index 6457ac843..cd8191348 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip29.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip92.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.encoders import com.vitorpamplona.quartz.events.FileHeaderEvent -class Nip29 { +class Nip92 { companion object { private const val IMETA = "imeta" } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMessageEvent.kt index ce9de89e0..31d690b38 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMessageEvent.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.events import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.encoders.HexKey -import com.vitorpamplona.quartz.encoders.Nip29 +import com.vitorpamplona.quartz.encoders.Nip92 import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.utils.TimeUtils @@ -78,7 +78,7 @@ class ChannelMessageEvent( geohash?.let { tags.addAll(geohashMipMap(it)) } nip94attachments?.let { it.forEach { - Nip29().convertFromFileHeader(it)?.let { + Nip92().convertFromFileHeader(it)?.let { tags.add(it) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt index e17753029..d07c40d05 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.quartz.events import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import com.vitorpamplona.quartz.encoders.HexKey -import com.vitorpamplona.quartz.encoders.Nip29 +import com.vitorpamplona.quartz.encoders.Nip92 import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableSet @@ -99,7 +99,7 @@ class ChatMessageEvent( subject?.let { tags.add(arrayOf("subject", it)) } nip94attachments?.let { it.forEach { - Nip29().convertFromFileHeader(it)?.let { + Nip92().convertFromFileHeader(it)?.let { tags.add(it) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesChatMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesChatMessageEvent.kt index efeb2441e..d840d57bf 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesChatMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesChatMessageEvent.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.quartz.events import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey -import com.vitorpamplona.quartz.encoders.Nip29 +import com.vitorpamplona.quartz.encoders.Nip92 import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.utils.TimeUtils @@ -91,7 +91,7 @@ class LiveActivitiesChatMessageEvent( geohash?.let { tags.addAll(geohashMipMap(it)) } nip94attachments?.let { it.forEach { - Nip29().convertFromFileHeader(it)?.let { + Nip92().convertFromFileHeader(it)?.let { tags.add(it) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/PollNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/PollNoteEvent.kt index 13d90d82a..fbf512fdf 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/PollNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/PollNoteEvent.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.quartz.events import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey -import com.vitorpamplona.quartz.encoders.Nip29 +import com.vitorpamplona.quartz.encoders.Nip92 import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.utils.TimeUtils @@ -105,7 +105,7 @@ class PollNoteEvent( geohash?.let { tags.addAll(geohashMipMap(it)) } nip94attachments?.let { it.forEach { - Nip29().convertFromFileHeader(it)?.let { + Nip92().convertFromFileHeader(it)?.let { tags.add(it) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/PrivateDmEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/PrivateDmEvent.kt index dd1d619cb..5929a6e5d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/PrivateDmEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/PrivateDmEvent.kt @@ -156,7 +156,7 @@ class PrivateDmEvent( /* Privacy issue: DO NOT ADD THESE TO THE TAGS. nip94attachments?.let { it.forEach { - Nip29().convertFromFileHeader(it)?.let { + Nip92().convertFromFileHeader(it)?.let { tags.add(it) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt index ac97f781a..91104aaa8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt @@ -25,7 +25,7 @@ import com.linkedin.urls.detection.UrlDetector import com.linkedin.urls.detection.UrlDetectorOptions import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey -import com.vitorpamplona.quartz.encoders.Nip29 +import com.vitorpamplona.quartz.encoders.Nip92 import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.utils.TimeUtils @@ -107,7 +107,7 @@ class TextNoteEvent( geohash?.let { tags.addAll(geohashMipMap(it)) } nip94attachments?.let { it.forEach { - Nip29().convertFromFileHeader(it)?.let { + Nip92().convertFromFileHeader(it)?.let { tags.add(it) } } From 9d92976eb9b0b51d7145e4e91757f1852c69c641 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 2 Feb 2024 09:30:36 -0500 Subject: [PATCH 030/176] Optimizes memory use of the visual user and url tagger in new posts. --- .../com/vitorpamplona/amethyst/model/Note.kt | 2 +- .../ui/actions/UrlUserTagTransformation.kt | 18 +++++------------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 4fae595d6..560deafa2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -615,7 +615,7 @@ open class Note(val idHex: String) { // Regular Zap Receipts zaps.forEach { - val noteEvent = it?.value?.event + val noteEvent = it.value?.event if (noteEvent is LnZapEvent) { sumOfAmounts += noteEvent.amount ?: BigDecimal.ZERO } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagTransformation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagTransformation.kt index f239f4af0..d44f1f6a2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagTransformation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagTransformation.kt @@ -56,11 +56,9 @@ fun buildAnnotatedStringWithUrlHighlighting( val builderAfter = StringBuilder() // important to correctly measure Tag start and end append( text - .split('\n') - .map { paragraph: String -> + .split('\n').joinToString("\n") { paragraph: String -> paragraph - .split(' ') - .map { word: String -> + .split(' ').joinToString(" ") { word: String -> try { if (word.startsWith("@npub") && word.length >= 64) { val keyB32 = word.substring(0, 64) @@ -121,9 +119,7 @@ fun buildAnnotatedStringWithUrlHighlighting( word } } - .joinToString(" ") - } - .joinToString("\n"), + }, ) substitutions.forEach { @@ -143,9 +139,7 @@ fun buildAnnotatedStringWithUrlHighlighting( object : OffsetMapping { override fun originalToTransformed(offset: Int): Int { val inInsideRange = - substitutions - .filter { offset > it.original.start && offset < it.original.end } - .firstOrNull() + substitutions.firstOrNull { offset > it.original.start && offset < it.original.end } if (inInsideRange != null) { val percentInRange = @@ -165,9 +159,7 @@ fun buildAnnotatedStringWithUrlHighlighting( override fun transformedToOriginal(offset: Int): Int { val inInsideRange = - substitutions - .filter { offset > it.modified.start && offset < it.modified.end } - .firstOrNull() + substitutions.firstOrNull { offset > it.modified.start && offset < it.modified.end } if (inInsideRange != null) { val percentInRange = From 103809d35b33192175f3e945043b293f05766722 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 2 Feb 2024 09:35:17 -0500 Subject: [PATCH 031/176] Fixes the top padding of the quoted note in a post. --- .../com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 05586e242..27fa55775 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -100,7 +100,6 @@ import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding import com.vitorpamplona.amethyst.ui.theme.MarkdownTextStyle import com.vitorpamplona.amethyst.ui.theme.innerPostModifier import com.vitorpamplona.amethyst.ui.theme.markdownStyle -import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.amethyst.ui.uriToRoute import com.vitorpamplona.quartz.encoders.Nip19 import com.vitorpamplona.quartz.events.EmptyTagList @@ -607,7 +606,7 @@ private fun DisplayFullNote( NoteCompose( baseNote = it, accountViewModel = accountViewModel, - modifier = MaterialTheme.colorScheme.replyModifier, + modifier = MaterialTheme.colorScheme.innerPostModifier, parentBackgroundColor = backgroundColor, isQuotedNote = true, nav = nav, From 48887f23c1be17d9663348a0996742c7f30cf44b Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 2 Feb 2024 14:37:01 +0000 Subject: [PATCH 032/176] New Crowdin translations by GitHub Action --- app/src/main/res/values-en-rGB/strings.xml | 9 ++++++++- app/src/main/res/values-fr/strings.xml | 7 +++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml index 66a570486..d02ba1edb 100644 --- a/app/src/main/res/values-en-rGB/strings.xml +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -1,2 +1,9 @@ - + + Point to the QR Code + Show QR + Scan QR + Show Anyway + Post was muted or reported by + Event is loading or can\'t be found in your relay list + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 62cc6b369..a7aa7fb4b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -90,6 +90,7 @@ Ajouter un relais Nom visible du public Mon nom visible du public + Bienvenue Ostrich! Nom d\'utilisateur Mon nom d\'utilisateur Biographie @@ -393,6 +394,8 @@ Paiement Jeton Cashu Échanger + Envoyer vers le portefeuille Zap + Ouvrir dans le portefeuille Cashu Copier le Jeton Adresse Lightning non définie Jeton copié dans le presse-papiers @@ -513,9 +516,13 @@ Impossible de payer la facture Impossible de retirer Impossible de configurer Wallet Connect + Impossible d\'utiliser ce Cashu + Les jetons de cashu sont déjà dépensés. Cashu reçu %1$s sats ont été envoyés à votre portefeuille. (Frais : %2$s sats) + Aucun portefeuille Cashu compatible trouvé dans le système Impossible de récupérer la facture depuis les serveurs du destinataire + Le fournisseur de connexion de votre portefeuille a retourné l\'erreur suivante : %1$s Impossible de se connecter à Tor Téléchargement du document relais indisponible Impossible d\'assembler LNUrl à partir de l\'adresse Lightning \"%1$s\". Vérifiez la configuration de l\'utilisateur From c78239a6b7b03ccdacdfd14dcbf4471bee7b1922 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 2 Feb 2024 16:08:36 -0500 Subject: [PATCH 033/176] Fixes some connectivity issues by basing the timeout from the last connection tentative instead of the drop out time. --- .../vitorpamplona/amethyst/service/relays/Relay.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt index fed6828b6..5a8e02523 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt @@ -77,7 +77,7 @@ class Relay( var errorCounter = 0 var pingInMs: Long? = null - var closingTimeInSeconds = 0L + var lastConnectTentative: Long = 0L var afterEOSEPerSubscription = mutableMapOf() @@ -120,6 +120,8 @@ class Relay( if (socket != null) return + lastConnectTentative = TimeUtils.now() + try { val request = Request.Builder() @@ -251,7 +253,6 @@ class Relay( this.isReady = false this.usingCompression = false this.resetEOSEStatuses() - this.closingTimeInSeconds = TimeUtils.now() } fun processNewRelayMessage(newMessage: String) { @@ -331,7 +332,7 @@ class Relay( Log.d("Relay", "Relay.disconnect $url") checkNotInMainThread() - closingTimeInSeconds = TimeUtils.now() + lastConnectTentative = 0L // this is not an error, so prepare to reconnect as soon as requested. socket?.cancel() socket = null isReady = false @@ -373,7 +374,7 @@ class Relay( } } else { // waits 60 seconds to reconnect after disconnected. - if (TimeUtils.now() > closingTimeInSeconds + RECONNECTING_IN_SECONDS) { + if (TimeUtils.now() > lastConnectTentative + RECONNECTING_IN_SECONDS) { // sends all filters after connection is successful. connect() } @@ -413,7 +414,7 @@ class Relay( if (socket == null) { // waits 60 seconds to reconnect after disconnected. - if (TimeUtils.now() > closingTimeInSeconds + RECONNECTING_IN_SECONDS) { + if (TimeUtils.now() > lastConnectTentative + RECONNECTING_IN_SECONDS) { // println("sendfilter Only if Disconnected ${url} ") connect() } From 3a2045eccc0f192ccec22b2d17a96d125047dc55 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 2 Feb 2024 17:39:48 -0500 Subject: [PATCH 034/176] Allows buttons in the New Post screen to scroll horizontally to partially fix https://github.com/vitorpamplona/amethyst/issues/723 We are waiting the default scrollbar implementation from Jetpack Compose --- .../amethyst/ui/actions/NewPostView.kt | 350 ++++++++++++------ 1 file changed, 242 insertions(+), 108 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index 2429a2990..7a9e7e45e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -30,6 +30,7 @@ import android.widget.Toast import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.border +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -304,25 +305,38 @@ fun NewPostView( ) { pad -> Surface( modifier = - Modifier.padding( - start = Size10dp, - top = pad.calculateTopPadding(), - end = Size10dp, - bottom = pad.calculateBottomPadding(), - ) + Modifier + .padding( + start = Size10dp, + top = pad.calculateTopPadding(), + end = Size10dp, + bottom = pad.calculateBottomPadding(), + ) .fillMaxSize(), ) { Column( - modifier = Modifier.fillMaxWidth().fillMaxHeight(), + modifier = + Modifier + .fillMaxWidth() + .fillMaxHeight(), ) { Column( - modifier = Modifier.imePadding().weight(1f), + modifier = + Modifier + .imePadding() + .weight(1f), ) { Row( - modifier = Modifier.fillMaxWidth().weight(1f), + modifier = + Modifier + .fillMaxWidth() + .weight(1f), ) { Column( - modifier = Modifier.fillMaxWidth().verticalScroll(scrollState), + modifier = + Modifier + .fillMaxWidth() + .verticalScroll(scrollState), ) { postViewModel.originalNote?.let { Row(Modifier.heightIn(max = 200.dp)) { @@ -474,7 +488,8 @@ fun NewPostView( contentDescription = myUrlPreview, contentScale = ContentScale.FillWidth, modifier = - Modifier.padding(top = 4.dp) + Modifier + .padding(top = 4.dp) .fillMaxWidth() .clip(shape = QuoteBorder) .border( @@ -529,61 +544,7 @@ fun NewPostView( } } - Row( - modifier = Modifier.fillMaxWidth().height(50.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - UploadFromGallery( - isUploading = postViewModel.isUploadingImage, - tint = MaterialTheme.colorScheme.onBackground, - modifier = Modifier, - ) { - postViewModel.selectImage(it) - } - - if (postViewModel.canUsePoll) { - // These should be hashtag recommendations the user selects in the future. - // val hashtag = stringResource(R.string.poll_hashtag) - // postViewModel.includePollHashtagInMessage(postViewModel.wantsPoll, hashtag) - AddPollButton(postViewModel.wantsPoll) { - postViewModel.wantsPoll = !postViewModel.wantsPoll - if (postViewModel.wantsPoll) { - postViewModel.wantsProduct = false - } - } - } - - AddClassifiedsButton(postViewModel) { - postViewModel.wantsProduct = !postViewModel.wantsProduct - if (postViewModel.wantsProduct) { - postViewModel.wantsPoll = false - } - } - - if (postViewModel.canAddInvoice) { - AddLnInvoiceButton(postViewModel.wantsInvoice) { - postViewModel.wantsInvoice = !postViewModel.wantsInvoice - } - } - - if (postViewModel.canAddZapRaiser) { - AddZapraiserButton(postViewModel.wantsZapraiser) { - postViewModel.wantsZapraiser = !postViewModel.wantsZapraiser - } - } - - MarkAsSensitive(postViewModel) { - postViewModel.wantsToMarkAsSensitive = !postViewModel.wantsToMarkAsSensitive - } - - AddGeoHash(postViewModel) { - postViewModel.wantsToAddGeoHash = !postViewModel.wantsToAddGeoHash - } - - ForwardZapTo(postViewModel) { - postViewModel.wantsForwardZapTo = !postViewModel.wantsForwardZapTo - } - } + BottomRowActions(postViewModel) } } } @@ -591,6 +552,71 @@ fun NewPostView( } } +@Composable +private fun BottomRowActions(postViewModel: NewPostViewModel) { + val scrollState = rememberScrollState() + + Row( + modifier = + Modifier + .horizontalScroll(scrollState) + .fillMaxWidth() + .height(50.dp), + verticalAlignment = CenterVertically, + ) { + UploadFromGallery( + isUploading = postViewModel.isUploadingImage, + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier, + ) { + postViewModel.selectImage(it) + } + + if (postViewModel.canUsePoll) { + // These should be hashtag recommendations the user selects in the future. + // val hashtag = stringResource(R.string.poll_hashtag) + // postViewModel.includePollHashtagInMessage(postViewModel.wantsPoll, hashtag) + AddPollButton(postViewModel.wantsPoll) { + postViewModel.wantsPoll = !postViewModel.wantsPoll + if (postViewModel.wantsPoll) { + postViewModel.wantsProduct = false + } + } + } + + AddClassifiedsButton(postViewModel) { + postViewModel.wantsProduct = !postViewModel.wantsProduct + if (postViewModel.wantsProduct) { + postViewModel.wantsPoll = false + } + } + + if (postViewModel.canAddInvoice) { + AddLnInvoiceButton(postViewModel.wantsInvoice) { + postViewModel.wantsInvoice = !postViewModel.wantsInvoice + } + } + + if (postViewModel.canAddZapRaiser) { + AddZapraiserButton(postViewModel.wantsZapraiser) { + postViewModel.wantsZapraiser = !postViewModel.wantsZapraiser + } + } + + MarkAsSensitive(postViewModel) { + postViewModel.wantsToMarkAsSensitive = !postViewModel.wantsToMarkAsSensitive + } + + AddGeoHash(postViewModel) { + postViewModel.wantsToAddGeoHash = !postViewModel.wantsToAddGeoHash + } + + ForwardZapTo(postViewModel) { + postViewModel.wantsForwardZapTo = !postViewModel.wantsForwardZapTo + } + } +} + @Composable private fun PollField(postViewModel: NewPostViewModel) { Column( @@ -644,7 +670,8 @@ private fun MessageField(postViewModel: NewPostViewModel) { capitalization = KeyboardCapitalization.Sentences, ), modifier = - Modifier.fillMaxWidth() + Modifier + .fillMaxWidth() .border( width = 1.dp, color = MaterialTheme.colorScheme.surface, @@ -684,21 +711,32 @@ fun ContentSensitivityExplainer(postViewModel: NewPostViewModel) { ) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth().padding(bottom = 10.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(bottom = 10.dp), ) { Box( - Modifier.height(20.dp).width(25.dp), + Modifier + .height(20.dp) + .width(25.dp), ) { Icon( imageVector = Icons.Default.VisibilityOff, contentDescription = stringResource(id = R.string.content_warning), - modifier = Modifier.size(18.dp).align(Alignment.BottomStart), + modifier = + Modifier + .size(18.dp) + .align(Alignment.BottomStart), tint = Color.Red, ) Icon( imageVector = Icons.Rounded.Warning, contentDescription = stringResource(id = R.string.content_warning), - modifier = Modifier.size(10.dp).align(Alignment.TopEnd), + modifier = + Modifier + .size(10.dp) + .align(Alignment.TopEnd), tint = Color.Yellow, ) } @@ -921,7 +959,10 @@ fun SellProduct(postViewModel: NewPostViewModel) { placeholder = conditionTypes.filter { it.first == postViewModel.condition }.first().second, options = conditionOptions, onSelect = { postViewModel.condition = conditionTypes[it].first }, - modifier = Modifier.weight(1f).padding(end = 5.dp, bottom = 1.dp), + modifier = + Modifier + .weight(1f) + .padding(end = 5.dp, bottom = 1.dp), ) { currentOption, modifier -> MyTextField( value = TextFieldValue(currentOption), @@ -982,7 +1023,10 @@ fun SellProduct(postViewModel: NewPostViewModel) { ?: "", options = categoryOptions, onSelect = { postViewModel.category = TextFieldValue(categoryTypes[it].second) }, - modifier = Modifier.weight(1f).padding(end = 5.dp, bottom = 1.dp), + modifier = + Modifier + .weight(1f) + .padding(end = 5.dp, bottom = 1.dp), ) { currentOption, modifier -> MyTextField( value = TextFieldValue(currentOption), @@ -1047,21 +1091,32 @@ fun FowardZapTo( ) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth().padding(bottom = 10.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(bottom = 10.dp), ) { Box( - Modifier.height(20.dp).width(25.dp), + Modifier + .height(20.dp) + .width(25.dp), ) { Icon( imageVector = Icons.Outlined.Bolt, contentDescription = stringResource(id = R.string.zaps), - modifier = Modifier.size(20.dp).align(Alignment.CenterStart), + modifier = + Modifier + .size(20.dp) + .align(Alignment.CenterStart), tint = BitcoinOrange, ) Icon( imageVector = Icons.Outlined.ArrowForwardIos, contentDescription = stringResource(id = R.string.zaps), - modifier = Modifier.size(13.dp).align(Alignment.CenterEnd), + modifier = + Modifier + .size(13.dp) + .align(Alignment.CenterEnd), tint = BitcoinOrange, ) } @@ -1161,10 +1216,15 @@ fun LocationAsHash(postViewModel: NewPostViewModel) { ) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth().padding(bottom = 10.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(bottom = 10.dp), ) { Box( - Modifier.height(20.dp).width(20.dp), + Modifier + .height(20.dp) + .width(20.dp), ) { Icon( imageVector = Icons.Default.LocationOn, @@ -1305,32 +1365,46 @@ private fun AddZapraiserButton( onClick = { onClick() }, ) { Box( - Modifier.height(20.dp).width(25.dp), + Modifier + .height(20.dp) + .width(25.dp), ) { if (!isLnInvoiceActive) { Icon( imageVector = Icons.Default.ShowChart, null, - modifier = Modifier.size(20.dp).align(Alignment.TopStart), + modifier = + Modifier + .size(20.dp) + .align(Alignment.TopStart), tint = MaterialTheme.colorScheme.onBackground, ) Icon( imageVector = Icons.Default.Bolt, contentDescription = stringResource(R.string.zaps), - modifier = Modifier.size(13.dp).align(Alignment.BottomEnd), + modifier = + Modifier + .size(13.dp) + .align(Alignment.BottomEnd), tint = MaterialTheme.colorScheme.onBackground, ) } else { Icon( imageVector = Icons.Default.ShowChart, null, - modifier = Modifier.size(20.dp).align(Alignment.TopStart), + modifier = + Modifier + .size(20.dp) + .align(Alignment.TopStart), tint = BitcoinOrange, ) Icon( imageVector = Icons.Default.Bolt, contentDescription = stringResource(R.string.zaps), - modifier = Modifier.size(13.dp).align(Alignment.BottomEnd), + modifier = + Modifier + .size(13.dp) + .align(Alignment.BottomEnd), tint = BitcoinOrange, ) } @@ -1399,32 +1473,46 @@ private fun ForwardZapTo( onClick = { onClick() }, ) { Box( - Modifier.height(20.dp).width(25.dp), + Modifier + .height(20.dp) + .width(25.dp), ) { if (!postViewModel.wantsForwardZapTo) { Icon( imageVector = Icons.Default.Bolt, contentDescription = stringResource(R.string.zaps), - modifier = Modifier.size(20.dp).align(Alignment.CenterStart), + modifier = + Modifier + .size(20.dp) + .align(Alignment.CenterStart), tint = MaterialTheme.colorScheme.onBackground, ) Icon( imageVector = Icons.Default.ArrowForwardIos, contentDescription = stringResource(R.string.zaps), - modifier = Modifier.size(13.dp).align(Alignment.CenterEnd), + modifier = + Modifier + .size(13.dp) + .align(Alignment.CenterEnd), tint = MaterialTheme.colorScheme.onBackground, ) } else { Icon( imageVector = Icons.Outlined.Bolt, contentDescription = stringResource(id = R.string.zaps), - modifier = Modifier.size(20.dp).align(Alignment.CenterStart), + modifier = + Modifier + .size(20.dp) + .align(Alignment.CenterStart), tint = BitcoinOrange, ) Icon( imageVector = Icons.Outlined.ArrowForwardIos, contentDescription = stringResource(id = R.string.zaps), - modifier = Modifier.size(13.dp).align(Alignment.CenterEnd), + modifier = + Modifier + .size(13.dp) + .align(Alignment.CenterEnd), tint = BitcoinOrange, ) } @@ -1467,32 +1555,46 @@ private fun MarkAsSensitive( onClick = { onClick() }, ) { Box( - Modifier.height(20.dp).width(23.dp), + Modifier + .height(20.dp) + .width(23.dp), ) { if (!postViewModel.wantsToMarkAsSensitive) { Icon( imageVector = Icons.Default.Visibility, contentDescription = stringResource(R.string.content_warning), - modifier = Modifier.size(18.dp).align(Alignment.BottomStart), + modifier = + Modifier + .size(18.dp) + .align(Alignment.BottomStart), tint = MaterialTheme.colorScheme.onBackground, ) Icon( imageVector = Icons.Rounded.Warning, contentDescription = stringResource(R.string.content_warning), - modifier = Modifier.size(10.dp).align(Alignment.TopEnd), + modifier = + Modifier + .size(10.dp) + .align(Alignment.TopEnd), tint = MaterialTheme.colorScheme.onBackground, ) } else { Icon( imageVector = Icons.Default.VisibilityOff, contentDescription = stringResource(id = R.string.content_warning), - modifier = Modifier.size(18.dp).align(Alignment.BottomStart), + modifier = + Modifier + .size(18.dp) + .align(Alignment.BottomStart), tint = Color.Red, ) Icon( imageVector = Icons.Rounded.Warning, contentDescription = stringResource(id = R.string.content_warning), - modifier = Modifier.size(10.dp).align(Alignment.TopEnd), + modifier = + Modifier + .size(10.dp) + .align(Alignment.TopEnd), tint = Color.Yellow, ) } @@ -1601,7 +1703,8 @@ fun ImageVideoDescription( Column( modifier = - Modifier.fillMaxWidth() + Modifier + .fillMaxWidth() .padding(start = 30.dp, end = 30.dp) .clip(shape = QuoteBorder) .border( @@ -1611,11 +1714,17 @@ fun ImageVideoDescription( ), ) { Column( - modifier = Modifier.fillMaxWidth().padding(30.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(30.dp), ) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth().padding(bottom = 10.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(bottom = 10.dp), ) { Text( text = @@ -1633,13 +1742,17 @@ fun ImageVideoDescription( fontSize = 20.sp, fontWeight = FontWeight.W500, modifier = - Modifier.padding(start = 10.dp) + Modifier + .padding(start = 10.dp) .weight(1.0f) .windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)), ) IconButton( - modifier = Modifier.size(30.dp).padding(end = 5.dp), + modifier = + Modifier + .size(30.dp) + .padding(end = 5.dp), onClick = onCancel, ) { CancelIcon() @@ -1651,7 +1764,8 @@ fun ImageVideoDescription( Row( verticalAlignment = Alignment.CenterVertically, modifier = - Modifier.fillMaxWidth() + Modifier + .fillMaxWidth() .padding(bottom = 10.dp) .windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)), ) { @@ -1661,7 +1775,8 @@ fun ImageVideoDescription( contentDescription = uri.toString(), contentScale = ContentScale.FillWidth, modifier = - Modifier.padding(top = 4.dp) + Modifier + .padding(top = 4.dp) .fillMaxWidth() .windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)), ) @@ -1687,7 +1802,10 @@ fun ImageVideoDescription( bitmap = it.asImageBitmap(), contentDescription = "some useful description", contentScale = ContentScale.FillWidth, - modifier = Modifier.padding(top = 4.dp).fillMaxWidth(), + modifier = + Modifier + .padding(top = 4.dp) + .fillMaxWidth(), ) } } else { @@ -1709,7 +1827,10 @@ fun ImageVideoDescription( ?: fileServers[0].server.name, options = fileServerOptions, onSelect = { selectedServer = fileServers[it] }, - modifier = Modifier.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)).weight(1f), + modifier = + Modifier + .windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)) + .weight(1f), ) } @@ -1718,7 +1839,10 @@ fun ImageVideoDescription( modifier = Modifier.fillMaxWidth(), ) { SettingSwitchItem( - modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), checked = sensitiveContent, onCheckedChange = { sensitiveContent = it }, title = R.string.add_sensitive_content_label, @@ -1729,12 +1853,16 @@ fun ImageVideoDescription( Row( verticalAlignment = Alignment.CenterVertically, modifier = - Modifier.fillMaxWidth().windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)), + Modifier + .fillMaxWidth() + .windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)), ) { OutlinedTextField( label = { Text(text = stringResource(R.string.content_description)) }, modifier = - Modifier.fillMaxWidth().windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)), + Modifier + .fillMaxWidth() + .windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)), value = message, onValueChange = { message = it }, placeholder = { @@ -1751,7 +1879,10 @@ fun ImageVideoDescription( } Button( - modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 10.dp), onClick = { onAdd(message, selectedServer, sensitiveContent) }, shape = QuoteBorder, colors = @@ -1767,7 +1898,10 @@ fun ImageVideoDescription( @Composable fun SettingSwitchItem( - modifier: Modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), + modifier: Modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), checked: Boolean, onCheckedChange: (Boolean) -> Unit, title: Int, From b5341f67e0f51c6effab8c247ee4819e1057b7f3 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 2 Feb 2024 18:23:44 -0500 Subject: [PATCH 035/176] Finally fixes: https://github.com/vitorpamplona/amethyst/issues/436 --- .../vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt index 94ab5037b..a603689a5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt @@ -105,12 +105,7 @@ class ZapOptionstViewModel : ViewModel() { } fun value(): Long? { - return try { - customAmount.text.trim().toLongOrNull() - } catch (e: Exception) { - if (e is CancellationException) throw e - null - } + return customAmount.text.trim().toLongOrNull() } fun cancel() {} @@ -285,7 +280,7 @@ fun ZapButton( onPost: () -> Unit, ) { Button( - onClick = { onPost() }, + onClick = { if (isActive) onPost() }, shape = ButtonBorder, colors = ButtonDefaults.buttonColors( From 63a7b442648c3b357e0c48c52bcd44159eee5b47 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 2 Feb 2024 18:44:10 -0500 Subject: [PATCH 036/176] Adds crowdin address to the readme --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 83bfc49a8..1f6987334 100644 --- a/README.md +++ b/README.md @@ -172,7 +172,9 @@ For the Play build: [Issues](https://github.com/vitorpamplona/amethyst/issues) and [pull requests](https://github.com/vitorpamplona/amethyst/pulls) here are also very welcome. -You can also send patches in Nostr using [GitStr](https://github.com/fiatjaf/gitstr) to `naddr1qqyxzmt9w358jum5qyg8v6t5daezumn0wd68yvfwvdhk6qg7waehxw309ahx7um5wgkhqatz9emk2mrvdaexgetj9ehx2ap0qy2hwumn8ghj7un9d3shjtnwdaehgu3wvfnj7q3qgcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqxpqqqpmej720gac` +Translations can be provided via [Crowdin](https://crowdin.com/project/amethyst-social) + +You can also send patches through Nostr using [GitStr](https://github.com/fiatjaf/gitstr) to `naddr1qqyxzmt9w358jum5qyg8v6t5daezumn0wd68yvfwvdhk6qg7waehxw309ahx7um5wgkhqatz9emk2mrvdaexgetj9ehx2ap0qy2hwumn8ghj7un9d3shjtnwdaehgu3wvfnj7q3qgcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqxpqqqpmej720gac` ## How to Deploy From 0bb39f91b9dddf81dcec63f3d97674f26872fbd0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 2 Feb 2024 18:53:36 -0500 Subject: [PATCH 037/176] README typos and formatting --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1f6987334..ffafec932 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases) - [ ] WebBrowser Signer (NIP-07, Not applicable) - [x] Old-style mentions (NIP-08) - [x] Event Deletion (NIP-09) -- [x] Replies, mentions, Threads and Notifications (NIP-10) +- [x] Replies, mentions, Threads, and Notifications (NIP-10) - [x] Relay Information Document (NIP-11) - [x] Generic Tag Queries (NIP-12) - [x] Proof of Work Display (NIP-13) @@ -113,13 +113,13 @@ The overall architecture consists of the UI, which uses the usual State/ViewMode and the model/repository layer, which keeps all Nostr objects in memory, in a full OO graph. The repository layer stores Nostr Events as Notes and Users separately. Those classes use LiveData objects to -allow the UI and other parts of the app to subscribe to each individual Note/User and receive updates when they happen. +allow the UI and other parts of the app to subscribe to each Note/User and receive updates when they happen. They are also responsible for updating viewModels when needed. Filters react to changes in the screen. As the user sees different Events, the Datasource classes are used to receive more information about those particular Events. Most of the UI is reactive to changes in the repository classes. The service layer assembles Nostr filters for each need of the app, receives the data from the Relay, and sends it to the repository. Connection with relays is never closed during the use of the app. -The UI receives a notification that objects were updated. Instances of User and Notes are mutable directly. +The UI receives a notification that objects have been updated. Instances of User and Notes are mutable directly. There will never be two Notes with the same ID or two User instances with the same pubkey. Lastly, the user's account information (priv key/pub key) is stored in the Android KeyStore for security. @@ -131,7 +131,7 @@ Make sure to have the following pre-requisites installed: 2. Android Studio 3. Android 8.0+ Phone or Emulation setup -Fork and clone this repository and import into Android Studio +Fork and clone this repository and import it into Android Studio ```bash git clone https://github.com/vitorpamplona/amethyst.git ``` @@ -174,7 +174,7 @@ For the Play build: Translations can be provided via [Crowdin](https://crowdin.com/project/amethyst-social) -You can also send patches through Nostr using [GitStr](https://github.com/fiatjaf/gitstr) to `naddr1qqyxzmt9w358jum5qyg8v6t5daezumn0wd68yvfwvdhk6qg7waehxw309ahx7um5wgkhqatz9emk2mrvdaexgetj9ehx2ap0qy2hwumn8ghj7un9d3shjtnwdaehgu3wvfnj7q3qgcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqxpqqqpmej720gac` +You can also send patches through Nostr using [GitStr](https://github.com/fiatjaf/gitstr) to [this nostr address](https://patch34.pages.dev/naddr1qqyxzmt9w358jum5qyg8v6t5daezumn0wd68yvfwvdhk6qg7waehxw309ahx7um5wgkhqatz9emk2mrvdaexgetj9ehx2ap0qy2hwumn8ghj7un9d3shjtnwdaehgu3wvfnj7q3qgcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqxpqqqpmej720gac) ## How to Deploy @@ -193,7 +193,7 @@ openssl base64 < | tr -d '\n' | tee some_signing_key.j 5. Tag the commit with `v{x.x.x}` 6. Let the [Create Release GitHub Action](https://github.com/vitorpamplona/amethyst/actions/workflows/create-release.yml) build a new `aab` file. 7. Add your CHANGE LOG to the description of the new release -8. Download the `aab` file and upload it to the` PlayStore. +8. Download the `aab` file and upload it to the PlayStore. # Privacy on Relays & nostr Your internet protocol (IP) address is exposed to the relays you connect to. If you want to improve your privacy, consider utilizing a service that masks your IP address (e.g. a VPN) from trackers online. From be43f2025f45c707f069cc25a2303d2450994cb3 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 5 Feb 2024 12:35:42 -0500 Subject: [PATCH 038/176] v0.83.10 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 11868fee2..b3cd712ac 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 34 - versionCode 350 - versionName "0.83.9" + versionCode 351 + versionName "0.83.10" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From 5bdc59d0e36e59233bf8d86d38899115f93d086a Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Mon, 5 Feb 2024 17:37:48 +0000 Subject: [PATCH 039/176] New Crowdin translations by GitHub Action --- app/src/main/res/values-fa/strings.xml | 65 ++++++++++++++++++++++++++ app/src/main/res/values-hu/strings.xml | 1 + 2 files changed, 66 insertions(+) diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index da4d9d1c3..a437c1bab 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -90,6 +90,8 @@ افزودن رله نمایش نام نام من + استریچ باحالیان + خوش آمدی استریچ! نام کاربری نام کاربری من درباره من @@ -143,7 +145,14 @@ شرایط استفاده پذیرش شرایط الازمیست کلید الزامیست + نام الزامی است ورود + ثبت نام + ایجاد حساب کاربری + شما را چی صدا بزنیم؟ + حساب کاربری ناستر ندارید؟ + از قبل حساب کاربری ناستر دارید؟ + ایجاد حساب کاربری جدید ساختن جفت کلید جدید در حال بارگیری خبرنامه بارگذاری حساب کاربری @@ -318,6 +327,7 @@ لیست دنبال ها همه دنبال ها همگانی + لیست خموش ## از طریق Tor با Orbot متصل شوید \n\n1. نصب کنید [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android) \n2. شروع Orbot @@ -385,6 +395,9 @@ پرداخت توکن Cashu بازپرداخت + ارسال به کیف پول زپ + باز کردن در کیف پول Cashu + کپی کردن توکن آدرس لایتنینگ تنظیم نشده است توکن به کلیپبورد کپی شد زنده @@ -396,6 +409,7 @@ خروج همه اطلاعات محلی شما را پاک می کند. مطمئن شوید که کلید خصوصی خود را بکاپ گرفته و ذخیره کرده اید تا حساب کاربری تان را از دست ندهید. می خواهید ادامه دهید؟ برچسب های دنبال شده رله ها + بازار زنده انجمن گپ @@ -454,6 +468,7 @@ پخش خودکار ویدئوها و جیف ها نشان دادن پیش نمایش URL هنگام بارگیری تصاویر + کپی به کلیپ‌بورد کپی URL به کلیپبورد کپی شناسه یادداشت به کلیپبورد ایجاد شده در @@ -489,6 +504,9 @@ پرداخت شد کیف پول %1$s خطا در بازکردن اپلیکیشن امضا کننده + اپ امضا کننده یافت نشد. بررسی کنید که اپ حذف نشده باشد. + اپ امضا کننده رد کرد + مطمئن شوید که اپ امضا کننده این تراکنش را تایید کرده است هیچ کیف پولی برای پرداخت صورتحساب لایتنینگ یافت نشد (خطا: %1$s). لطفا کیف پول لایتنینگی برای استفاده از زپ نصب کنید هیچ کیف پولی برای پرداخت صورتحساب لایتنینگ یافت نشد. لطفا یک کیف پول لایتنینگی برای استفاده از زپ نصب کنید کلمات پنهان @@ -506,6 +524,7 @@ توکن Cashu خرج شده است. Cashu دریافت شد %1$s ساتوشی به کیف پول شما ارسال شد. (کارمزد: %2$s ساتوشی) + هیچ کیف پول Cashu سازگاری در سیستم یافت نشد صورتحساب از سرور گیرنده دریافت نشد ارائه دهنده اتصال کیف پول شما خطای روبرو را داد: %1$s نمی توان به Tor وصل شد @@ -513,6 +532,7 @@ LNUrl از آدرس لایتنینگ \"%1$s\" بدست نیامد. تنظیمات کاربر را بررسی کنید. خدمات لایتنینگ %1$s گیرنده دردسترس نیست. از آدرس لایتنینگ \"%2$s\" محاسبه شد. خطا: %3$s. بررسی کنید آیا سرور کار می کند و آیا آدرس لایتنینگ صحیح است. مشکل %1$s حل نشد. بررسی کنید که به اینترنت متصل باشید، سرور کار کند و آدرس لایتنینگ %2$s صحیح باشد. + مشکل %1$s حل نشد. بررسی کنید که به اینترنت متصل باشید، سرور کار کند و آدرس لایتنینگ %2$s صحیح باشد. \n\nخطای %3$s صورتحساب از %1$s گرفته نشد خطا در تفسیر JSON از آدرس لایتنینگ. تنظیمات لایتنینگ کاربر را بررسی کنید. URL بازخوانی در پیکربندی سرور آدرس لایتنینگ یافت نشد @@ -532,4 +552,49 @@ برای دریافت اعلان ها، می توانید هر اپلیکیشنی که از [Unified Push](https://unifiedpush.org/) پشتیبانی می کند نصب کنید، مانند[Nfty](https://ntfy.sh/). پس از نصب، اپلیکیشنی را که می خواهید استفاده کنید در تنظیمات انتخاب کنید. + پیغام از طرف %1$s + رسته + به فروشنده پیغام دهید + سلام %1$s، هنوز موجوده؟ + سلام، هنوز موجوده؟ + چیزی بفروشید + عنوان + آیفون 13 + شرایط + دسته بندی + قیمت (به ساتوشی) + 1000 + موقعیت مکانی + شهر، استان، کشور + جدید + این مورد نو هست، در بسته بندی اصلی + در حد نو + دست دوم است، اما ظاهرش تمیز است + خوب + کمی در ظاهر نشان می دهد که دست دوم است + منصفانه + هنوز مناسب است و کار می کند + پوشاک + لوازم جانبی + الکترونیکی + مبلمان + یادگاری + کتاب + حیوانات خانگی + ورزش + تناسب اندام + هنر + دست سازه + خانه + اداره + خوراکی + گوناگون + سایر + رسانه بارگذاری نشد + فایل فشرده باز نشد + خطا در فشرده سازی رسانه: %1$s + خطای بارگیری: %1$s + سرور پس از بارگذاری URL نداد + فایل بارگذاری شده از سرور بارگیری نشد + فایل محلی برای بارگذاری آماده نشد: %1$s diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index f1d550af2..73643d7bc 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -532,6 +532,7 @@ Nem sikerült az LNUrl-t a(z) \"%1$s\" Lightning-címről összeállítani. Ellenőrizd a felhasználó beállítását A fogadó lightning szolgáltatása itt: %1$s nem érhető el. A \"%2$s\" lightning címből lett kiszámítva. Hiba: %3$s. Ellenőrizd, hogy a szerver működik-e, és hogy a lightning cím helyes-e Nem sikerült megoldani: %1$s. Ellenőrizd, hogy csatlakozol-e, működik-e a szerver, és hogy a %2$s lightning cím helyes-e + Nem sikerült megoldani: %1$s. Ellenőrizd, hogy csatlakozol-e, működik-e a szerver, és hogy a %2$s lightning cím helyes-e.\n\nKivétel: %3$s Nem sikerült a számlát a következőtől: %1$s lekérni Hiba a Lightning-címből származó JSON elemzésekor. Ellenőrizd a felhasználó Lightning beállítását A visszahívási URL a felhasználó Lightning címszerver konfigurációjában nem található From 36c49b2171791263db4523b5badbfee778135ba8 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 5 Feb 2024 16:52:48 -0500 Subject: [PATCH 040/176] Updates Zoomable and Zxing Qr Reader --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index b3cd712ac..d42eb3d99 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -192,7 +192,7 @@ dependencies { implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" // Zoomable images - implementation 'net.engawapg.lib:zoomable:1.5.3' + implementation 'net.engawapg.lib:zoomable:1.6.0' // Biometrics implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05" @@ -229,7 +229,7 @@ dependencies { implementation "com.google.accompanist:accompanist-permissions:$accompanist_version" // For QR generation - implementation 'com.google.zxing:core:3.5.2' + implementation 'com.google.zxing:core:3.5.3' implementation 'com.journeyapps:zxing-android-embedded:4.3.0' // Markdown From b13b83269658f34494c6242daf42f71730310a80 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 5 Feb 2024 18:18:33 -0500 Subject: [PATCH 041/176] Starting a new module for app benchmarks --- app/build.gradle | 1 + .../ui/components/ExpandableRichTextViewer.kt | 29 +---- benchmark/build.gradle | 1 + .../ExpandableViewComputationBenchmark.kt | 119 ++++++++++++++++++ .../benchmark/BechBenchmark.kt | 2 +- .../benchmark/ContainsBenchmark.kt | 2 +- .../benchmark/CryptoBenchmark.kt | 2 +- .../benchmark/EventBenchmark.kt | 2 +- .../benchmark/GiftWrapBenchmark.kt | 2 +- .../benchmark/GiftWrapReceivingBenchmark.kt | 2 +- .../benchmark/GiftWrapSigningBenchmark.kt | 2 +- .../benchmark/HexBenchmark.kt | 2 +- .../benchmark/RobohashBenchmark.kt | 2 +- commons/.gitignore | 1 + commons/build.gradle | 41 ++++++ commons/consumer-rules.pro | 0 commons/proguard-rules.pro | 21 ++++ commons/src/main/AndroidManifest.xml | 4 + .../amethyst/commons/ExpandableTextParser.kt | 52 ++++++++ settings.gradle | 1 + 20 files changed, 252 insertions(+), 36 deletions(-) create mode 100644 benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/ExpandableViewComputationBenchmark.kt rename benchmark/src/androidTest/java/com/vitorpamplona/{amethyst => quartz}/benchmark/BechBenchmark.kt (98%) rename benchmark/src/androidTest/java/com/vitorpamplona/{amethyst => quartz}/benchmark/ContainsBenchmark.kt (99%) rename benchmark/src/androidTest/java/com/vitorpamplona/{amethyst => quartz}/benchmark/CryptoBenchmark.kt (98%) rename benchmark/src/androidTest/java/com/vitorpamplona/{amethyst => quartz}/benchmark/EventBenchmark.kt (99%) rename benchmark/src/androidTest/java/com/vitorpamplona/{amethyst => quartz}/benchmark/GiftWrapBenchmark.kt (99%) rename benchmark/src/androidTest/java/com/vitorpamplona/{amethyst => quartz}/benchmark/GiftWrapReceivingBenchmark.kt (99%) rename benchmark/src/androidTest/java/com/vitorpamplona/{amethyst => quartz}/benchmark/GiftWrapSigningBenchmark.kt (99%) rename benchmark/src/androidTest/java/com/vitorpamplona/{amethyst => quartz}/benchmark/HexBenchmark.kt (98%) rename benchmark/src/androidTest/java/com/vitorpamplona/{amethyst => quartz}/benchmark/RobohashBenchmark.kt (99%) create mode 100644 commons/.gitignore create mode 100644 commons/build.gradle create mode 100644 commons/consumer-rules.pro create mode 100644 commons/proguard-rules.pro create mode 100644 commons/src/main/AndroidManifest.xml create mode 100644 commons/src/main/java/com/vitorpamplona/amethyst/commons/ExpandableTextParser.kt diff --git a/app/build.gradle b/app/build.gradle index d42eb3d99..d5687053f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -162,6 +162,7 @@ android { dependencies { implementation project(path: ':quartz') + implementation project(path: ':commons') implementation "androidx.core:core-ktx:$core_ktx_version" implementation 'androidx.activity:activity-compose:1.8.2' implementation "androidx.compose.ui:ui:$compose_ui_version" diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt index 8128f54b7..325211aa2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt @@ -44,6 +44,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.ExpandableTextParser import com.vitorpamplona.amethyst.ui.note.getGradient import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.ButtonBorder @@ -51,9 +52,6 @@ import com.vitorpamplona.amethyst.ui.theme.ButtonPadding import com.vitorpamplona.amethyst.ui.theme.secondaryButtonBackground import com.vitorpamplona.quartz.events.ImmutableListOfLists -const val SHORT_TEXT_LENGTH = 350 -const val SHORTEN_AFTER_LINES = 10 - @Composable fun ExpandableRichTextViewer( content: String, @@ -66,30 +64,7 @@ fun ExpandableRichTextViewer( ) { var showFullText by remember { mutableStateOf(false) } - val whereToCut = - remember(content) { - // Cuts the text in the first space or new line after SHORT_TEXT_LENGTH characters - val firstSpaceAfterCut = - content.indexOf(' ', SHORT_TEXT_LENGTH).let { if (it < 0) content.length else it } - val firstNewLineAfterCut = - content.indexOf('\n', SHORT_TEXT_LENGTH).let { if (it < 0) content.length else it } - - // or after SHORTEN_AFTER_LINES lines - val numberOfLines = content.count { it == '\n' } - - var charactersInLines = minOf(firstSpaceAfterCut, firstNewLineAfterCut) - - if (numberOfLines > SHORTEN_AFTER_LINES) { - val shortContent = content.lines().take(SHORTEN_AFTER_LINES) - charactersInLines = 0 - for (line in shortContent) { - // +1 because new line character is omitted from .lines - charactersInLines += (line.length + 1) - } - } - - minOf(firstSpaceAfterCut, firstNewLineAfterCut, charactersInLines) - } + val whereToCut = remember(content) { ExpandableTextParser().computeWhereToCutIfPostIsTooLong(content) } val text by remember(content) { diff --git a/benchmark/build.gradle b/benchmark/build.gradle index 79f7fc1f5..8dcb746c1 100644 --- a/benchmark/build.gradle +++ b/benchmark/build.gradle @@ -54,6 +54,7 @@ dependencies { androidTestImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.benchmark:benchmark-junit4:1.2.3' androidTestImplementation project(path: ':quartz') + androidTestImplementation project(path: ':commons') // Add your dependencies here. Note that you cannot benchmark code // in an app module this way - you will need to move any code you diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/ExpandableViewComputationBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/ExpandableViewComputationBenchmark.kt new file mode 100644 index 000000000..a1c6dbde9 --- /dev/null +++ b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/ExpandableViewComputationBenchmark.kt @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2023 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.benchmark + +import androidx.benchmark.junit4.BenchmarkRule +import androidx.benchmark.junit4.measureRepeated +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.amethyst.commons.ExpandableTextParser +import junit.framework.TestCase +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ExpandableViewComputationBenchmark { + @get:Rule + val benchmarkRule = BenchmarkRule() + + @Test + fun computeTestCase1() { + benchmarkRule.measureRepeated { + TestCase.assertEquals( + 294, + ExpandableTextParser().computeWhereToCutIfPostIsTooLong(testCase1), + ) + } + } + + @Test + fun computeTestCase2() { + benchmarkRule.measureRepeated { + TestCase.assertEquals( + 355, + ExpandableTextParser().computeWhereToCutIfPostIsTooLong(testCase2), + ) + } + } + + val testCase1 = """ +#Amethyst v0.83.10 + +تحديث جديد لـ Amethyst بإصدار 0.83.10 مع تعديلات وإضافات جديدة + +: NIP-92 إصلاحات الأخطاء + + الإضافات الجديدة: + - يتضمن رابط المنتج في الرسالة الأولى من المشتري في السوق + - يضيف دعمًا لـ NIP-92 في الرسائل العامة والرسائل المباشرة الجديدة (NIP-17). يبقى NIP-54 في NIP-04 DMs + - إضافة التمرير الأفقي إلى أزرار الإجراءات في شاشة النشر الجديد لإصلاح الأزرار المخفية جزئيًا في الشاشات الصغيرة/الرفيعة. + + اصلاحات الشوائب: + - إصلاحات التعطل مع مبلغ Zap مخصص غير صالح + - يعمل على إصلاح مشكلات إعادة اتصال التتابع عندما يقوم المرحل بإغلاق الاتصال + - إصلاح الحشو العلوي للملاحظة المقتبسة في المنشور + - تحسين استخدام الذاكرة للمستخدم المرئي وعلامة URL في المشاركات الجديدة + + الترجمات المحدثة: + - الفارسية بواسطة + - الفرنسية والإنجليزية، المملكة المتحدة بواسطة + - الأوكرانية + - الإسبانية والإسبانية والمكسيك والإسبانية والولايات المتحدة بواسطة + - العربية + + تحسينات جودة الكود: + - تحديثات لنظام Android Studio 2023.1.1 Patch 2 + + + + +nostr:nevent1qqszq7kl888sw0c5rpvepn8w373zt0jrw8864x8lkauxxw335s66rzgppemhxue69uhkummn9ekx7mp0qgsyvrp9u6p0mfur9dfdru3d853tx9mdjuhkphxuxgfwmryja7zsvhqrqsqqqqqpaax7m2 +""" + + val testCase2 = """ +#Amethyst v0.83.10: NIP-92 and Bug Fixes + +New Additions: +- Includes a link to the product in the first message from the buyer in the marketplace +- Adds support for NIP-92 in public messages and new DMs (NIP-17). NIP-54 stays in NIP-04 DMs +- Adds Horizontal Scroll to the action buttons in the New Post screen to partially fix hidden buttons in small/thin screens. + +Bugfixes: +- Fixes crash with an invalid custom Zap Amount +- Fixes relay re-connection issues when the relay closes a connection +- Fixes the top padding of the quoted note in a post +- Optimizes memory use of the visual user and url tagger in new posts + +Updated translations: +- Persian by nostr:npub1cpazafytvafazxkjn43zjfwtfzatfz508r54f6z6a3rf2ws8223qc3xxpk +- French and English, United Kingdom by nostr:npub13qtw3yu0uc9r4yj5x0rhgy8nj5q0uyeq0pavkgt9ly69uuzxgkfqwvx23t +- Ukrainian by crowdin.com/profile/liizzzz +- Spanish, Spanish, Mexico and Spanish, United States by nostr:npub1luhyzgce7qtcs6r6v00ryjxza8av8u4dzh3avg0zks38tjktnmxspxq903 +- Arabic by nostr:npub13qtw3yu0uc9r4yj5x0rhgy8nj5q0uyeq0pavkgt9ly69uuzxgkfqwvx23t + +Code Quality Improvements: +- Updates to Android Studio 2023.1.1 Patch 2 + +Download: +- [Play Edition](https://github.com/vitorpamplona/amethyst/releases/download/v0.83.10/amethyst-googleplay-universal-v0.83.10.apk ) +- [FOSS Edition - No translations](https://github.com/vitorpamplona/amethyst/releases/download/v0.83.10/amethyst-fdroid-universal-v0.83.10.apk ) +""" +} diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/BechBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/BechBenchmark.kt similarity index 98% rename from benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/BechBenchmark.kt rename to benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/BechBenchmark.kt index eecc0213f..fa63a5afa 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/BechBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/BechBenchmark.kt @@ -18,7 +18,7 @@ * 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.benchmark +package com.vitorpamplona.quartz.benchmark import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/ContainsBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/ContainsBenchmark.kt similarity index 99% rename from benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/ContainsBenchmark.kt rename to benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/ContainsBenchmark.kt index b0063d14d..151e9e421 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/ContainsBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/ContainsBenchmark.kt @@ -18,7 +18,7 @@ * 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.benchmark +package com.vitorpamplona.quartz.benchmark import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/CryptoBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CryptoBenchmark.kt similarity index 98% rename from benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/CryptoBenchmark.kt rename to benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CryptoBenchmark.kt index 82b1bbb7a..4f484dbed 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/CryptoBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CryptoBenchmark.kt @@ -18,7 +18,7 @@ * 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.benchmark +package com.vitorpamplona.quartz.benchmark import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/EventBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventBenchmark.kt similarity index 99% rename from benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/EventBenchmark.kt rename to benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventBenchmark.kt index ddfbb0fd8..75140de66 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/EventBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventBenchmark.kt @@ -18,7 +18,7 @@ * 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.benchmark +package com.vitorpamplona.quartz.benchmark import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/GiftWrapBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapBenchmark.kt similarity index 99% rename from benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/GiftWrapBenchmark.kt rename to benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapBenchmark.kt index ecf035d6e..ab8791e8c 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/GiftWrapBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapBenchmark.kt @@ -18,7 +18,7 @@ * 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.benchmark +package com.vitorpamplona.quartz.benchmark import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/GiftWrapReceivingBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapReceivingBenchmark.kt similarity index 99% rename from benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/GiftWrapReceivingBenchmark.kt rename to benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapReceivingBenchmark.kt index 19591c4fd..ec38a52dd 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/GiftWrapReceivingBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapReceivingBenchmark.kt @@ -18,7 +18,7 @@ * 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.benchmark +package com.vitorpamplona.quartz.benchmark import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/GiftWrapSigningBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapSigningBenchmark.kt similarity index 99% rename from benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/GiftWrapSigningBenchmark.kt rename to benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapSigningBenchmark.kt index 6363da2b9..5d352c10d 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/GiftWrapSigningBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapSigningBenchmark.kt @@ -18,7 +18,7 @@ * 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.benchmark +package com.vitorpamplona.quartz.benchmark import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/HexBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt similarity index 98% rename from benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/HexBenchmark.kt rename to benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt index b641c7574..1c6d904e9 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/HexBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt @@ -18,7 +18,7 @@ * 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.benchmark +package com.vitorpamplona.quartz.benchmark import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/RobohashBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/RobohashBenchmark.kt similarity index 99% rename from benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/RobohashBenchmark.kt rename to benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/RobohashBenchmark.kt index bc32301c7..d5682a81a 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/RobohashBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/RobohashBenchmark.kt @@ -18,7 +18,7 @@ * 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.benchmark +package com.vitorpamplona.quartz.benchmark import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated diff --git a/commons/.gitignore b/commons/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/commons/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/commons/build.gradle b/commons/build.gradle new file mode 100644 index 000000000..59b024720 --- /dev/null +++ b/commons/build.gradle @@ -0,0 +1,41 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'com.vitorpamplona.amethyst.commons' + compileSdk 34 + + defaultConfig { + minSdk 26 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + create("benchmark") { + initWith(getByName("release")) + signingConfig signingConfigs.debug + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = '17' + freeCompilerArgs += "-Xstring-concat=inline" + } +} + +dependencies { + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' +} \ No newline at end of file diff --git a/commons/consumer-rules.pro b/commons/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/commons/proguard-rules.pro b/commons/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/commons/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/commons/src/main/AndroidManifest.xml b/commons/src/main/AndroidManifest.xml new file mode 100644 index 000000000..44008a433 --- /dev/null +++ b/commons/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/ExpandableTextParser.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/ExpandableTextParser.kt new file mode 100644 index 000000000..a194933a0 --- /dev/null +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/ExpandableTextParser.kt @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2023 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.commons + +class ExpandableTextParser { + companion object { + const val SHORT_TEXT_LENGTH = 350 + const val SHORTEN_AFTER_LINES = 10 + } + + fun computeWhereToCutIfPostIsTooLong(content: String): Int { + // Cuts the text in the first space or new line after SHORT_TEXT_LENGTH characters + val firstSpaceAfterCut = + content.indexOf(' ', SHORT_TEXT_LENGTH).let { if (it < 0) content.length else it } + val firstNewLineAfterCut = + content.indexOf('\n', SHORT_TEXT_LENGTH).let { if (it < 0) content.length else it } + + // or after SHORTEN_AFTER_LINES lines + val numberOfLines = content.count { it == '\n' } + + var charactersInLines = minOf(firstSpaceAfterCut, firstNewLineAfterCut) + + if (numberOfLines > SHORTEN_AFTER_LINES) { + val shortContent = content.lines().take(SHORTEN_AFTER_LINES) + charactersInLines = 0 + for (line in shortContent) { + // +1 because new line character is omitted from .lines + charactersInLines += (line.length + 1) + } + } + + return minOf(firstSpaceAfterCut, firstNewLineAfterCut, charactersInLines) + } +} diff --git a/settings.gradle b/settings.gradle index d535b492f..6ac181c95 100644 --- a/settings.gradle +++ b/settings.gradle @@ -23,3 +23,4 @@ rootProject.name = "Amethyst" include ':app' include ':benchmark' include ':quartz' +include ':commons' From 845c99b7983212a97ad5c01eecb5b2c568955001 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 5 Feb 2024 19:37:21 -0500 Subject: [PATCH 042/176] Faster Expandable Text calculator. --- .../ui/components/ExpandableRichTextViewer.kt | 2 +- .../ExpandableViewComputationBenchmark.kt | 27 ++++++++-- .../amethyst/commons/ExpandableTextParser.kt | 50 ++++++++++--------- 3 files changed, 52 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt index 325211aa2..fc845ba2a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt @@ -64,7 +64,7 @@ fun ExpandableRichTextViewer( ) { var showFullText by remember { mutableStateOf(false) } - val whereToCut = remember(content) { ExpandableTextParser().computeWhereToCutIfPostIsTooLong(content) } + val whereToCut = remember(content) { ExpandableTextParser.computeWhereToCutIfPostIsTooLong(content) } val text by remember(content) { diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/ExpandableViewComputationBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/ExpandableViewComputationBenchmark.kt index a1c6dbde9..b4e52f3d3 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/ExpandableViewComputationBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/ExpandableViewComputationBenchmark.kt @@ -24,6 +24,7 @@ import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.amethyst.commons.ExpandableTextParser +import com.vitorpamplona.amethyst.commons.nthIndexOf import junit.framework.TestCase import org.junit.Rule import org.junit.Test @@ -38,18 +39,38 @@ class ExpandableViewComputationBenchmark { fun computeTestCase1() { benchmarkRule.measureRepeated { TestCase.assertEquals( - 294, - ExpandableTextParser().computeWhereToCutIfPostIsTooLong(testCase1), + 293, + testCase1.nthIndexOf('\n', 10), ) } } @Test fun computeTestCase2() { + benchmarkRule.measureRepeated { + TestCase.assertEquals( + 423, + testCase2.nthIndexOf('\n', 10), + ) + } + } + + @Test + fun computeTestCase1All() { + benchmarkRule.measureRepeated { + TestCase.assertEquals( + 293, + ExpandableTextParser.computeWhereToCutIfPostIsTooLong(testCase1), + ) + } + } + + @Test + fun computeTestCase2All() { benchmarkRule.measureRepeated { TestCase.assertEquals( 355, - ExpandableTextParser().computeWhereToCutIfPostIsTooLong(testCase2), + ExpandableTextParser.computeWhereToCutIfPostIsTooLong(testCase2), ) } } diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/ExpandableTextParser.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/ExpandableTextParser.kt index a194933a0..38b43528a 100644 --- a/commons/src/main/java/com/vitorpamplona/amethyst/commons/ExpandableTextParser.kt +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/ExpandableTextParser.kt @@ -22,31 +22,35 @@ package com.vitorpamplona.amethyst.commons class ExpandableTextParser { companion object { - const val SHORT_TEXT_LENGTH = 350 - const val SHORTEN_AFTER_LINES = 10 - } + private const val SHORT_TEXT_LENGTH = 350 + private const val SHORTEN_AFTER_LINES = 10 - fun computeWhereToCutIfPostIsTooLong(content: String): Int { - // Cuts the text in the first space or new line after SHORT_TEXT_LENGTH characters - val firstSpaceAfterCut = - content.indexOf(' ', SHORT_TEXT_LENGTH).let { if (it < 0) content.length else it } - val firstNewLineAfterCut = - content.indexOf('\n', SHORT_TEXT_LENGTH).let { if (it < 0) content.length else it } + fun computeWhereToCutIfPostIsTooLong(content: String): Int { + // Cuts the text in the first space or new line after SHORT_TEXT_LENGTH characters + val firstSpaceAfterCut = + content.indexOf(' ', SHORT_TEXT_LENGTH).let { if (it < 0) content.length else it } + val firstNewLineAfterCut = + content.indexOf('\n', SHORT_TEXT_LENGTH).let { if (it < 0) content.length else it } + val firstLineAfterLineLimits = + content.nthIndexOf('\n', SHORTEN_AFTER_LINES).let { if (it < 0) content.length else it } - // or after SHORTEN_AFTER_LINES lines - val numberOfLines = content.count { it == '\n' } - - var charactersInLines = minOf(firstSpaceAfterCut, firstNewLineAfterCut) - - if (numberOfLines > SHORTEN_AFTER_LINES) { - val shortContent = content.lines().take(SHORTEN_AFTER_LINES) - charactersInLines = 0 - for (line in shortContent) { - // +1 because new line character is omitted from .lines - charactersInLines += (line.length + 1) - } + return minOf(firstSpaceAfterCut, firstNewLineAfterCut, firstLineAfterLineLimits) } - - return minOf(firstSpaceAfterCut, firstNewLineAfterCut, charactersInLines) } } + +fun String.nthIndexOf( + ch: Char, + N: Int, +): Int { + var occur = N + var pos = -1 + + while (occur > 0) { + // calling the native function multiple times is faster than looping just once + pos = indexOf(ch, pos + 1) + occur-- + } + + return if (occur == 0) pos else -1 +} From cce7a209182f5e7f1b85f2dd2ea69e74f8171bad Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 5 Feb 2024 19:47:33 -0500 Subject: [PATCH 043/176] Speeds up parsing of the event content. --- .../amethyst/service/CachedRichTextParser.kt | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt index 70816cd69..e43f0a138 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt @@ -45,6 +45,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toImmutableSet +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CancellationException import java.util.regex.Pattern @@ -99,12 +100,12 @@ val HTTPRegex = class RichTextParser() { fun parseMediaUrl( fullUrl: String, - tags: ImmutableListOfLists, + eventTags: ImmutableListOfLists, ): ZoomableUrlContent? { val removedParamsFromUrl = removeQueryParamsForExtensionComparison(fullUrl) return if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) { val frags = Nip54().parse(fullUrl) - val tags = Nip92().parse(fullUrl, tags.lists) + val tags = Nip92().parse(fullUrl, eventTags.lists) ZoomableUrlImage( url = fullUrl, @@ -116,7 +117,7 @@ class RichTextParser() { ) } else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) { val frags = Nip54().parse(fullUrl) - val tags = Nip92().parse(fullUrl, tags.lists) + val tags = Nip92().parse(fullUrl, eventTags.lists) ZoomableUrlVideo( url = fullUrl, description = frags[FileHeaderEvent.ALT] ?: tags[FileHeaderEvent.ALT], @@ -179,34 +180,34 @@ class RichTextParser() { emojis: Map, tags: ImmutableListOfLists, ): ImmutableList { - var paragraphSegments = persistentListOf() + val lines = content.split('\n') + val paragraphSegments = ArrayList(lines.size) - content.split('\n').forEach { paragraph -> - var segments = persistentListOf() + lines.forEach { paragraph -> var isDirty = false - val isRTL = isArabic(paragraph) val wordList = paragraph.trimEnd().split(' ') + val segments = ArrayList(wordList.size) wordList.forEach { word -> val wordSegment = wordIdentifier(word, images, urls, emojis, tags) if (wordSegment !is RegularTextSegment) { isDirty = true } - segments = segments.add(wordSegment) + segments.add(wordSegment) } val newSegments = if (isDirty) { - ParagraphState(segments, isRTL) + ParagraphState(segments.toPersistentList(), isRTL) } else { ParagraphState(persistentListOf(RegularTextSegment(paragraph)), isRTL) } - paragraphSegments = paragraphSegments.add(newSegments) + paragraphSegments.add(newSegments) } - return paragraphSegments + return paragraphSegments.toImmutableList() } fun isNumber(word: String): Boolean { From 248bdffd502a59f4c18a8dc583c3da04d25c433b Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 5 Feb 2024 19:49:27 -0500 Subject: [PATCH 044/176] v0.83.11 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d5687053f..52723d6c5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 34 - versionCode 351 - versionName "0.83.10" + versionCode 352 + versionName "0.83.11" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From 3298fd44933ed23053b7f4b2669c079f66a1865c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 5 Feb 2024 20:26:37 -0500 Subject: [PATCH 045/176] BugFix for short lists --- .../ExpandableViewComputationBenchmark.kt | 15 +++++++++++++++ .../amethyst/commons/ExpandableTextParser.kt | 1 + 2 files changed, 16 insertions(+) diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/ExpandableViewComputationBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/ExpandableViewComputationBenchmark.kt index b4e52f3d3..dc97a4449 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/ExpandableViewComputationBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/ExpandableViewComputationBenchmark.kt @@ -75,6 +75,16 @@ class ExpandableViewComputationBenchmark { } } + @Test + fun computeTestCase3All() { + benchmarkRule.measureRepeated { + TestCase.assertEquals( + 65, + ExpandableTextParser.computeWhereToCutIfPostIsTooLong(testCase3), + ) + } + } + val testCase1 = """ #Amethyst v0.83.10 @@ -137,4 +147,9 @@ Download: - [Play Edition](https://github.com/vitorpamplona/amethyst/releases/download/v0.83.10/amethyst-googleplay-universal-v0.83.10.apk ) - [FOSS Edition - No translations](https://github.com/vitorpamplona/amethyst/releases/download/v0.83.10/amethyst-fdroid-universal-v0.83.10.apk ) """ + + val testCase3 = """#100aDayUntil100k +Day 5 ✔️ + +Seems like they may be getting easier""" } diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/ExpandableTextParser.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/ExpandableTextParser.kt index 38b43528a..601993357 100644 --- a/commons/src/main/java/com/vitorpamplona/amethyst/commons/ExpandableTextParser.kt +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/ExpandableTextParser.kt @@ -49,6 +49,7 @@ fun String.nthIndexOf( while (occur > 0) { // calling the native function multiple times is faster than looping just once pos = indexOf(ch, pos + 1) + if (pos == -1) return -1 occur-- } From 95e19cf681cbcf1f52399a5d5ea972971bb6537d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 5 Feb 2024 20:29:43 -0500 Subject: [PATCH 046/176] v0.83.12 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 52723d6c5..ff2a2345c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 34 - versionCode 352 - versionName "0.83.11" + versionCode 353 + versionName "0.83.12" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From 68e90ce3edc16d8325ea6583942da51985c7bf56 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 7 Feb 2024 16:14:48 -0500 Subject: [PATCH 047/176] Unwrapps the reply message if the GiftWrap was tagged and not the inner message id. --- .../ui/note/ChatroomMessageCompose.kt | 11 +++- .../ui/screen/loggedIn/AccountViewModel.kt | 50 +++++++++++++++++++ .../quartz/events/GiftWrapEvent.kt | 4 ++ .../quartz/events/SealedGossipEvent.kt | 4 ++ 4 files changed, 67 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index 08dca663f..0951de209 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -46,6 +46,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -492,8 +493,14 @@ private fun RenderReply( onWantsToReply: (Note) -> Unit, ) { Row(verticalAlignment = Alignment.CenterVertically) { - val replyTo by remember { derivedStateOf { note.replyTo?.lastOrNull() } } - replyTo?.let { note -> + val replyTo = + produceState(initialValue = note.replyTo?.lastOrNull()) { + accountViewModel.unwrapIfNeeded(value) { + value = it + } + } + + replyTo.value?.let { note -> ChatroomMessageCompose( note, null, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 36671804d..e5b295140 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -73,6 +73,7 @@ import com.vitorpamplona.quartz.encoders.Nip19 import com.vitorpamplona.quartz.events.ChatroomKey import com.vitorpamplona.quartz.events.ChatroomKeyable import com.vitorpamplona.quartz.events.Event +import com.vitorpamplona.quartz.events.EventInterface import com.vitorpamplona.quartz.events.GiftWrapEvent import com.vitorpamplona.quartz.events.ImmutableListOfLists import com.vitorpamplona.quartz.events.LnZapEvent @@ -1175,6 +1176,55 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View ) } } + + fun unwrapIfNeeded( + event: EventInterface?, + onReady: (Note) -> Unit, + ) { + when (event) { + is GiftWrapEvent -> { + event.cachedGift(account.signer) { + val existingNote = LocalCache.getNoteIfExists(it.id) + if (existingNote != null) { + unwrapIfNeeded(existingNote.event, onReady) + } else { + LocalCache.verifyAndConsume(it, null) + unwrapIfNeeded(it, onReady) + } + } + } + is SealedGossipEvent -> { + event.cachedGossip(account.signer) { + val existingNote = LocalCache.getNoteIfExists(it.id) + if (existingNote != null) { + unwrapIfNeeded(existingNote.event, onReady) + } else { + // this is not verifiable + LocalCache.justConsume(it, null) + unwrapIfNeeded(it, onReady) + } + } + } + else -> { + event?.id()?.let { + LocalCache.getNoteIfExists(it)?.let { + onReady(it) + } + } + } + } + } + + fun unwrapIfNeeded( + note: Note?, + onReady: (Note) -> Unit, + ) { + viewModelScope.launch(Dispatchers.IO) { + unwrapIfNeeded(note?.event) { + onReady(it) + } + } + } } class HasNotificationDot(bottomNavigationItems: ImmutableList) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GiftWrapEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GiftWrapEvent.kt index dc576da31..f63c0b7b2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GiftWrapEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GiftWrapEvent.kt @@ -38,6 +38,10 @@ class GiftWrapEvent( ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { @Transient private var cachedInnerEvent: Map = mapOf() + fun preCachedGift(signer: NostrSigner): Event? { + return cachedInnerEvent[signer.pubKey] + } + fun cachedGift( signer: NostrSigner, onReady: (Event) -> Unit, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/SealedGossipEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/SealedGossipEvent.kt index 610baabf2..f3d93b45c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/SealedGossipEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/SealedGossipEvent.kt @@ -39,6 +39,10 @@ class SealedGossipEvent( ) : WrappedEvent(id, pubKey, createdAt, KIND, tags, content, sig) { @Transient private var cachedInnerEvent: Map = mapOf() + fun preCachedGossip(signer: NostrSigner): Event? { + return cachedInnerEvent[signer.pubKey] + } + fun cachedGossip( signer: NostrSigner, onReady: (Event) -> Unit, From d1c40efd90c2ace7c072bc73d2b6bb856686848e Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Wed, 7 Feb 2024 21:16:44 +0000 Subject: [PATCH 048/176] New Crowdin translations by GitHub Action --- app/src/main/res/values-en-rGB/strings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml index d02ba1edb..1d66bcd8f 100644 --- a/app/src/main/res/values-en-rGB/strings.xml +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -2,8 +2,14 @@ Point to the QR Code Show QR + Profile Image Scan QR Show Anyway Post was muted or reported by Event is loading or can\'t be found in your relay list + Channel Image + Referenced event not found + Could not decrypt the message + Group Picture + Explicit Content From ddfe2abb42070cd00548ffd87a901f34e5fb4045 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 8 Feb 2024 14:53:35 -0500 Subject: [PATCH 049/176] Fixes send to Top in marketplaces. --- .../com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt index c2916d3b3..9dfea373e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt @@ -270,6 +270,7 @@ fun MainScreen( videoFeedViewModel.sendToTop() } Route.Discover.base -> { + discoverMarketplaceFeedViewModel.sendToTop() discoveryLiveFeedViewModel.sendToTop() discoveryCommunityFeedViewModel.sendToTop() discoveryChatFeedViewModel.sendToTop() From 18709a1b220602a77e09f33700b2a34d97543c10 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 8 Feb 2024 14:53:53 -0500 Subject: [PATCH 050/176] Disables the notification dot in the discovery tab --- .../java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index f33b457de..95a978070 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -107,9 +107,9 @@ sealed class Route( Route( route = "Discover", icon = R.drawable.ic_sensors, - hasNewItems = { accountViewModel, newNotes -> - DiscoverLatestItem.hasNewItems(accountViewModel, newNotes) - }, + // hasNewItems = { accountViewModel, newNotes -> + // DiscoverLatestItem.hasNewItems(accountViewModel, newNotes) + // }, ) object Notification : From f97128e11c32e3d35328fa92f342df8cc3d71def Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 8 Feb 2024 17:16:54 -0500 Subject: [PATCH 051/176] Fixing accessibility issues raised on https://github.com/vitorpamplona/amethyst/issues/765 --- .../amethyst/ui/actions/NewPostView.kt | 32 +++---- .../amethyst/ui/buttons/ChannelFabColumn.kt | 2 +- .../ui/buttons/NewCommunityNoteButton.kt | 3 +- .../amethyst/ui/buttons/NewImageButton.kt | 8 +- .../amethyst/ui/buttons/NewNoteButton.kt | 3 +- .../ui/components/RobohashAsyncImage.kt | 2 +- .../amethyst/ui/navigation/AppBottomBar.kt | 10 +- .../amethyst/ui/navigation/AppTopBar.kt | 12 +-- .../amethyst/ui/navigation/Routes.kt | 10 ++ .../vitorpamplona/amethyst/ui/note/Icons.kt | 31 +++--- .../amethyst/ui/note/NoteCompose.kt | 2 +- .../amethyst/ui/note/ReactionsRow.kt | 4 +- .../amethyst/ui/note/RelayListBox.kt | 4 +- .../amethyst/ui/note/RelayListRow.kt | 10 +- .../amethyst/ui/note/UserProfilePicture.kt | 17 +++- .../amethyst/ui/note/UsernameDisplay.kt | 5 +- .../ui/screen/loggedIn/ProfileScreen.kt | 94 +++++++++++++++---- .../ui/screen/loggedOff/LoginScreen.kt | 5 +- app/src/main/res/values/strings.xml | 53 ++++++++++- 19 files changed, 228 insertions(+), 79 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index 7a9e7e45e..b2b9462ed 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -264,7 +264,7 @@ fun NewPostView( ) { Icon( painter = painterResource(R.drawable.relays), - contentDescription = null, + contentDescription = stringResource(id = R.string.relay_list_selector), modifier = Modifier.height(25.dp), tint = MaterialTheme.colorScheme.onBackground, ) @@ -1381,7 +1381,7 @@ private fun AddZapraiserButton( ) Icon( imageVector = Icons.Default.Bolt, - contentDescription = stringResource(R.string.zaps), + contentDescription = stringResource(R.string.add_zapraiser), modifier = Modifier .size(13.dp) @@ -1400,7 +1400,7 @@ private fun AddZapraiserButton( ) Icon( imageVector = Icons.Default.Bolt, - contentDescription = stringResource(R.string.zaps), + contentDescription = stringResource(R.string.cancel_zapraiser), modifier = Modifier .size(13.dp) @@ -1423,14 +1423,14 @@ fun AddGeoHash( if (!postViewModel.wantsToAddGeoHash) { Icon( imageVector = Icons.Default.LocationOff, - null, + contentDescription = stringResource(id = R.string.add_location), modifier = Modifier.size(20.dp), tint = MaterialTheme.colorScheme.onBackground, ) } else { Icon( imageVector = Icons.Default.LocationOn, - null, + contentDescription = stringResource(id = R.string.remove_location), modifier = Modifier.size(20.dp), tint = MaterialTheme.colorScheme.primary, ) @@ -1449,14 +1449,14 @@ private fun AddLnInvoiceButton( if (!isLnInvoiceActive) { Icon( imageVector = Icons.Default.CurrencyBitcoin, - null, + contentDescription = stringResource(id = R.string.add_bitcoin_invoice), modifier = Modifier.size(20.dp), tint = MaterialTheme.colorScheme.onBackground, ) } else { Icon( imageVector = Icons.Default.CurrencyBitcoin, - null, + contentDescription = stringResource(id = R.string.cancel_bitcoin_invoice), modifier = Modifier.size(20.dp), tint = BitcoinOrange, ) @@ -1480,7 +1480,7 @@ private fun ForwardZapTo( if (!postViewModel.wantsForwardZapTo) { Icon( imageVector = Icons.Default.Bolt, - contentDescription = stringResource(R.string.zaps), + contentDescription = stringResource(R.string.add_zap_split), modifier = Modifier .size(20.dp) @@ -1489,7 +1489,7 @@ private fun ForwardZapTo( ) Icon( imageVector = Icons.Default.ArrowForwardIos, - contentDescription = stringResource(R.string.zaps), + contentDescription = null, modifier = Modifier .size(13.dp) @@ -1499,7 +1499,7 @@ private fun ForwardZapTo( } else { Icon( imageVector = Icons.Outlined.Bolt, - contentDescription = stringResource(id = R.string.zaps), + contentDescription = stringResource(id = R.string.cancel_zap_split), modifier = Modifier .size(20.dp) @@ -1508,7 +1508,7 @@ private fun ForwardZapTo( ) Icon( imageVector = Icons.Outlined.ArrowForwardIos, - contentDescription = stringResource(id = R.string.zaps), + contentDescription = null, modifier = Modifier .size(13.dp) @@ -1538,7 +1538,7 @@ private fun AddClassifiedsButton( } else { Icon( imageVector = Icons.Default.Sell, - contentDescription = stringResource(id = R.string.classifieds), + contentDescription = stringResource(id = R.string.cancel_classifieds), modifier = Modifier.size(20.dp), tint = BitcoinOrange, ) @@ -1562,7 +1562,7 @@ private fun MarkAsSensitive( if (!postViewModel.wantsToMarkAsSensitive) { Icon( imageVector = Icons.Default.Visibility, - contentDescription = stringResource(R.string.content_warning), + contentDescription = stringResource(R.string.add_content_warning), modifier = Modifier .size(18.dp) @@ -1571,7 +1571,7 @@ private fun MarkAsSensitive( ) Icon( imageVector = Icons.Rounded.Warning, - contentDescription = stringResource(R.string.content_warning), + contentDescription = null, modifier = Modifier .size(10.dp) @@ -1581,7 +1581,7 @@ private fun MarkAsSensitive( } else { Icon( imageVector = Icons.Default.VisibilityOff, - contentDescription = stringResource(id = R.string.content_warning), + contentDescription = stringResource(id = R.string.remove_content_warning), modifier = Modifier .size(18.dp) @@ -1590,7 +1590,7 @@ private fun MarkAsSensitive( ) Icon( imageVector = Icons.Rounded.Warning, - contentDescription = stringResource(id = R.string.content_warning), + contentDescription = null, modifier = Modifier .size(10.dp) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/ChannelFabColumn.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/ChannelFabColumn.kt index a9a386ca2..38025fe1b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/ChannelFabColumn.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/ChannelFabColumn.kt @@ -123,7 +123,7 @@ fun ChannelFabColumn( ) { Icon( imageVector = Icons.Outlined.Add, - contentDescription = stringResource(R.string.messages_create_public_chat), + contentDescription = stringResource(R.string.messages_create_public_private_chat_desription), modifier = Modifier.size(26.dp), tint = Color.White, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewCommunityNoteButton.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewCommunityNoteButton.kt index 39d724aa4..13fd9bd13 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewCommunityNoteButton.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewCommunityNoteButton.kt @@ -33,6 +33,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note @@ -72,7 +73,7 @@ fun NewCommunityNoteButton( ) { Icon( painter = painterResource(R.drawable.ic_compose), - null, + contentDescription = stringResource(id = R.string.new_community_note), modifier = Modifier.size(26.dp), tint = Color.White, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewImageButton.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewImageButton.kt index 6ef527715..0c9c19fde 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewImageButton.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewImageButton.kt @@ -46,6 +46,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -135,7 +136,7 @@ fun NewImageButton( ) { Icon( painter = painterResource(R.drawable.ic_compose), - null, + contentDescription = stringResource(id = R.string.new_short), modifier = Modifier.size(26.dp), tint = Color.White, ) @@ -153,7 +154,10 @@ private fun ShowProgress(postViewModel: NewMediaModel) { animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec, ) .value, - modifier = Size55Modifier.clip(CircleShape).background(MaterialTheme.colorScheme.background), + modifier = + Size55Modifier + .clip(CircleShape) + .background(MaterialTheme.colorScheme.background), strokeWidth = 5.dp, ) postViewModel.uploadingDescription.value?.let { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewNoteButton.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewNoteButton.kt index d128bed2e..0c0a17cc5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewNoteButton.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewNoteButton.kt @@ -33,6 +33,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.actions.NewPostView @@ -58,7 +59,7 @@ fun NewNoteButton( ) { Icon( painter = painterResource(R.drawable.ic_compose), - null, + contentDescription = stringResource(R.string.new_post), modifier = Modifier.size(26.dp), tint = Color.White, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt index d119568cc..cae0ea2aa 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt @@ -117,7 +117,7 @@ fun RobohashFallbackAsyncImage( Image( painter = base64Painter, - contentDescription = null, + contentDescription = contentDescription, modifier = modifier, alignment = alignment, contentScale = contentScale, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt index e2f173a71..8bae02284 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt @@ -49,6 +49,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavBackStackEntry @@ -180,7 +181,7 @@ private fun NotifiableIcon( Box(route.notifSize) { Icon( painter = painterResource(id = route.icon), - contentDescription = null, + contentDescription = stringResource(route.contentDescriptor), modifier = route.iconSize, tint = if (selected) MaterialTheme.colorScheme.primary else Color.Unspecified, ) @@ -216,7 +217,12 @@ private fun NotificationDotIcon(modifier: Modifier) { color = Color.White, textAlign = TextAlign.Center, fontSize = Font12SP, - modifier = remember { Modifier.wrapContentHeight().align(Alignment.TopEnd) }, + modifier = + remember { + Modifier + .wrapContentHeight() + .align(Alignment.TopEnd) + }, ) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt index 825258813..f48443400 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt @@ -542,23 +542,17 @@ private fun LoggedInUserPictureDrawer( ) { val profilePicture by accountViewModel.account.userProfile().live().profilePictureChanges.observeAsState() - val pubkeyHex = remember { accountViewModel.userProfile().pubkeyHex } - - val automaticallyShowProfilePicture = - remember { - accountViewModel.settings.showProfilePictures.value - } IconButton( onClick = onClick, ) { RobohashFallbackAsyncImage( - robot = pubkeyHex, + robot = accountViewModel.userProfile().pubkeyHex, model = profilePicture, - contentDescription = stringResource(id = R.string.profile_image), + contentDescription = stringResource(id = R.string.your_profile_image), modifier = HeaderPictureModifier, contentScale = ContentScale.Crop, - loadProfilePicture = automaticallyShowProfilePicture, + loadProfilePicture = accountViewModel.settings.showProfilePictures.value, ) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index 95a978070..03de452d3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -60,6 +60,7 @@ sealed class Route( val icon: Int, val notifSize: Modifier = Modifier.size(Size23dp), val iconSize: Modifier = Modifier.size(Size20dp), + val contentDescriptor: Int = R.string.route, val hasNewItems: (Account, Set) -> Boolean = { _, _ -> false }, @@ -80,6 +81,7 @@ sealed class Route( }, ) .toImmutableList(), + contentDescriptor = R.string.route_home, hasNewItems = { accountViewModel, newNotes -> HomeLatestItem.hasNewItems(accountViewModel, newNotes) }, @@ -89,18 +91,21 @@ sealed class Route( Route( route = "Global", icon = R.drawable.ic_globe, + contentDescriptor = R.string.route_global, ) object Search : Route( route = "Search", icon = R.drawable.ic_search, + contentDescriptor = R.string.route_search, ) object Video : Route( route = "Video", icon = R.drawable.ic_video, + contentDescriptor = R.string.route_video, ) object Discover : @@ -110,6 +115,7 @@ sealed class Route( // hasNewItems = { accountViewModel, newNotes -> // DiscoverLatestItem.hasNewItems(accountViewModel, newNotes) // }, + contentDescriptor = R.string.route_discover, ) object Notification : @@ -119,6 +125,7 @@ sealed class Route( hasNewItems = { accountViewModel, newNotes -> NotificationLatestItem.hasNewItems(accountViewModel, newNotes) }, + contentDescriptor = R.string.route_notifications, ) object Message : @@ -128,18 +135,21 @@ sealed class Route( hasNewItems = { accountViewModel, newNotes -> MessagesLatestItem.hasNewItems(accountViewModel, newNotes) }, + contentDescriptor = R.string.route_messages, ) object BlockedUsers : Route( route = "BlockedUsers", icon = R.drawable.ic_security, + contentDescriptor = R.string.route_security_filters, ) object Bookmarks : Route( route = "Bookmarks", icon = R.drawable.ic_bookmarks, + contentDescriptor = R.string.route_home, ) object Profile : diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt index 514e11796..465551a07 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt @@ -152,7 +152,7 @@ fun LikeIcon( ) { Icon( painter = painterResource(R.drawable.ic_like), - null, + contentDescription = stringResource(id = R.string.like_description), modifier = iconSizeModifier, tint = grayTint, ) @@ -165,7 +165,7 @@ fun RepostedIcon( ) { Icon( painter = painterResource(R.drawable.ic_retweeted), - null, + contentDescription = stringResource(id = R.string.boost_or_quote_description), modifier = modifier, tint = tint, ) @@ -198,10 +198,11 @@ fun ZappedIcon(modifier: Modifier) { fun ZapIcon( modifier: Modifier, tint: Color = Color.Unspecified, + contentDescriptor: Int = R.string.zap_description, ) { Icon( imageVector = Icons.Default.Bolt, - contentDescription = stringResource(R.string.zaps), + contentDescription = stringResource(contentDescriptor), tint = tint, modifier = modifier, ) @@ -244,20 +245,26 @@ fun OpenInNewIcon( } @Composable -fun ExpandLessIcon(modifier: Modifier) { +fun ExpandLessIcon( + modifier: Modifier, + contentDescriptor: Int, +) { Icon( imageVector = Icons.Default.ExpandLess, - null, + contentDescription = stringResource(id = contentDescriptor), modifier = modifier, tint = MaterialTheme.colorScheme.subtleButton, ) } @Composable -fun ExpandMoreIcon(modifier: Modifier) { +fun ExpandMoreIcon( + modifier: Modifier, + contentDescriptor: Int, +) { Icon( imageVector = Icons.Default.ExpandMore, - null, + contentDescription = stringResource(id = contentDescriptor), modifier = modifier, tint = MaterialTheme.colorScheme.subtleButton, ) @@ -270,7 +277,7 @@ fun CommentIcon( ) { Icon( painter = painterResource(R.drawable.ic_comment), - contentDescription = null, + contentDescription = stringResource(id = R.string.reply_description), modifier = iconSizeModifier, tint = tint, ) @@ -293,7 +300,7 @@ fun ViewCountIcon( fun PollIcon() { Icon( painter = painterResource(R.drawable.ic_poll), - null, + contentDescription = stringResource(id = R.string.poll), modifier = Size20Modifier, tint = MaterialTheme.colorScheme.onBackground, ) @@ -303,7 +310,7 @@ fun PollIcon() { fun RegularPostIcon() { Icon( painter = painterResource(R.drawable.ic_lists), - null, + contentDescription = stringResource(id = R.string.disable_poll), modifier = Size20Modifier, tint = MaterialTheme.colorScheme.onBackground, ) @@ -435,10 +442,10 @@ fun LinkIcon( } @Composable -fun VerticalDotsIcon() { +fun VerticalDotsIcon(contentDescriptor: Int? = null) { Icon( imageVector = Icons.Default.MoreVert, - null, + contentDescription = contentDescriptor?.let { stringResource(id = it) }, modifier = Size18Modifier, tint = MaterialTheme.colorScheme.placeholderText, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index cad8439b5..7abbabe58 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -2658,7 +2658,7 @@ fun MoreOptionsButton( modifier = Size24Modifier, onClick = enablePopup, ) { - VerticalDotsIcon() + VerticalDotsIcon(R.string.note_options) NoteDropDownMenu( baseNote, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index dfee68fc7..e95c26965 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -379,9 +379,9 @@ private fun RenderShowIndividualReactionsButton(wantsToSeeReactions: MutableStat label = "RenderShowIndividualReactionsButton", ) { if (it) { - ExpandLessIcon(modifier = Size22Modifier) + ExpandLessIcon(modifier = Size22Modifier, R.string.close_all_reactions_to_this_post) } else { - ExpandMoreIcon(modifier = Size22Modifier) + ExpandMoreIcon(modifier = Size22Modifier, R.string.open_all_reactions_to_this_post) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt index 3fdc8e644..184a44334 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt @@ -37,6 +37,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.res.stringResource +import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer @@ -87,7 +89,7 @@ private fun ShowMoreRelaysButton(onClick: () -> Unit) { ) { Icon( imageVector = Icons.Default.ExpandMore, - null, + contentDescription = stringResource(id = R.string.expand_relay_list), modifier = ShowMoreRelaysButtonIconModifier, tint = MaterialTheme.colorScheme.placeholderText, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt index 9891828db..3a5b95c8e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt @@ -119,7 +119,7 @@ fun ChatRelayExpandButton(onClick: () -> Unit) { ) { Icon( imageVector = Icons.Default.ChevronRight, - null, + contentDescription = stringResource(id = R.string.expand_relay_list), modifier = Size15Modifier, tint = MaterialTheme.colorScheme.placeholderText, ) @@ -156,7 +156,8 @@ fun RenderRelay( val clickableModifier = remember(relay) { - Modifier.padding(1.dp) + Modifier + .padding(1.dp) .size(Size15dp) .clickable( role = Role.Button, @@ -175,18 +176,21 @@ fun RenderRelay( url, exceptionMessage, ) + Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER -> context.getString( R.string.relay_information_document_error_assemble_url, url, exceptionMessage, ) + Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT -> context.getString( R.string.relay_information_document_error_assemble_url, url, exceptionMessage, ) + Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> context.getString( R.string.relay_information_document_error_assemble_url, @@ -223,7 +227,7 @@ fun RenderRelayIcon( RobohashFallbackAsyncImage( robot = displayUrl, model = iconUrl, - contentDescription = stringResource(id = R.string.relay_icon), + contentDescription = stringResource(id = R.string.relay_info, displayUrl), colorFilter = RelayIconFilter, modifier = iconModifier, loadProfilePicture = loadProfilePicture, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index ac9efe810..04a8f29d1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -340,10 +340,11 @@ fun BaseUserPicture( val myIconSize by remember(size) { derivedStateOf { size.div(3.5f) } } Box(outerModifier, contentAlignment = Alignment.TopEnd) { - LoadUserProfilePicture(baseUser) { userProfilePicture -> + LoadUserProfilePicture(baseUser) { userProfilePicture, userName -> InnerUserPicture( userHex = baseUser.pubkeyHex, userPicture = userProfilePicture, + userName = userName, size = size, modifier = innerModifier, accountViewModel = accountViewModel, @@ -357,17 +358,18 @@ fun BaseUserPicture( @Composable fun LoadUserProfilePicture( baseUser: User, - innerContent: @Composable (String?) -> Unit, + innerContent: @Composable (String?, String?) -> Unit, ) { - val userProfile by baseUser.live().profilePictureChanges.observeAsState(baseUser.profilePicture()) + val userProfile by baseUser.live().userMetadataInfo.observeAsState(baseUser.info) - innerContent(userProfile) + innerContent(userProfile?.profilePicture(), userProfile?.bestDisplayName() ?: userProfile?.bestDisplayName()) } @Composable fun InnerUserPicture( userHex: String, userPicture: String?, + userName: String?, size: Dp, modifier: Modifier, accountViewModel: AccountViewModel, @@ -386,7 +388,12 @@ fun InnerUserPicture( RobohashFallbackAsyncImage( robot = userHex, model = userPicture, - contentDescription = stringResource(id = R.string.profile_image), + contentDescription = + if (userName != null) { + stringResource(id = R.string.profile_image_of_user, userName) + } else { + stringResource(id = R.string.profile_image) + }, modifier = myImageModifier, contentScale = ContentScale.Crop, loadProfilePicture = automaticallyShowProfilePicture, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt index 85ce58f12..48b7d5667 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt @@ -213,7 +213,10 @@ fun DrawPlayName(name: String) { @Composable fun DrawPlayNameIcon(onClick: () -> Unit) { IconButton(onClick = onClick, modifier = StdButtonSizeModifier) { - PlayIcon(modifier = StdButtonSizeModifier, tint = MaterialTheme.colorScheme.placeholderText) + PlayIcon( + modifier = StdButtonSizeModifier, + tint = MaterialTheme.colorScheme.placeholderText, + ) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index b1996662d..82ece50df 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -390,7 +390,10 @@ private fun RenderSurface( var tabsSize by remember { mutableStateOf(IntSize.Zero) } Column( - modifier = Modifier.fillMaxSize().onSizeChanged { columnSize = it }, + modifier = + Modifier + .fillMaxSize() + .onSizeChanged { columnSize = it }, ) { val coroutineScope = rememberCoroutineScope() val scrollState = rememberScrollState() @@ -403,7 +406,8 @@ private fun RenderSurface( Box( modifier = remember { - Modifier.verticalScroll(scrollState) + Modifier + .verticalScroll(scrollState) .nestedScroll( object : NestedScrollConnection { override fun onPreScroll( @@ -726,10 +730,17 @@ private fun ProfileHeader( DrawBanner(baseUser, accountViewModel) Box( - modifier = Modifier.padding(horizontal = 10.dp).size(40.dp).align(Alignment.TopEnd), + modifier = + Modifier + .padding(horizontal = 10.dp) + .size(40.dp) + .align(Alignment.TopEnd), ) { Button( - modifier = Modifier.size(30.dp).align(Alignment.Center), + modifier = + Modifier + .size(30.dp) + .align(Alignment.Center), onClick = { popupExpanded = true }, shape = ButtonBorder, colors = @@ -754,7 +765,11 @@ private fun ProfileHeader( } Column( - modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp).padding(top = 75.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 10.dp) + .padding(top = 75.dp), ) { Row( horizontalArrangement = Arrangement.SpaceBetween, @@ -789,7 +804,10 @@ private fun ProfileHeader( Spacer(Modifier.weight(1f)) Row( - modifier = Modifier.height(Size35dp).padding(bottom = 3.dp), + modifier = + Modifier + .height(Size35dp) + .padding(bottom = 3.dp), ) { MessageButton(baseUser, accountViewModel, nav) @@ -969,12 +987,15 @@ private fun DrawAdditionalInfo( ) IconButton( - modifier = Modifier.size(25.dp).padding(start = 5.dp), + modifier = + Modifier + .size(25.dp) + .padding(start = 5.dp), onClick = { clipboardManager.setText(AnnotatedString(user.pubkeyNpub())) }, ) { Icon( imageVector = Icons.Default.ContentCopy, - null, + contentDescription = stringResource(id = R.string.copy_npub_to_clipboard), modifier = Modifier.size(15.dp), tint = MaterialTheme.colorScheme.placeholderText, ) @@ -1000,7 +1021,7 @@ private fun DrawAdditionalInfo( ) { Icon( painter = painterResource(R.drawable.ic_qrcode), - null, + contentDescription = stringResource(id = R.string.show_npub_as_a_qr_code), modifier = Modifier.size(15.dp), tint = MaterialTheme.colorScheme.placeholderText, ) @@ -1059,7 +1080,10 @@ private fun DrawAdditionalInfo( text = AnnotatedString(identity.identity), onClick = { runCatching { uri.openUri(identity.toProofUrl()) } }, style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary), - modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp).weight(1f), + modifier = + Modifier + .padding(top = 1.dp, bottom = 1.dp, start = 5.dp) + .weight(1f), ) } } @@ -1131,7 +1155,10 @@ fun DisplayLNAddress( text = AnnotatedString(lud16), onClick = { zapExpanded = !zapExpanded }, style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary), - modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp).weight(1f), + modifier = + Modifier + .padding(top = 1.dp, bottom = 1.dp, start = 5.dp) + .weight(1f), ) } @@ -1223,12 +1250,21 @@ private fun WatchApp( appLogo?.let { Box( - remember { Modifier.size(Size35dp).clickable { nav("Note/${baseApp.idHex}") } }, + remember { + Modifier + .size(Size35dp) + .clickable { nav("Note/${baseApp.idHex}") } + }, ) { AsyncImage( model = appLogo, contentDescription = null, - modifier = remember { Modifier.size(Size35dp).clip(shape = CircleShape) }, + modifier = + remember { + Modifier + .size(Size35dp) + .clip(shape = CircleShape) + }, ) } } @@ -1343,7 +1379,11 @@ fun BadgeThumb( onClick: ((String) -> Unit)? = null, ) { Box( - remember { Modifier.width(size).height(size) }, + remember { + Modifier + .width(size) + .height(size) + }, ) { WatchAndRenderBadgeImage(baseNote, loadProfilePicture, size, pictureModifier, onClick) } @@ -1373,7 +1413,13 @@ private fun WatchAndRenderBadgeImage( RobohashAsyncImage( robot = "authornotfound", contentDescription = stringResource(R.string.unknown_author), - modifier = remember { pictureModifier.width(size).height(size).background(bgColor) }, + modifier = + remember { + pictureModifier + .width(size) + .height(size) + .background(bgColor) + }, ) } else { RobohashFallbackAsyncImage( @@ -1418,7 +1464,8 @@ fun DrawBanner( contentDescription = stringResource(id = R.string.profile_image), contentScale = ContentScale.FillWidth, modifier = - Modifier.fillMaxWidth() + Modifier + .fillMaxWidth() .height(125.dp) .combinedClickable( onClick = { zoomImageDialogOpen = true }, @@ -1438,7 +1485,10 @@ fun DrawBanner( painter = painterResource(R.drawable.profile_banner), contentDescription = stringResource(id = R.string.profile_banner), contentScale = ContentScale.FillWidth, - modifier = Modifier.fillMaxWidth().height(125.dp), + modifier = + Modifier + .fillMaxWidth() + .height(125.dp), ) } } @@ -1692,7 +1742,10 @@ private fun MessageButton( val scope = rememberCoroutineScope() Button( - modifier = Modifier.padding(horizontal = 3.dp).width(50.dp), + modifier = + Modifier + .padding(horizontal = 3.dp) + .width(50.dp), onClick = { scope.launch(Dispatchers.IO) { accountViewModel.createChatRoomFor(user) { nav("Room/$it") } } }, @@ -1727,7 +1780,10 @@ private fun InnerEditButtonPreview() { @Composable private fun InnerEditButton(onClick: () -> Unit) { Button( - modifier = Modifier.padding(horizontal = 3.dp).width(50.dp), + modifier = + Modifier + .padding(horizontal = 3.dp) + .width(50.dp), onClick = onClick, contentPadding = ZeroPadding, ) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt index 3fec84763..501347bf3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt @@ -312,7 +312,10 @@ fun LoginPage( IconButton(onClick = { dialogOpen = true }) { Icon( painter = painterResource(R.drawable.ic_qrcode), - null, + contentDescription = + stringResource( + R.string.login_with_qr_code, + ), modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.primary, ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index de1131a51..c8c0655a4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,7 +3,8 @@ Amethyst Debug Point to the QR Code Show QR - Profile Image + Profile Picture + Your Profile Picture Scan QR Show Anyway Post was muted or reported by @@ -523,6 +524,7 @@ Activate Public + New Public or Private Group Private To Subject @@ -543,6 +545,7 @@ When to load images Copy to clipboard + Copy npub to clipboard Copy URL to clipboard Copy Note ID to clipboard @@ -702,4 +705,52 @@ Server did not provide a URL after uploading Could not download uploaded media from the server Could not prepare local file to upload: %1$s + + Login with QR Code + Route + Home + Search + Discover + Messages + Notifications + Global + Shorts + Security Filters + + New Post + New Shorts: images or videos + New Community Note + + Open all reactions to this post + Close all reactions to this post + + Reply + Boost Or Quote + Like + Zap + + Profile Picture of %1$s + Relay %1$s + Expand relay list + Note options + Relay list selector + Poll + Disable Poll + + Bitcoin Invoice + Cancel Bitcoin Invoice + Cancel Sell an Item + + Zapraiser + Cancel Zapraiser + + Location + Remove Location + + Zap splits + Cancel Zap split + + Add content warning + Remove content warning + Show npub as a QR code From f7ab963db9f5413da72f9541b84545f5d842ab0f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 9 Feb 2024 14:37:49 -0500 Subject: [PATCH 052/176] Small fixes in the use of modifiers --- .../com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt | 3 ++- .../vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt | 6 ++++-- .../amethyst/ui/screen/loggedIn/ProfileScreen.kt | 8 +++++--- .../java/com/vitorpamplona/amethyst/ui/theme/Shape.kt | 1 + 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt index a8cc82304..01b80009c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt @@ -57,6 +57,7 @@ import com.vitorpamplona.amethyst.ui.navigation.routeFor import com.vitorpamplona.amethyst.ui.screen.BadgeCard import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.ui.theme.Size15Modifier import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor import com.vitorpamplona.amethyst.ui.theme.placeholderText import kotlinx.coroutines.launch @@ -162,7 +163,7 @@ fun BadgeCompose( Icon( imageVector = Icons.Default.MoreVert, null, - modifier = Modifier.size(15.dp), + modifier = Size15Modifier, tint = MaterialTheme.colorScheme.placeholderText, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index 84eec451d..91e90d159 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -128,6 +128,8 @@ import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.EditFieldBorder import com.vitorpamplona.amethyst.ui.theme.EditFieldTrailingIconModifier import com.vitorpamplona.amethyst.ui.theme.FeedPadding +import com.vitorpamplona.amethyst.ui.theme.Size15Modifier +import com.vitorpamplona.amethyst.ui.theme.Size24Modifier import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.lessImportantLink import com.vitorpamplona.amethyst.ui.theme.placeholderText @@ -396,13 +398,13 @@ fun NoteMaster( ) IconButton( - modifier = Modifier.then(Modifier.size(24.dp)), + modifier = Size24Modifier, onClick = enablePopup, ) { Icon( imageVector = Icons.Default.MoreVert, null, - modifier = Modifier.size(15.dp), + modifier = Size15Modifier, tint = MaterialTheme.colorScheme.placeholderText, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index 82ece50df..6899e929b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -155,7 +155,9 @@ import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.ButtonPadding import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.ui.theme.Size15Modifier import com.vitorpamplona.amethyst.ui.theme.Size16Modifier +import com.vitorpamplona.amethyst.ui.theme.Size25Modifier import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.ZeroPadding import com.vitorpamplona.amethyst.ui.theme.placeholderText @@ -996,7 +998,7 @@ private fun DrawAdditionalInfo( Icon( imageVector = Icons.Default.ContentCopy, contentDescription = stringResource(id = R.string.copy_npub_to_clipboard), - modifier = Modifier.size(15.dp), + modifier = Size15Modifier, tint = MaterialTheme.colorScheme.placeholderText, ) } @@ -1016,13 +1018,13 @@ private fun DrawAdditionalInfo( } IconButton( - modifier = Modifier.size(25.dp), + modifier = Size25Modifier, onClick = { dialogOpen = true }, ) { Icon( painter = painterResource(R.drawable.ic_qrcode), contentDescription = stringResource(id = R.string.show_npub_as_a_qr_code), - modifier = Modifier.size(15.dp), + modifier = Size15Modifier, tint = MaterialTheme.colorScheme.placeholderText, ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index 67f1cb8ac..1f8ba22ff 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -116,6 +116,7 @@ val Size19Modifier = Modifier.size(19.dp) val Size20Modifier = Modifier.size(20.dp) val Size22Modifier = Modifier.size(22.dp) val Size24Modifier = Modifier.size(24.dp) +val Size25Modifier = Modifier.size(25.dp) val Size26Modifier = Modifier.size(26.dp) val Size30Modifier = Modifier.size(30.dp) val Size35Modifier = Modifier.size(35.dp) From 72503b90617e465c9556f2e6846508812050e069 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 9 Feb 2024 15:54:56 -0500 Subject: [PATCH 053/176] Adds community and public chat list events from nip51 --- .../amethyst/model/LocalCache.kt | 18 ++ .../quartz/events/ChannelListEvent.kt | 274 +++++++++++++++++ .../quartz/events/CommunityListEvent.kt | 279 ++++++++++++++++++ .../quartz/events/EventFactory.kt | 5 +- 4 files changed, 574 insertions(+), 2 deletions(-) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelListEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/CommunityListEvent.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 7c530058f..499dd5ef4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -50,6 +50,7 @@ import com.vitorpamplona.quartz.events.CalendarRSVPEvent import com.vitorpamplona.quartz.events.CalendarTimeSlotEvent import com.vitorpamplona.quartz.events.ChannelCreateEvent import com.vitorpamplona.quartz.events.ChannelHideMessageEvent +import com.vitorpamplona.quartz.events.ChannelListEvent import com.vitorpamplona.quartz.events.ChannelMessageEvent import com.vitorpamplona.quartz.events.ChannelMetadataEvent import com.vitorpamplona.quartz.events.ChannelMuteUserEvent @@ -57,6 +58,7 @@ import com.vitorpamplona.quartz.events.ChatMessageEvent import com.vitorpamplona.quartz.events.ChatroomKey import com.vitorpamplona.quartz.events.ClassifiedsEvent import com.vitorpamplona.quartz.events.CommunityDefinitionEvent +import com.vitorpamplona.quartz.events.CommunityListEvent import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent import com.vitorpamplona.quartz.events.ContactListEvent import com.vitorpamplona.quartz.events.DeletionEvent @@ -463,6 +465,20 @@ object LocalCache { consumeBaseReplaceable(event, relay) } + fun consume( + event: CommunityListEvent, + relay: Relay?, + ) { + consumeBaseReplaceable(event, relay) + } + + fun consume( + event: ChannelListEvent, + relay: Relay?, + ) { + consumeBaseReplaceable(event, relay) + } + fun consume( event: FileServersEvent, relay: Relay?, @@ -1772,6 +1788,7 @@ object LocalCache { is CalendarTimeSlotEvent -> consume(event, relay) is CalendarRSVPEvent -> consume(event, relay) is ChannelCreateEvent -> consume(event) + is ChannelListEvent -> consume(event, relay) is ChannelHideMessageEvent -> consume(event) is ChannelMessageEvent -> consume(event, relay) is ChannelMetadataEvent -> consume(event) @@ -1779,6 +1796,7 @@ object LocalCache { is ChatMessageEvent -> consume(event, relay) is ClassifiedsEvent -> consume(event, relay) is CommunityDefinitionEvent -> consume(event, relay) + is CommunityListEvent -> consume(event, relay) is CommunityPostApprovalEvent -> { event.containedPost()?.let { verifyAndConsume(it, relay) } consume(event) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelListEvent.kt new file mode 100644 index 000000000..49e7f1191 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelListEvent.kt @@ -0,0 +1,274 @@ +/** + * Copyright (c) 2023 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.events + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlinx.collections.immutable.ImmutableSet + +@Immutable +class ChannelListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : GeneralListEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + @Transient var publicAndPrivateEventCache: ImmutableSet? = null + + override fun dTag() = FIXED_D_TAG + + fun publicAndPrivateEvents( + signer: NostrSigner, + onReady: (ImmutableSet) -> Unit, + ) { + publicAndPrivateEventCache?.let { eventList -> + onReady(eventList) + return + } + + privateTagsOrEmpty(signer) { + publicAndPrivateEventCache = filterTagList("e", it) + + publicAndPrivateEventCache?.let { eventList -> + onReady(eventList) + } + } + } + + companion object { + const val KIND = 10005 + const val FIXED_D_TAG = "" + const val ALT = "Public Chat List" + + fun blockListFor(pubKeyHex: HexKey): String { + return "$KIND:$pubKeyHex:" + } + + fun createListWithTag( + key: String, + tag: String, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (ChannelListEvent) -> Unit, + ) { + if (isPrivate) { + encryptTags(arrayOf(arrayOf(key, tag)), signer) { encryptedTags -> + create( + content = encryptedTags, + tags = emptyArray(), + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) + } + } else { + create( + content = "", + tags = arrayOf(arrayOf(key, tag)), + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) + } + } + + fun createListWithEvent( + eventId: HexKey, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (ChannelListEvent) -> Unit, + ) { + return createListWithTag("e", eventId, isPrivate, signer, createdAt, onReady) + } + + fun addEvents( + earlierVersion: ChannelListEvent, + listEvents: List, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (ChannelListEvent) -> Unit, + ) { + if (isPrivate) { + earlierVersion.privateTagsOrEmpty(signer) { privateTags -> + encryptTags( + privateTags = + privateTags.plus( + listEvents.map { arrayOf("e", it) }, + ), + signer = signer, + ) { encryptedTags -> + create( + content = encryptedTags, + tags = earlierVersion.tags, + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) + } + } + } else { + create( + content = earlierVersion.content, + tags = + earlierVersion.tags.plus( + listEvents.map { arrayOf("e", it) }, + ), + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) + } + } + + fun addEvent( + earlierVersion: ChannelListEvent, + event: HexKey, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (ChannelListEvent) -> Unit, + ) { + return addTag(earlierVersion, "e", event, isPrivate, signer, createdAt, onReady) + } + + fun addTag( + earlierVersion: ChannelListEvent, + key: String, + tag: String, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (ChannelListEvent) -> Unit, + ) { + earlierVersion.isTagged(key, tag, isPrivate, signer) { isTagged -> + if (!isTagged) { + if (isPrivate) { + earlierVersion.privateTagsOrEmpty(signer) { privateTags -> + encryptTags( + privateTags = privateTags.plus(element = arrayOf(key, tag)), + signer = signer, + ) { encryptedTags -> + create( + content = encryptedTags, + tags = earlierVersion.tags, + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) + } + } + } else { + create( + content = earlierVersion.content, + tags = earlierVersion.tags.plus(element = arrayOf(key, tag)), + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) + } + } + } + } + + fun removeEvent( + earlierVersion: ChannelListEvent, + event: HexKey, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (ChannelListEvent) -> Unit, + ) { + return removeTag(earlierVersion, "e", event, isPrivate, signer, createdAt, onReady) + } + + fun removeTag( + earlierVersion: ChannelListEvent, + key: String, + tag: String, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (ChannelListEvent) -> Unit, + ) { + earlierVersion.isTagged(key, tag, isPrivate, signer) { isTagged -> + if (isTagged) { + if (isPrivate) { + earlierVersion.privateTagsOrEmpty(signer) { privateTags -> + encryptTags( + privateTags = + privateTags + .filter { it.size > 1 && !(it[0] == key && it[1] == tag) } + .toTypedArray(), + signer = signer, + ) { encryptedTags -> + create( + content = encryptedTags, + tags = + earlierVersion.tags + .filter { it.size > 1 && !(it[0] == key && it[1] == tag) } + .toTypedArray(), + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) + } + } + } else { + create( + content = earlierVersion.content, + tags = + earlierVersion.tags + .filter { it.size > 1 && !(it[0] == key && it[1] == tag) } + .toTypedArray(), + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) + } + } + } + } + + fun create( + content: String, + tags: Array>, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (ChannelListEvent) -> Unit, + ) { + val newTags = + if (tags.any { it.size > 1 && it[0] == "alt" }) { + tags + } else { + tags + arrayOf("alt", ALT) + } + + signer.sign(createdAt, KIND, newTags, content, onReady) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/CommunityListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/CommunityListEvent.kt new file mode 100644 index 000000000..cb26353b5 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/CommunityListEvent.kt @@ -0,0 +1,279 @@ +/** + * Copyright (c) 2023 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.events + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.encoders.ATag +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.toImmutableSet + +@Immutable +class CommunityListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : GeneralListEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + @Transient var publicAndPrivateEventCache: ImmutableSet? = null + + override fun dTag() = FIXED_D_TAG + + fun publicAndPrivateEvents( + signer: NostrSigner, + onReady: (ImmutableSet) -> Unit, + ) { + publicAndPrivateEventCache?.let { eventList -> + onReady(eventList) + return + } + + privateTagsOrEmpty(signer) { + publicAndPrivateEventCache = + filterTagList("a", it) + .mapNotNull { ATag.parseAtag(it, null) } + .toImmutableSet() + + publicAndPrivateEventCache?.let { eventList -> + onReady(eventList) + } + } + } + + companion object { + const val KIND = 10004 + const val FIXED_D_TAG = "" + const val ALT = "Community List" + + fun blockListFor(pubKeyHex: HexKey): String { + return "$KIND:$pubKeyHex:" + } + + fun createListWithTag( + key: String, + tag: String, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (CommunityListEvent) -> Unit, + ) { + if (isPrivate) { + encryptTags(arrayOf(arrayOf(key, tag)), signer) { encryptedTags -> + create( + content = encryptedTags, + tags = emptyArray(), + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) + } + } else { + create( + content = "", + tags = arrayOf(arrayOf(key, tag)), + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) + } + } + + fun createListWithEvent( + address: ATag, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (CommunityListEvent) -> Unit, + ) { + return createListWithTag("a", address.toTag(), isPrivate, signer, createdAt, onReady) + } + + fun addEvents( + earlierVersion: CommunityListEvent, + listAddresses: List, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (CommunityListEvent) -> Unit, + ) { + if (isPrivate) { + earlierVersion.privateTagsOrEmpty(signer) { privateTags -> + encryptTags( + privateTags = + privateTags.plus( + listAddresses.map { arrayOf("a", it.toTag()) }, + ), + signer = signer, + ) { encryptedTags -> + create( + content = encryptedTags, + tags = earlierVersion.tags, + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) + } + } + } else { + create( + content = earlierVersion.content, + tags = + earlierVersion.tags.plus( + listAddresses.map { arrayOf("a", it.toTag()) }, + ), + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) + } + } + + fun addEvent( + earlierVersion: CommunityListEvent, + address: ATag, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (CommunityListEvent) -> Unit, + ) { + return addTag(earlierVersion, "a", address.toTag(), isPrivate, signer, createdAt, onReady) + } + + fun addTag( + earlierVersion: CommunityListEvent, + key: String, + tag: String, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (CommunityListEvent) -> Unit, + ) { + earlierVersion.isTagged(key, tag, isPrivate, signer) { isTagged -> + if (!isTagged) { + if (isPrivate) { + earlierVersion.privateTagsOrEmpty(signer) { privateTags -> + encryptTags( + privateTags = privateTags.plus(element = arrayOf(key, tag)), + signer = signer, + ) { encryptedTags -> + create( + content = encryptedTags, + tags = earlierVersion.tags, + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) + } + } + } else { + create( + content = earlierVersion.content, + tags = earlierVersion.tags.plus(element = arrayOf(key, tag)), + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) + } + } + } + } + + fun removeEvent( + earlierVersion: CommunityListEvent, + address: ATag, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (CommunityListEvent) -> Unit, + ) { + return removeTag(earlierVersion, "a", address.toTag(), isPrivate, signer, createdAt, onReady) + } + + fun removeTag( + earlierVersion: CommunityListEvent, + key: String, + tag: String, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (CommunityListEvent) -> Unit, + ) { + earlierVersion.isTagged(key, tag, isPrivate, signer) { isTagged -> + if (isTagged) { + if (isPrivate) { + earlierVersion.privateTagsOrEmpty(signer) { privateTags -> + encryptTags( + privateTags = + privateTags + .filter { it.size > 1 && !(it[0] == key && it[1] == tag) } + .toTypedArray(), + signer = signer, + ) { encryptedTags -> + create( + content = encryptedTags, + tags = + earlierVersion.tags + .filter { it.size > 1 && !(it[0] == key && it[1] == tag) } + .toTypedArray(), + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) + } + } + } else { + create( + content = earlierVersion.content, + tags = + earlierVersion.tags + .filter { it.size > 1 && !(it[0] == key && it[1] == tag) } + .toTypedArray(), + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) + } + } + } + } + + fun create( + content: String, + tags: Array>, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (CommunityListEvent) -> Unit, + ) { + val newTags = + if (tags.any { it.size > 1 && it[0] == "alt" }) { + tags + } else { + tags + arrayOf("alt", ALT) + } + + signer.sign(createdAt, KIND, newTags, content, onReady) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt index 6c29e297b..3e5e3b5dd 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt @@ -53,14 +53,14 @@ class EventFactory { ChannelCreateEvent.KIND -> ChannelCreateEvent(id, pubKey, createdAt, tags, content, sig) ChannelHideMessageEvent.KIND -> ChannelHideMessageEvent(id, pubKey, createdAt, tags, content, sig) + ChannelListEvent.KIND -> ChannelListEvent(id, pubKey, createdAt, tags, content, sig) ChannelMessageEvent.KIND -> ChannelMessageEvent(id, pubKey, createdAt, tags, content, sig) ChannelMetadataEvent.KIND -> ChannelMetadataEvent(id, pubKey, createdAt, tags, content, sig) ChannelMuteUserEvent.KIND -> ChannelMuteUserEvent(id, pubKey, createdAt, tags, content, sig) ChatMessageEvent.KIND -> { if (id.isBlank()) { - val newId = Event.generateId(pubKey, createdAt, kind, tags, content).toHexKey() ChatMessageEvent( - newId, + Event.generateId(pubKey, createdAt, kind, tags, content).toHexKey(), pubKey, createdAt, tags, @@ -74,6 +74,7 @@ class EventFactory { ClassifiedsEvent.KIND -> ClassifiedsEvent(id, pubKey, createdAt, tags, content, sig) CommunityDefinitionEvent.KIND -> CommunityDefinitionEvent(id, pubKey, createdAt, tags, content, sig) + CommunityListEvent.KIND -> CommunityListEvent(id, pubKey, createdAt, tags, content, sig) CommunityPostApprovalEvent.KIND -> CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig) ContactListEvent.KIND -> ContactListEvent(id, pubKey, createdAt, tags, content, sig) From 04fe439e0b966694437a2c81663f91858ff0c20e Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Sat, 10 Feb 2024 09:45:17 +0000 Subject: [PATCH 054/176] Updated translations. DE, SV, PT --- app/src/main/res/values-de/strings.xml | 52 ++++++++++++++++++++++ app/src/main/res/values-pt-rBR/strings.xml | 52 ++++++++++++++++++++++ app/src/main/res/values-sv-rSE/strings.xml | 52 ++++++++++++++++++++++ 3 files changed, 156 insertions(+) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 6a3c3bc45..b92135ea7 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -601,4 +601,56 @@ anz der Bedingungen ist erforderlich Der Server hat nach dem Hochladen keine URL angegeben Hochgeladene Medien konnten nicht vom Server heruntergeladen werden Lokale Datei konnte nicht zum Hochladen vorbereitet werden: %1$s + + Einloggen mit QR-Code + Route + Startseite + Suche + Entdecken + Nachrichten + Benachrichtigungen + Global + Kurzfilme + Sicherheitsfilter + + Neuer Beitrag + Neue Kurzfilme: Bilder oder Videos + Neue Community-Notiz + + Alle Reaktionen auf diesen Beitrag öffnen + Alle Reaktionen auf diesen Beitrag schließen + + Antworten + Boosten oder Zitieren + Gefällt mir + Zap + + Profilbild von %1$s + %1$s weiterleiten + Rela-Liste erweitern + Notizoptionen + Rela-Listenauswahl + Umfrage + Umfrage deaktivieren + + Bitcoin-Rechnung hinzufügen + Bitcoin-Rechnung abbrechen + Verkauf eines Artikels abbrechen + + Zapraiser hinzufügen + Zapraiser abbrechen + + Ort hinzufügen + Ort entfernen + + Zap-Aufteilung hinzufügen + Zap-Aufteilung abbrechen + + Inhaltswarnung hinzufügen + Inhaltswarnung entfernen + Npub als QR-Code anzeigen + + Dein Profilbild + Neue öffentliche oder private Gruppe + Npub in Zwischenablage kopieren diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index d9dd7d7f6..1fa812452 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -597,4 +597,56 @@ O servidor não forneceu uma URL após o upload Não foi possível baixar o arquivo de mídia carregado do servidor Não foi possível preparar o arquivo local para enviar: %1$s + + Entrar com Código QR + Rota + Início + Buscar + Descobrir + Mensagens + Notificações + Global + Vídeos Curtos + Filtros de Segurança + + Novo Post + Novos Vídeos Curtos: imagens ou vídeos + Nova Nota da Comunidade + + Abrir todas as reações a esta postagem + Fechar todas as reações a esta postagem + + Responder + Impulsionar ou Citar + Gostar + Zap + + Foto de perfil de %1$s + Repassar %1$s + Expandir lista de repasse + Opções de nota + Seletor de lista de repasse + Enquete + Desativar enquete + + Fatura de Bitcoin + Cancelar Fatura de Bitcoin + Cancelar Venda de Item + + Avaliador Zap + Cancelar Avaliador Zap + + Localização + Remover Localização + + Divisão Zap + Cancelar Divisão Zap + + Adicionar aviso de conteúdo + Remover aviso de conteúdo + Mostrar npub como um código QR + + Sua Foto de Perfil + Novo Grupo Público ou Privado + Copiar npub para a área de transferência diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index e90c19e84..4331160b2 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -595,4 +595,56 @@ Servern gav inte en URL efter uppladdning Kunde inte ladda ner uppladdade medier från servern Kunde inte förbereda lokal fil att ladda upp: %1$s + + Logga in med QR-kod + Rutt + Hem + Sök + Upptäck + Meddelanden + Aviseringar + Global + Kortfilmer + Säkerhetsfilter + + Nytt inlägg + Nya kort: bilder eller videor + Nytt Community-meddelande + + Öppna alla reaktioner på detta inlägg + Stäng alla reaktioner på detta inlägg + + Svara + Boosta eller citera + Gilla + Zap + + %1$s Profilbild + Relä %1$s + Expandera relälistan + Notalternativ + Relälistväljare + Omröstning + Inaktivera omröstning + + Bitcoin-faktura + Avbryt Bitcoin-faktura + Avbryt Sälja en artikel + + Zappraiser + Avbryt Zappraiser + + Plats + Ta bort plats + + Zap-split + Avbryt Zap-split + + Lägg till varning för innehåll + Ta bort varning för innehåll + Visa npub som en QR-kod + + Din profilbild + Ny offentlig eller privat grupp + Kopiera npub till urklipp From 88662a4e5585ae3d7c4389be5481ef102d420ea2 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Sat, 10 Feb 2024 13:27:57 +0000 Subject: [PATCH 055/176] New Crowdin translations by GitHub Action --- app/src/main/res/values-cs/strings.xml | 41 +++++++++++++++++++ app/src/main/res/values-de/strings.xml | 20 ++-------- app/src/main/res/values-fr/strings.xml | 46 ++++++++++++++++++++++ app/src/main/res/values-pt-rBR/strings.xml | 19 ++------- app/src/main/res/values-sv-rSE/strings.xml | 19 ++------- 5 files changed, 96 insertions(+), 49 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index fb3ace03e..dccb96054 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -3,6 +3,7 @@ Nasměrujte na QR kód Zobrazit QR kód Profilový obrázek + Profilový obrázek Skenovat QR kód Zobrazit přesto Příspěvek byl označen jako nevhodný uživatelem @@ -453,6 +454,7 @@ Aktivace tohoto režimu vyžaduje od Amethystu odeslání zprávy NIP-24 (GiftWrapped, Zapečetěné přímé a skupinové zprávy). NIP-24 je nový a většina klientů ho zatím neimplementovala. Ujistěte se, že příjemce používá kompatibilního klienta. Aktivovat Veřejné + Nová veřejná nebo soukromá skupina Soukromé Pro Předmět @@ -469,6 +471,7 @@ Zobrazit náhledy URL Kdy načíst obrázek Kopírovat do schránky + Kopírovat npub do schránky Kopírovat URL do schránky Kopírovat ID poznámky do schránky Vytvořeno @@ -597,4 +600,42 @@ Server po nahrání neposkytl URL Nepodařilo se stáhnout nahraná média ze serveru Nelze připravit místní soubor k nahrání: %1$s + Přihlášení pomocí QR kódu + Trasa + Domů + Hledat + Objevit + Zprávy + Upozornění + Globální + Krátké + Bezpečnostní filtry + Nový příspěvek + Nové Shorts: obrázky nebo videa + Nová poznámka komunity + Otevřít všechny reakce pro tento příspěvek + Zavřít všechny reakce na tento příspěvek + Odpověď + Zvýšit nebo citovat + Olajkovat + Zap + Obrázek profilu %1$s + Relé %1$s + Rozbalit seznam relací + Volby poznámky + Výběr seznamu relé + Anketa + Vypnout anketu + Bitcoinová faktura + Zrušit Bitcoinovou fakturu + Zrušit prodej položky + Zap-sběr + Zrušit Zap-zběrku + Místo + Odstranit umístění + Zap splits + Zrušit rozdělení Zap + Přidat upozornění na obsah + Odstranit upozornění na obsah + Zobrazit npub jako QR kód diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index b92135ea7..476f34ae7 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -3,6 +3,7 @@ Zeigen Sie auf den QR Code QR Code anzeigen Profilbild + Dein Profilbild QR Code scannen Trotzdem anzeigen Der Beitrag wurde als unangemessen gekennzeichnet von @@ -457,6 +458,7 @@ anz der Bedingungen ist erforderlich Um diesen Modus zu aktivieren, muss Amethyst eine NIP-24-Nachricht senden (GiftWrapped, Versiegelte Direkt- und Gruppennachrichten). NIP-24 ist neu und die meisten Clients haben es noch nicht implementiert. Stellen Sie sicher, dass der Empfänger einen kompatiblen Client verwendet. Aktivieren Öffentlich + Neue öffentliche oder private Gruppe Privat An Betreff @@ -473,6 +475,7 @@ anz der Bedingungen ist erforderlich URL-Vorschauen anzeigen Wann Bilder geladen werden sollen In Zwischenablage kopieren + Npub in Zwischenablage kopieren URL in die Zwischenablage kopieren Notiz-ID in die Zwischenablage kopieren Erstellt am @@ -601,30 +604,22 @@ anz der Bedingungen ist erforderlich Der Server hat nach dem Hochladen keine URL angegeben Hochgeladene Medien konnten nicht vom Server heruntergeladen werden Lokale Datei konnte nicht zum Hochladen vorbereitet werden: %1$s - Einloggen mit QR-Code - Route Startseite Suche Entdecken Nachrichten Benachrichtigungen - Global Kurzfilme Sicherheitsfilter - Neuer Beitrag Neue Kurzfilme: Bilder oder Videos Neue Community-Notiz - Alle Reaktionen auf diesen Beitrag öffnen Alle Reaktionen auf diesen Beitrag schließen - Antworten Boosten oder Zitieren Gefällt mir - Zap - Profilbild von %1$s %1$s weiterleiten Rela-Liste erweitern @@ -632,25 +627,16 @@ anz der Bedingungen ist erforderlich Rela-Listenauswahl Umfrage Umfrage deaktivieren - Bitcoin-Rechnung hinzufügen Bitcoin-Rechnung abbrechen Verkauf eines Artikels abbrechen - Zapraiser hinzufügen Zapraiser abbrechen - Ort hinzufügen Ort entfernen - Zap-Aufteilung hinzufügen Zap-Aufteilung abbrechen - Inhaltswarnung hinzufügen Inhaltswarnung entfernen Npub als QR-Code anzeigen - - Dein Profilbild - Neue öffentliche oder private Gruppe - Npub in Zwischenablage kopieren diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index a7aa7fb4b..78f2ca027 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -3,6 +3,7 @@ Pointer vers le QR code Montrer le QR code Image de profil + Votre Photo de Profil Scanner le QR code Montrer quand même Post signalé inapproprié par @@ -90,6 +91,7 @@ Ajouter un relais Nom visible du public Mon nom visible du public + Ostrich McGénial Bienvenue Ostrich! Nom d\'utilisateur Mon nom d\'utilisateur @@ -452,6 +454,7 @@ Pour activer ce mode, Amethyst doit envoyer un message NIP-24 (GiftWrapped, Sealed Direct et Group Messages). Le protocole NIP-24 est nouveau et la plupart des clients ne l\'ont pas encore mis en oeuvre. Assurez-vous que le destinataire utilise un client compatible. Activer Public + Nouveau Groupe Public ou Privé Privé À Sujet @@ -468,6 +471,7 @@ Afficher la prévisualisation d\'URL Quand charger les images Copier dans le presse-papiers + Copier npub dans le presse-papiers Copier l\'URL dans le presse-papiers Copier l\'ID de la note dans le presse-papiers Créé le @@ -516,7 +520,10 @@ Impossible de payer la facture Impossible de retirer Impossible de configurer Wallet Connect + Erreur lors de l\'analyse de l\'événement de connexion NIP-47. Vérifiez si tout est correct avec le prestataire de votre Portefeuille : %1$s. Erreur : %2$s + Erreur lors de l\'analyse de l\'événement de connexion NIP-47. Vérifiez si tout est correct avec le prestataire de votre Portefeuille : %1$s. Impossible d\'utiliser ce Cashu + Le Mint a fourni le message d\'erreur suivant : %1$s Les jetons de cashu sont déjà dépensés. Cashu reçu %1$s sats ont été envoyés à votre portefeuille. (Frais : %2$s sats) @@ -528,6 +535,7 @@ Impossible d\'assembler LNUrl à partir de l\'adresse Lightning \"%1$s\". Vérifiez la configuration de l\'utilisateur Le service lightning du récepteur à %1$s n\'est pas disponible. Il a été calculé à partir de l\'adresse lightning \"%2$s\". Erreur: %3$s. Vérifiez si le serveur est en ligne et si l\'adresse lightning est correcte Impossible de résoudre %1$s. Vérifiez si vous êtes connecté, si le serveur est en ligne et si l\'adresse lightning %2$s est correcte + Impossible de résoudre %1$s. Vérifiez si vous êtes connecté, si le serveur est actif et si l\'adresse Lightning %2$s est correcte.\n\nL\'erreur était : %3$s Impossible de récupérer la facture depuis %1$s Erreur lors de l\'analyse du JSON à partir de l\'Adresse Lightning. Vérifiez la configuration lightning de l\'utilisateur URL de callback introuvable dans la configuration serveur de l\'adresse lightning de l\'utilisateur @@ -592,4 +600,42 @@ Le serveur n\'a pas fourni d\'URL après le téléversement Impossible de télécharger le média depuis le serveur Impossible de préparer le fichier local à téléverser: %1$s + Se connecter avec un QR Code + Route + Accueil + Rechercher + Découvrir + Messages + Notifications + Général + Shorts + Filtres de Sécurité + Nouveau Message + Nouveaux Shorts : images ou vidéos + Nouvelle Note Communautaire + Ouvrir toutes les réactions à ce message + Fermer toutes les réactions à ce message + Répondre + Booster ou Citer + J\'aime + Zap + Photo de Profil de %1$s + Relais %1$s + Développer la liste des relais + Options de la note + Sélecteur de liste de relais + Sondage + Désactiver le Sondage + Facture Bitcoin + Annuler la Facture Bitcoin + Annuler la Vente d\'un Objet + Collecte de Zaps + Annuler la Collecte de Zaps + Emplacement + Retirer l\'Emplacement + Partage des Zaps + Annuler le partage des Zaps + Ajouter un avertissement de contenu + Retirer l\'avertissement de contenu + Afficher npub en tant que QR code diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 1fa812452..c361dd760 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -3,6 +3,7 @@ Aponte para o código QR Mostrar QR Imagem de perfil + Sua Foto de Perfil Escanear QR Mostrar de qualquer maneira A postagem foi sinalizada como imprópria por @@ -453,6 +454,7 @@ Ativando este modo requer o Amethyst para enviar uma mensagem de NIP-24 (GiftWrapped, Sealed Direct and Group Messages). NIP-24 é novo e a maioria dos clientes ainda não o implementaram. Certifique-se de que o destinatário está usando um cliente compatível. Ativar Público + Novo Grupo Público ou Privado Privado Para Assunto @@ -469,6 +471,7 @@ Mostrar visualizações de URL Quando carregar imagens Copiar para a Área de Transferência + Copiar npub para a área de transferência Copiar URL para a área de transferência Copiar ID da nota para a área de transferência Criado em @@ -597,7 +600,6 @@ O servidor não forneceu uma URL após o upload Não foi possível baixar o arquivo de mídia carregado do servidor Não foi possível preparar o arquivo local para enviar: %1$s - Entrar com Código QR Rota Início @@ -605,22 +607,16 @@ Descobrir Mensagens Notificações - Global Vídeos Curtos Filtros de Segurança - Novo Post Novos Vídeos Curtos: imagens ou vídeos Nova Nota da Comunidade - Abrir todas as reações a esta postagem Fechar todas as reações a esta postagem - Responder Impulsionar ou Citar Gostar - Zap - Foto de perfil de %1$s Repassar %1$s Expandir lista de repasse @@ -628,25 +624,16 @@ Seletor de lista de repasse Enquete Desativar enquete - Fatura de Bitcoin Cancelar Fatura de Bitcoin Cancelar Venda de Item - Avaliador Zap Cancelar Avaliador Zap - Localização Remover Localização - Divisão Zap Cancelar Divisão Zap - Adicionar aviso de conteúdo Remover aviso de conteúdo Mostrar npub como um código QR - - Sua Foto de Perfil - Novo Grupo Público ou Privado - Copiar npub para a área de transferência diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 4331160b2..3a589b300 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -3,6 +3,7 @@ Peka mot QR-koden Visa QR Profilbild + Din profilbild Scanna QR Visa ändå Inlägget flaggades som olämpligt av @@ -451,6 +452,7 @@ För att aktivera denna funktion kräver det att Amethyst skickar ett NIP-24 meddelande (GiftWrapped, Förseglade Direkta och Gruppmeddelanden). NIP-24 är nytt och de flesta klienter har ännu inte implementerat det. Se till att mottagaren använder en kompatibel klient. Aktivera Publik + Ny offentlig eller privat grupp Privat Till Ämne @@ -467,6 +469,7 @@ Visa förhandsgranskning av URL När bilder ska laddas Kopiera till Urklipp + Kopiera npub till urklipp Kopiera URL till urklipp Kopiera anteckningens ID till urklipp Skapad den @@ -595,7 +598,6 @@ Servern gav inte en URL efter uppladdning Kunde inte ladda ner uppladdade medier från servern Kunde inte förbereda lokal fil att ladda upp: %1$s - Logga in med QR-kod Rutt Hem @@ -603,22 +605,16 @@ Upptäck Meddelanden Aviseringar - Global Kortfilmer Säkerhetsfilter - Nytt inlägg Nya kort: bilder eller videor Nytt Community-meddelande - Öppna alla reaktioner på detta inlägg Stäng alla reaktioner på detta inlägg - Svara Boosta eller citera Gilla - Zap - %1$s Profilbild Relä %1$s Expandera relälistan @@ -626,25 +622,16 @@ Relälistväljare Omröstning Inaktivera omröstning - Bitcoin-faktura Avbryt Bitcoin-faktura Avbryt Sälja en artikel - Zappraiser Avbryt Zappraiser - Plats Ta bort plats - Zap-split Avbryt Zap-split - Lägg till varning för innehåll Ta bort varning för innehåll Visa npub som en QR-kod - - Din profilbild - Ny offentlig eller privat grupp - Kopiera npub till urklipp From 27248bff17adef967149816142a06fb1f0717850 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 10 Feb 2024 09:59:01 -0500 Subject: [PATCH 056/176] Invert the order of the hidden users in the screen. --- .../vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt index ed6f508df..b05cce59a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt @@ -34,7 +34,7 @@ class HiddenAccountsFeedFilter(val account: Account) : FeedFilter() { } override fun feed(): List { - return account.flowHiddenUsers.value.hiddenUsers.map { LocalCache.getOrCreateUser(it) } + return account.flowHiddenUsers.value.hiddenUsers.reversed().map { LocalCache.getOrCreateUser(it) } } } From 84d4d68116161e0bc3b937ca9e2bb42f22aff4c5 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Sat, 10 Feb 2024 15:00:43 +0000 Subject: [PATCH 057/176] New Crowdin translations by GitHub Action --- app/src/main/res/values-de/strings.xml | 3 +++ app/src/main/res/values-pt-rBR/strings.xml | 2 ++ app/src/main/res/values-sv-rSE/strings.xml | 2 ++ 3 files changed, 7 insertions(+) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 476f34ae7..dbc869010 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -605,11 +605,13 @@ anz der Bedingungen ist erforderlich Hochgeladene Medien konnten nicht vom Server heruntergeladen werden Lokale Datei konnte nicht zum Hochladen vorbereitet werden: %1$s Einloggen mit QR-Code + Route Startseite Suche Entdecken Nachrichten Benachrichtigungen + Global Kurzfilme Sicherheitsfilter Neuer Beitrag @@ -620,6 +622,7 @@ anz der Bedingungen ist erforderlich Antworten Boosten oder Zitieren Gefällt mir + Zap Profilbild von %1$s %1$s weiterleiten Rela-Liste erweitern diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index c361dd760..7fb37dfad 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -607,6 +607,7 @@ Descobrir Mensagens Notificações + Global Vídeos Curtos Filtros de Segurança Novo Post @@ -617,6 +618,7 @@ Responder Impulsionar ou Citar Gostar + Zap Foto de perfil de %1$s Repassar %1$s Expandir lista de repasse diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 3a589b300..c4e45a883 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -605,6 +605,7 @@ Upptäck Meddelanden Aviseringar + Globalt Kortfilmer Säkerhetsfilter Nytt inlägg @@ -615,6 +616,7 @@ Svara Boosta eller citera Gilla + Zap %1$s Profilbild Relä %1$s Expandera relälistan From bdff8c6c0d8b445aacccbf8cdadb761aca851dd7 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 12 Feb 2024 11:15:59 -0500 Subject: [PATCH 058/176] Moves language translation cleanup to the IO thread since it clears disk files as well --- .../java/com/vitorpamplona/amethyst/ui/MainActivity.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 85fef8da5..9ffec6bae 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -113,7 +113,9 @@ class MainActivity : AppCompatActivity() { val accountStateViewModel: AccountStateViewModel = viewModel() accountStateViewModel.serviceManager = serviceManager - LaunchedEffect(key1 = Unit) { accountStateViewModel.tryLoginExistingAccountAsync() } + LaunchedEffect(key1 = Unit) { + accountStateViewModel.tryLoginExistingAccountAsync() + } AccountScreen(accountStateViewModel, sharedPreferencesViewModel) } @@ -158,7 +160,9 @@ class MainActivity : AppCompatActivity() { override fun onPause() { Log.d("Lifetime Event", "MainActivity.onPause") - LanguageTranslatorService.clear() + GlobalScope.launch(Dispatchers.IO) { + LanguageTranslatorService.clear() + } serviceManager.cleanObservers() // if (BuildConfig.DEBUG) { From c0a9ce8e2170e6e0ece056ccc36cf615fa6f4d0d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 12 Feb 2024 11:52:18 -0500 Subject: [PATCH 059/176] Small refactoring to focus the encrypted storage procedures to the application context. --- .../com/vitorpamplona/amethyst/Amethyst.kt | 5 +++ .../amethyst/EncryptedStorage.kt | 45 ++++++++++--------- .../amethyst/LocalPreferences.kt | 2 +- 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt b/app/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt index a16fde1b4..74927d4a1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt @@ -26,6 +26,7 @@ import android.os.StrictMode import android.os.StrictMode.ThreadPolicy import android.os.StrictMode.VmPolicy import android.util.Log +import androidx.security.crypto.EncryptedSharedPreferences import coil.ImageLoader import coil.disk.DiskCache import com.vitorpamplona.amethyst.service.playback.VideoCache @@ -87,6 +88,10 @@ class Amethyst : Application() { return ImageLoader.Builder(applicationContext).diskCache { imageCache } } + fun encryptedStorage(npub: String? = null): EncryptedSharedPreferences { + return EncryptedStorage.preferences(instance, npub) + } + companion object { lateinit var instance: Amethyst private set diff --git a/app/src/main/java/com/vitorpamplona/amethyst/EncryptedStorage.kt b/app/src/main/java/com/vitorpamplona/amethyst/EncryptedStorage.kt index 9d6b15dad..130658ecf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/EncryptedStorage.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/EncryptedStorage.kt @@ -20,32 +20,35 @@ */ package com.vitorpamplona.amethyst +import android.content.Context import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey -object EncryptedStorage { - private const val PREFERENCES_NAME = "secret_keeper" +class EncryptedStorage { + companion object { + private const val PREFERENCES_NAME = "secret_keeper" - // returns the preferences for each account or a global file if null. - fun prefsFileName(npub: String? = null): String { - return if (npub == null) PREFERENCES_NAME else "${PREFERENCES_NAME}_$npub" - } + // returns the preferences for each account or a global file if null. + fun prefsFileName(npub: String? = null): String { + return if (npub == null) PREFERENCES_NAME else "${PREFERENCES_NAME}_$npub" + } - fun preferences(npub: String? = null): EncryptedSharedPreferences { - val context = Amethyst.instance - val masterKey: MasterKey = - MasterKey.Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS) - .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) - .build() + fun preferences( + applicationContext: Context, + npub: String? = null, + ): EncryptedSharedPreferences { + val masterKey: MasterKey = + MasterKey.Builder(applicationContext, MasterKey.DEFAULT_MASTER_KEY_ALIAS) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() - val preferencesName = prefsFileName(npub) - - return EncryptedSharedPreferences.create( - context, - preferencesName, - masterKey, - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, - ) as EncryptedSharedPreferences + return EncryptedSharedPreferences.create( + applicationContext, + prefsFileName(npub), + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, + ) as EncryptedSharedPreferences + } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index b553a33d2..178c1a514 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -233,7 +233,7 @@ object LocalPreferences { if (npub == null) DEBUG_PREFERENCES_NAME else "${DEBUG_PREFERENCES_NAME}_$npub" Amethyst.instance.getSharedPreferences(preferenceFile, Context.MODE_PRIVATE) } else { - return EncryptedStorage.preferences(npub) + return Amethyst.instance.encryptedStorage(npub) } } From 5109b65e3797f6717e300ee5138b5137d14f9d1d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 12 Feb 2024 14:21:32 -0500 Subject: [PATCH 060/176] Fixes crash when MuteList has `e` tags that are not valid hexes --- .../amethyst/ui/dal/HiddenAccountsFeedFilter.kt | 12 +++++++++++- .../vitorpamplona/quartz/events/GeneralListEvent.kt | 12 +++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt index b05cce59a..7843c396c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt @@ -20,9 +20,11 @@ */ package com.vitorpamplona.amethyst.ui.dal +import android.util.Log import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User +import kotlinx.coroutines.CancellationException class HiddenAccountsFeedFilter(val account: Account) : FeedFilter() { override fun feedKey(): String { @@ -34,7 +36,15 @@ class HiddenAccountsFeedFilter(val account: Account) : FeedFilter() { } override fun feed(): List { - return account.flowHiddenUsers.value.hiddenUsers.reversed().map { LocalCache.getOrCreateUser(it) } + return account.flowHiddenUsers.value.hiddenUsers.reversed().mapNotNull { + try { + LocalCache.getOrCreateUser(it) + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e("HiddenAccountsFeedFilter", "Failed to parse key $it") + null + } + } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt index 2563ad821..3ac1f4080 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt @@ -28,6 +28,7 @@ import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.signers.NostrSigner import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.toImmutableSet +import java.util.HashSet @Immutable abstract class GeneralListEvent( @@ -61,12 +62,13 @@ abstract class GeneralListEvent( key: String, privateTags: Array>?, ): ImmutableSet { - val privateUserList = - privateTags?.let { it.filter { it.size > 1 && it[0] == key }.map { it[1] }.toSet() } - ?: emptySet() - val publicUserList = tags.filter { it.size > 1 && it[0] == key }.map { it[1] }.toSet() + val result = HashSet(tags.size + (privateTags?.size ?: 0)) - return (privateUserList + publicUserList).toImmutableSet() + privateTags?.let { it.filter { it.size > 1 && it[0] == key }.mapTo(result) { it[1] } } + + tags.filter { it.size > 1 && it[0] == key }.mapTo(result) { it[1] } + + return result.toImmutableSet() } fun isTagged( From bbc4ec26257213cd1624a3958928416aa301a597 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 12 Feb 2024 15:10:48 -0500 Subject: [PATCH 061/176] Fixes thread rendering when "mention" events are added with mentioning the event. --- .../quartz/events/BaseTextNoteEvent.kt | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt index 5afa7657d..1317d6ae9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt @@ -42,13 +42,26 @@ open class BaseTextNoteEvent( ) : Event(id, pubKey, createdAt, kind, tags, content, sig) { fun mentions() = taggedUsers() - open fun replyTos() = taggedEvents() + open fun replyTos(): List { + val oldStylePositional = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] } + val newStyleReply = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "reply" }?.get(1) + val newStyleRoot = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "root" }?.get(1) + + val newStyleReplyTos = listOfNotNull(newStyleReply, newStyleRoot) + + return if (newStyleReplyTos.isNotEmpty()) { + newStyleReplyTos + } else { + oldStylePositional + } + } fun replyingTo(): HexKey? { val oldStylePositional = tags.lastOrNull { it.size > 1 && it[0] == "e" }?.get(1) - val newStyle = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "reply" }?.get(1) + val newStyleReply = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "reply" }?.get(1) + val newStyleRoot = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "root" }?.get(1) - return newStyle ?: oldStylePositional + return newStyleReply ?: newStyleRoot ?: oldStylePositional } @Transient private var citedUsersCache: Set? = null From 1deed59d5be5d455a99d9b3aeb6a23e68a28840e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 12 Feb 2024 16:03:37 -0500 Subject: [PATCH 062/176] Fixes: https://github.com/vitorpamplona/amethyst/issues/740 Displays error messages when the URI sent to Amethyst is invalid --- .../amethyst/ui/navigation/AppNavigation.kt | 62 ++++++++++++++----- .../ui/screen/loggedIn/AccountViewModel.kt | 14 ++++- .../amethyst/ui/screen/loggedIn/MainScreen.kt | 20 ++++-- app/src/main/res/values/strings.xml | 3 + .../vitorpamplona/quartz/encoders/Nip19.kt | 7 ++- 5 files changed, 83 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 35a453b64..3d7a696fc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -39,6 +39,7 @@ import androidx.core.util.Consumer import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.MainActivity import com.vitorpamplona.amethyst.ui.note.UserReactionsViewModel import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel @@ -359,31 +360,60 @@ fun AppNavigation( } val activity = LocalContext.current.getActivity() - var actionableNextPage by remember { - mutableStateOf(uriToRoute(activity.intent?.data?.toString()?.ifBlank { null })) + + var currentIntentNextPage by remember { + mutableStateOf(activity.intent?.data?.toString()?.ifBlank { null }) } - actionableNextPage?.let { - LaunchedEffect(it) { - navController.navigate(it) { - popUpTo(Route.Home.route) - launchSingleTop = true - } + + currentIntentNextPage?.let { intentNextPage -> + var actionableNextPage by remember { + mutableStateOf(uriToRoute(intentNextPage)) + } + + LaunchedEffect(intentNextPage) { + if (actionableNextPage != null) { + actionableNextPage?.let { + navController.navigate(it) { + popUpTo(Route.Home.route) + launchSingleTop = true + } + actionableNextPage = null + } + } else { + accountViewModel.toast( + R.string.invalid_nip19_uri, + R.string.invalid_nip19_uri_description, + intentNextPage, + ) + } + + currentIntentNextPage = null } - actionableNextPage = null } DisposableEffect(activity) { val consumer = Consumer { intent -> val uri = intent?.data?.toString() - val newPage = uriToRoute(uri) + if (!uri.isNullOrBlank()) { + val newPage = uriToRoute(uri) - newPage?.let { route -> - val currentRoute = getRouteWithArguments(navController) - if (!isSameRoute(currentRoute, route)) { - navController.navigate(route) { - popUpTo(Route.Home.route) - launchSingleTop = true + if (newPage != null) { + val currentRoute = getRouteWithArguments(navController) + if (!isSameRoute(currentRoute, newPage)) { + navController.navigate(newPage) { + popUpTo(Route.Home.route) + launchSingleTop = true + } + } + } else { + scope.launch { + delay(1000) + accountViewModel.toast( + R.string.invalid_nip19_uri, + R.string.invalid_nip19_uri_description, + uri, + ) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index e5b295140..adbed38b5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -105,7 +105,11 @@ import kotlin.time.measureTimedValue @Immutable class StringToastMsg(val title: String, val msg: String) : ToastMsg() -@Immutable class ResourceToastMsg(val titleResId: Int, val resourceId: Int) : ToastMsg() +@Immutable class ResourceToastMsg( + val titleResId: Int, + val resourceId: Int, + val params: Array? = null, +) : ToastMsg() @Stable class AccountViewModel(val account: Account, val settings: SettingsState) : ViewModel(), Dao { @@ -141,6 +145,14 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View viewModelScope.launch { toasts.emit(ResourceToastMsg(titleResId, resourceId)) } } + fun toast( + titleResId: Int, + resourceId: Int, + vararg params: String, + ) { + viewModelScope.launch { toasts.emit(ResourceToastMsg(titleResId, resourceId, params)) } + } + fun isWriteable(): Boolean { return account.isWriteable() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt index 9dfea373e..cbe5114e6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt @@ -473,12 +473,22 @@ private fun DisplayErrorMessages(accountViewModel: AccountViewModel) { openDialogMsg.value?.let { obj -> when (obj) { is ResourceToastMsg -> - InformationDialog( - context.getString(obj.titleResId), - context.getString(obj.resourceId), - ) { - accountViewModel.clearToasts() + if (obj.params != null) { + InformationDialog( + context.getString(obj.titleResId), + context.getString(obj.resourceId, *obj.params), + ) { + accountViewModel.clearToasts() + } + } else { + InformationDialog( + context.getString(obj.titleResId), + context.getString(obj.resourceId), + ) { + accountViewModel.clearToasts() + } } + is StringToastMsg -> InformationDialog( obj.title, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c8c0655a4..6ad75e819 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -753,4 +753,7 @@ Add content warning Remove content warning Show npub as a QR code + + Invalid address + Amethyst received a URI to open but that uri was invalid: %1$s diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19.kt index 7e1d2418e..03f638351 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19.kt @@ -97,7 +97,12 @@ object Nip19 { "naddr1" -> naddr(bytes) else -> null } - parsed?.copy(additionalChars = additionalChars ?: "") + + if (parsed?.hex?.isBlank() == true) { + null + } else { + parsed?.copy(additionalChars = additionalChars ?: "") + } } catch (e: Throwable) { Log.w("NIP19 Parser", "Issue trying to Decode NIP19 $key: ${e.message}", e) null From 60e87d943a5596dc940ee5433e4cd523c3c02495 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 12 Feb 2024 16:09:03 -0500 Subject: [PATCH 063/176] v0.83.13 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ff2a2345c..a483cf79d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 34 - versionCode 353 - versionName "0.83.12" + versionCode 354 + versionName "0.83.13" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From d41eb4ce3189acc19d584a1903579bfabf3be79e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 12 Feb 2024 18:11:28 -0500 Subject: [PATCH 064/176] No need to crossfade when expanding content --- .../ui/components/ExpandableRichTextViewer.kt | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt index fc845ba2a..81d234ae4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.components -import androidx.compose.animation.Crossfade import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -78,17 +77,15 @@ fun ExpandableRichTextViewer( } Box { - Crossfade(text, label = "ExpandableRichTextViewer") { - RichTextViewer( - it, - canPreview, - modifier.align(Alignment.TopStart), - tags, - backgroundColor, - accountViewModel, - nav, - ) - } + RichTextViewer( + text, + canPreview, + modifier.align(Alignment.TopStart), + tags, + backgroundColor, + accountViewModel, + nav, + ) if (content.length > whereToCut && !showFullText) { Row( From c9d59fd5ac87aa02b68a6c761df34f364d635de2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Feb 2024 11:38:28 -0500 Subject: [PATCH 065/176] Adds a check for the main thread when pulling opengraph tags. --- .../vitorpamplona/amethyst/service/previews/UrlPreviewUtils.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreviewUtils.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreviewUtils.kt index f028f5804..14e36c2d8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreviewUtils.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreviewUtils.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.service.previews import com.vitorpamplona.amethyst.service.HttpClientManager +import com.vitorpamplona.amethyst.service.checkNotInMainThread import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.MediaType @@ -91,6 +92,7 @@ suspend fun getDocument( withContext(Dispatchers.IO) { val request: Request = Request.Builder().url(url).get().build() HttpClientManager.getHttpClient().newCall(request).execute().use { + checkNotInMainThread() if (it.isSuccessful) { val mimeType = it.headers.get("Content-Type")?.toMediaType() From f75104e616360ef3bafd4903c06c712b1005f4bb Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Feb 2024 11:38:55 -0500 Subject: [PATCH 066/176] Updates Compose dependencies --- app/build.gradle | 2 +- build.gradle | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index a483cf79d..b267a5d54 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -253,7 +253,7 @@ dependencies { playImplementation 'com.google.mlkit:translate:17.0.2' // PushNotifications - playImplementation platform('com.google.firebase:firebase-bom:32.7.1') + playImplementation platform('com.google.firebase:firebase-bom:32.7.2') playImplementation 'com.google.firebase:firebase-messaging-ktx' //PushNotifications(FDroid) diff --git a/build.gradle b/build.gradle index a4d9e0b29..f3c3da5aa 100644 --- a/build.gradle +++ b/build.gradle @@ -4,15 +4,15 @@ buildscript { ext { fragment_version = "1.6.2" lifecycle_version = '2.7.0' - compose_ui_version = '1.6.0' - nav_version = '2.7.6' + compose_ui_version = '1.6.1' + nav_version = '2.7.7' room_version = "2.4.3" accompanist_version = '0.34.0' coil_version = '2.5.0' vico_version = '1.13.1' media3_version = '1.2.1' core_ktx_version = '1.12.0' - material3_version = '1.1.2' + material3_version = '1.2.0' } dependencies { classpath 'com.google.gms:google-services:4.4.0' From 9d8f70e04b621efed1781191e323fe594c1db619 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Feb 2024 11:39:54 -0500 Subject: [PATCH 067/176] No need to remember this param. --- .../com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index 66a84440d..f1848eab4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -542,11 +542,6 @@ fun WatchUserMetadataAndFollowsAndRenderUserProfilePicture( author: User, accountViewModel: AccountViewModel, ) { - val automaticallyShowProfilePicture = - remember { - accountViewModel.settings.showProfilePictures.value - } - WatchUserMetadata(author) { baseUserPicture -> // Crossfade(targetState = baseUserPicture) { userPicture -> RobohashFallbackAsyncImage( @@ -555,7 +550,7 @@ fun WatchUserMetadataAndFollowsAndRenderUserProfilePicture( contentDescription = stringResource(id = R.string.profile_image), modifier = MaterialTheme.colorScheme.profile35dpModifier, contentScale = ContentScale.Crop, - loadProfilePicture = automaticallyShowProfilePicture, + loadProfilePicture = accountViewModel.settings.showProfilePictures.value, ) // } } From 629c8b8167c02ae360b2151ac10ef6e8a18d2fbe Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Feb 2024 11:40:42 -0500 Subject: [PATCH 068/176] Avoids the memory use of the flatten operation --- .../amethyst/ui/note/UserReactionsRow.kt | 72 ++++++++++--------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt index 06514959c..dc8c30ca5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt @@ -275,7 +275,7 @@ class UserReactionsViewModel(val account: Account) : ViewModel() { refreshChartModel() } - suspend fun addToStatsSuspend(newNotes: Set) { + suspend fun addToStatsSuspend(newBlockNotes: Set>) { checkNotInMainThread() val currentUser = user.pubkeyHex @@ -287,39 +287,41 @@ class UserReactionsViewModel(val account: Account) : ViewModel() { val takenIntoAccount = this.takenIntoAccount.toMutableSet() var hasNewElements = false - newNotes.forEach { - val noteEvent = it.event - if (noteEvent != null && !takenIntoAccount.contains(noteEvent.id())) { - if (noteEvent is ReactionEvent) { - if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) { - val netDate = formatDate(noteEvent.createdAt) - reactions[netDate] = (reactions[netDate] ?: 0) + 1 - takenIntoAccount.add(noteEvent.id()) - hasNewElements = true - } - } else if (noteEvent is RepostEvent || noteEvent is GenericRepostEvent) { - if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey() != currentUser) { - val netDate = formatDate(noteEvent.createdAt()) - boosts[netDate] = (boosts[netDate] ?: 0) + 1 - takenIntoAccount.add(noteEvent.id()) - hasNewElements = true - } - } else if (noteEvent is LnZapEvent) { - if ( - noteEvent.isTaggedUser(currentUser) - ) { // && noteEvent.pubKey != currentUser User might be sending his own receipts - val netDate = formatDate(noteEvent.createdAt) - zaps[netDate] = - (zaps[netDate] ?: BigDecimal.ZERO) + (noteEvent.amount ?: BigDecimal.ZERO) - takenIntoAccount.add(noteEvent.id()) - hasNewElements = true - } - } else if (noteEvent is TextNoteEvent) { - if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) { - val netDate = formatDate(noteEvent.createdAt) - replies[netDate] = (replies[netDate] ?: 0) + 1 - takenIntoAccount.add(noteEvent.id()) - hasNewElements = true + newBlockNotes.forEach { newNotes -> + newNotes.forEach { + val noteEvent = it.event + if (noteEvent != null && !takenIntoAccount.contains(noteEvent.id())) { + if (noteEvent is ReactionEvent) { + if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) { + val netDate = formatDate(noteEvent.createdAt) + reactions[netDate] = (reactions[netDate] ?: 0) + 1 + takenIntoAccount.add(noteEvent.id()) + hasNewElements = true + } + } else if (noteEvent is RepostEvent || noteEvent is GenericRepostEvent) { + if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey() != currentUser) { + val netDate = formatDate(noteEvent.createdAt()) + boosts[netDate] = (boosts[netDate] ?: 0) + 1 + takenIntoAccount.add(noteEvent.id()) + hasNewElements = true + } + } else if (noteEvent is LnZapEvent) { + if ( + noteEvent.isTaggedUser(currentUser) + ) { // && noteEvent.pubKey != currentUser User might be sending his own receipts + val netDate = formatDate(noteEvent.createdAt) + zaps[netDate] = + (zaps[netDate] ?: BigDecimal.ZERO) + (noteEvent.amount ?: BigDecimal.ZERO) + takenIntoAccount.add(noteEvent.id()) + hasNewElements = true + } + } else if (noteEvent is TextNoteEvent) { + if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) { + val netDate = formatDate(noteEvent.createdAt) + replies[netDate] = (replies[netDate] ?: 0) + 1 + takenIntoAccount.add(noteEvent.id()) + hasNewElements = true + } } } } @@ -422,7 +424,7 @@ class UserReactionsViewModel(val account: Account) : ViewModel() { private val bundlerInsert = BundledInsert>(250, Dispatchers.IO) fun invalidateInsertData(newItems: Set) { - bundlerInsert.invalidateList(newItems) { addToStatsSuspend(it.flatten().toSet()) } + bundlerInsert.invalidateList(newItems) { addToStatsSuspend(it) } } override fun onCleared() { From 1592d55db50378aaa971ded05c9e47249547ebc7 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Tue, 13 Feb 2024 16:43:27 +0000 Subject: [PATCH 069/176] New Crowdin translations by GitHub Action --- app/src/main/res/values-nl/strings.xml | 43 ++++++++++++++++++ app/src/main/res/values-zh-rCN/strings.xml | 53 ++++++++++++++++++++++ app/src/main/res/values-zh-rTW/strings.xml | 53 ++++++++++++++++++++++ 3 files changed, 149 insertions(+) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 238b124b3..c212df65a 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -3,6 +3,7 @@ Richt camera op de QR-code Toon QR Profielfoto + Jouw profielfoto Scan QR Laat toch zien Bericht gemarkeerd als ongepast door @@ -453,6 +454,7 @@ Voor het activeren van deze modus is Amethyst nodig om een NIP-24 bericht te versturen. NIP-24 is nieuw en de meeste clients hebben deze nog niet geïmplementeerd. Zorg ervoor dat de ontvanger een compatibele client gebruikt. Activeren Publiek + Nieuwe openbare of besloten groep Privé Aan Onderwerp @@ -469,6 +471,7 @@ Toon URL-voorbeelden Wanneer afbeeldingen te laden Kopiëren naar klembord + Kopieer npub naar klembord Kopieer URL naar klembord Note naar klembord kopiëren Gemaakt op @@ -597,4 +600,44 @@ Server heeft geen URL opgegeven na uploaden Kan de geüploade media niet downloaden van de server Kon lokaal bestand niet voorbereiden voor upload: %1$s + Inloggen met QR-Code + Route + Beginscherm + Zoeken + Ontdekken + Berichten + Notificaties + Globaal + Shorts + Beveiligingsfilters + Nieuw bericht + Nieuwe shorts: afbeeldingen of video\'s + Nieuwe community note + Open alle reacties op dit bericht + Sluit alle reacties op dit bericht + Antwoorden + Boost of citaat + Vind ik leuk + Zap + Profielfoto van %1$s + Relay %1$s + Lijst van relays uitvouwen + Note opties + Relay selectiemenu + Poll + Poll uitschakelen + Bitcoin invoice + Annuleer Bitcoin invoice + Verkoop van een item annuleren + Zapraiser + Annuleer Zapraiser + Locatie + Locatie verwijderen + Zap splits + Annuleer splitsing Zap + Content waarschuwing toevoegen + Content waarschuwing verwijderen + Toon npub als een QR-code + Ongeldig adres + Amethist ontving een URI om te openen, maar die uri was ongeldig: %1$s diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 8f9adfa0f..ac4f6de64 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -3,6 +3,7 @@ 对准二维码 显示二维码 头像图片 + 头像图片 扫描二维码 仍然显示 帖文被标记为不当 @@ -90,6 +91,8 @@ 添加中继器 显示名称 我的显示名称 + Ostrich McAwesome + 欢迎! 用户名 我的用户名 关于我 @@ -143,7 +146,14 @@ 使用条款 需要接受条款 需要密钥 + 名称必填 登录 + 注册 + 创建帐户 + 我们应该如何称呼你? + 还没有 Nostr 帐户? + 已经有 Nostr 帐户? + 创建新帐户 生成新密钥 正在加载 正在载入帐户 @@ -443,6 +453,7 @@ 启用此模式需要 Amethyst 发送一条 NIP-24 消息(包装的、密封的私信和群聊消息)。因为 NIP-24 是新的,大多数客户端尚未执行。请确保接收方正在使用兼容的客户端。 启用 公开 + 新建公开或私人群组 私人 主题 @@ -459,6 +470,7 @@ 显示 URL 预览 何时加载图像 复制到剪贴板 + 复制 npub 到剪贴板 复制链接到剪贴板 复制笔记 ID 到剪贴板 创建于 @@ -522,6 +534,7 @@ 无法从闪电地址集合LNURL\"%1$s\"。请检查用户设置 %1$s 的接收者闪电服务不可用。它是根据雷电地址\"%2$s\"计算的。 错误: %3$s。请检查服务器是否已经上线,闪电地址是否正确 无法解析 %1$s。请检查你是否已连接,服务器是否启动,以及闪电地址 %2$s 是否正确 + 无法解析 %1$s。请检查你是否已连接,服务器是否启动,以及闪电地址 %2$s 是否正确。\n\n异常为:%3$s 无法从 %1$s 获取发票 从闪电地址解析JSON出错。请检查用户的打闪设置 在用户闪电地址服务器配置中找不到回调URL @@ -586,4 +599,44 @@ 上传后服务器没有提供 URL 无法从服务器下载上传的媒体 无法准备要上传的本地文件:%1$s + 使用二维码登录 + 路径 + 主页 + 搜索 + 发现 + 消息 + 通知 + 全球 + 短篇 + 安全滤镜 + 新帖子 + 新短篇媒体:图像或视频 + 新社区笔记 + 展开对此帖子的所有回应 + 收起对此帖子的所有回应 + 回复 + 提升或引用 + 点赞 + 打闪 + %1$s 的个人头像 + 中继器 %1$s + 展开中继列表 + 笔记选项 + 中继列表选择器 + 投票 + 停用投票 + 比特币发票 + 取消比特币发票 + 取消出售物品 + Zapraiser + 取消 Zapraiser + 地点 + 移除地点 + 打闪拆分 + 取消打闪拆分 + 添加内容警告 + 移除内容警告 + 将 npub 显示为二维码 + 地址无效 + Amethyst 收到了要打开的 URI,但该 uri 无效:%1$s diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 223f7b1fa..13b3ecedf 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -3,6 +3,7 @@ 對準 QR 顯示 QR 頭像圖片 + 頭像圖片 掃描 QR 一直顯示 貼文被標記為不當 @@ -90,6 +91,8 @@ 添加中繼器 顯示名稱 我的顯示名稱 + Ostrich McAwesome + 歡迎! 用户名 我的用户名 關於我 @@ -143,7 +146,14 @@ 使用條款 需要接受條款 需要密鑰 + 名稱為必填 登錄 + 註冊 + 建立帳戶 + 我們應該如何稱呼你? + 還沒有 Nostr 帳戶? + 已經有 Nostr 帳戶? + 創建新帳戶 生成新密鑰 正在加載 正在加載帳戶 @@ -444,6 +454,7 @@ 啟用此模式需要 Amethyst 發送一條 NIP-24 消息(包裝的、密封的私信和群聊消息)。因為 NIP-24 是新的,大多數客戶端尚未執行。請確保接收方正在使用兼容的客戶端。 啟用 公開 + 新建公開或私人群組 私人 主題 @@ -460,6 +471,7 @@ 顯示鏈接預覽 何時加載圖像 複製至剪貼簿 + 複製 npub 至剪貼簿 將 URL 複製到剪貼簿 將筆記 ID 複製到剪貼簿 創建於 @@ -523,6 +535,7 @@ 無法從閃電地址集合 LNURL “%1$s”。請檢查用戶設置 %1$s 的接收方閃電服務不可用。它是根據閃電地址“%2$s”計算的。錯誤:%3$s。請檢查伺服器是否已上線,閃電地址是否正確 無法解析 %1$s。請檢查你是否已連接,伺服器是否啟動,以及閃電地址 %2$s 是否正確 + 無法解析 %1$s。請檢查你是否已連接,服務器是否啓動,以及閃電地址 %2$s 是否正確。\n\n異常爲:%3$s 無法從 %1$s 獲取發票 從閃電地址解析 JSON 發生錯誤。請檢查用戶的閃電設置 在用戶閃電地址伺服器配置中找不到回調 URL @@ -587,4 +600,44 @@ 上傳後伺服器沒有提供 URL 無法從伺服器下載上傳的媒體 無法準備要上傳的本地文件:%1$s + 使用二維碼登錄 + 路徑 + 主頁 + 搜索 + 發現 + 訊息 + 通知 + 全球 + 短篇 + 安全濾鏡 + 新帖子 + 新短篇:圖像或視頻 + 新社區筆記 + 展開所有對此帖子的回應 + 收起所有對此帖子的回應 + 回覆 + 提升或引用 + 點贊 + 打閃 + %1$s 的頭像圖片 + 中繼器 %1$s + 展開中繼列表 + 筆記選項 + 中繼列表選擇器 + 投票 + 停用投票 + 比特幣發票 + 取消比特幣發票 + 取消出售物品 + Zapraiser + 取消 Zapraiser + 地點 + 移除地點 + 打閃拆分 + 取消打閃拆分 + 添加內容警告 + 移除內容警告 + 將 npub 顯示爲二維碼 + 地址無效 + Amethyst 收到了要打開的 URI,但該 URI 無效:%1$s From 74f0393706984201282f57269fbac73ef87e8312 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Feb 2024 12:42:46 -0500 Subject: [PATCH 070/176] =?UTF-8?q?Adds=20"=E2=82=BFitcoin"=20to=20the=20s?= =?UTF-8?q?et=20of=20custom=20hashtags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/vitorpamplona/amethyst/model/HashtagIcon.kt | 1 + .../amethyst/ui/components/RichTextViewer.kt | 8 -------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt index fb40ae0d2..c40b37d43 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt @@ -32,6 +32,7 @@ fun checkForHashtagWithIcon( primary: Color, ): HashtagIcon? { return when (tag.lowercase()) { + "₿itcoin", "bitcoin", "btc", "timechain", diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 27fa55775..ddb47afc1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -623,14 +623,6 @@ private fun DisplayFullNote( @Composable fun HashTag( - word: HashTagSegment, - nav: (String) -> Unit, -) { - RenderHashtag(word, nav) -} - -@Composable -private fun RenderHashtag( segment: HashTagSegment, nav: (String) -> Unit, ) { From b94c1e2792ed1142a24e45eafa5e86e04d746d26 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Feb 2024 19:51:32 -0500 Subject: [PATCH 071/176] Quartz support for NIP-49 encrypted nsecs --- README.md | 2 +- quartz/build.gradle | 2 + .../com/vitorpamplona/quartz/NIP49Test.kt | 103 +++++++++++ .../com/vitorpamplona/quartz/crypto/Nip49.kt | 173 ++++++++++++++++++ .../vitorpamplona/quartz/encoders/Nip19.kt | 1 + .../quartz/encoders/NIP19ParserTest.kt | 12 ++ 6 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP49Test.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip49.kt diff --git a/README.md b/README.md index ffafec932..5339adc3a 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases) - [ ] Nostr Connect (NIP-46) - [x] Wallet Connect API (NIP-47) - [ ] Proxy Tags (NIP-48, Not applicable) -- [ ] Private key encryption for import/export (NIP-49) +- [x] Private key encryption for import/export (NIP-49) - [x] Online Relay Search (NIP-50) - [x] Lists (NIP-51) - [ ] Calendar Events (NIP-52) diff --git a/quartz/build.gradle b/quartz/build.gradle index c01833fb1..3dd64b7cd 100644 --- a/quartz/build.gradle +++ b/quartz/build.gradle @@ -54,6 +54,8 @@ dependencies { // immutable collections to avoid recomposition api('org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7') + api('com.lambdaworks:scrypt:1.4.0') + // Parses URLs from Text: api "io.github.url-detector:url-detector:0.1.23" diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP49Test.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP49Test.kt new file mode 100644 index 000000000..01825904a --- /dev/null +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP49Test.kt @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2023 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 + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.crypto.Nip49 +import com.vitorpamplona.quartz.encoders.toHexKey +import fr.acinq.secp256k1.Secp256k1 +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import org.junit.Test +import org.junit.runner.RunWith +import java.security.SecureRandom + +@RunWith(AndroidJUnit4::class) +public class NIP49Test { + companion object { + val TEST_CASE = "ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p" + + val TEST_CASE_EXPECTED = "3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683" + val TEST_CASE_PASSWORD = "nostr" + + val MAIN_TEST_CASES = + listOf( + Nip49TestCase(".ksjabdk.aselqwe", "14c226dbdd865d5e1645e72c7470fd0a17feb42cc87b750bab6538171b3a3f8a", 1, 0x00), + Nip49TestCase("skjdaklrnçurbç l", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 2, 0x01), + Nip49TestCase("777z7z7z7z7z7z7z", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 3, 0x02), + Nip49TestCase(".ksjabdk.aselqwe", "14c226dbdd865d5e1645e72c7470fd0a17feb42cc87b750bab6538171b3a3f8a", 7, 0x00), + Nip49TestCase("skjdaklrnçurbç l", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 8, 0x01), + Nip49TestCase("777z7z7z7z7z7z7z", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 9, 0x02), + ) + } + + val random = SecureRandom() + val nip49 = Nip49(Secp256k1.get(), random) + + @Test + fun decodeBech32() { + val data = + Nip49.EncryptedInfo.decodePayload( + TEST_CASE, + )!! + + assertEquals(2, data.version) + assertEquals(16, data.logn) + assertEquals("52d7c3f8580e7b41953381e5bc49646b", data.salt.toHexKey()) + assertEquals("c33f02a7dcaac8bdd8da23cd449783240b6ebc12edeea7bf", data.nonce.toHexKey()) + assertEquals(0, data.keySecurity) + assertEquals("b8e8803440de7b3e9519c3e734cb2ac9a211ea2dc52312e5117a11a3022d813ab438719ca0b504a1193be510c3aee776", data.encryptedKey.toHexKey()) + } + + @Test + fun decrypt() { + val decrypted = nip49.decrypt(TEST_CASE, TEST_CASE_PASSWORD) + assertEquals(TEST_CASE_EXPECTED, decrypted) + } + + @Test + fun encryptDecryptTestCase() { + val encrypted = nip49.encrypt(TEST_CASE_EXPECTED, TEST_CASE_PASSWORD, 16, 0) + val decrypted = nip49.decrypt(encrypted!!, TEST_CASE_PASSWORD) + + assertEquals(TEST_CASE_EXPECTED, decrypted) + } + + @Test + fun encryptDecrypt() { + MAIN_TEST_CASES.forEach { + val encrypted = nip49.encrypt(it.secretKey, it.password, it.logn, it.ksb) + + assertNotNull(encrypted) + + val decrypted = nip49.decrypt(encrypted!!, it.password) + + assertEquals(it.secretKey, decrypted) + } + } + + class Nip49TestCase( + val password: String, + val secretKey: String, + val logn: Int, + val ksb: Byte, + ) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip49.kt b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip49.kt new file mode 100644 index 000000000..2d53fc2d7 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip49.kt @@ -0,0 +1,173 @@ +/** + * Copyright (c) 2023 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.crypto + +import android.util.Log +import com.goterl.lazysodium.LazySodiumAndroid +import com.goterl.lazysodium.SodiumAndroid +import com.lambdaworks.crypto.SCrypt +import com.vitorpamplona.quartz.encoders.Bech32 +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.encoders.bechToBytes +import com.vitorpamplona.quartz.encoders.hexToByteArray +import com.vitorpamplona.quartz.encoders.toHexKey +import fr.acinq.secp256k1.Secp256k1 +import java.security.SecureRandom + +class Nip49(val secp256k1: Secp256k1, val random: SecureRandom) { + private val libSodium = SodiumAndroid() + private val lazySodium = LazySodiumAndroid(libSodium) + + fun decrypt( + nCryptSec: String, + password: String, + ): HexKey? { + return decrypt(EncryptedInfo.decodePayload(nCryptSec), password) + } + + fun decrypt( + encryptedInfo: EncryptedInfo?, + password: String = "", + ): String? { + check(encryptedInfo != null) { "Couldn't decode key" } + check(encryptedInfo.version == EncryptedInfo.V) { "invalid version" } + + val n = Math.pow(2.0, encryptedInfo.logn.toDouble()).toInt() + val key = SCrypt.scrypt(password.toByteArray(Charsets.UTF_8), encryptedInfo.salt, n, 8, 1, 32) + val m = ByteArray(32) + + lazySodium.cryptoAeadXChaCha20Poly1305IetfDecrypt( + m, + longArrayOf(32L), + key, + encryptedInfo.encryptedKey, + encryptedInfo.encryptedKey.size.toLong(), + byteArrayOf(encryptedInfo.keySecurity), + 1, + encryptedInfo.nonce, + key, + ) + + return m.toHexKey() + } + + fun encrypt( + secretKeyHex: String, + password: String, + logn: Int, + ksb: Byte, + ): String? { + return encrypt(secretKeyHex.hexToByteArray(), password, logn, ksb) + } + + fun encrypt( + secretKey: ByteArray, + password: String, + logn: Int, + ksb: Byte, + ): String { + check(secretKey.size == 32) { "invalid secret key" } + val salt = ByteArray(16) + random.nextBytes(salt) + + val nonce = ByteArray(24) + random.nextBytes(nonce) + + val n = Math.pow(2.0, logn.toDouble()).toInt() + val key = SCrypt.scrypt(password.toByteArray(Charsets.UTF_8), salt, n, 8, 1, 32) + val ciphertext = ByteArray(48) + + // byte[] c, long[] cLen, + // byte[] m, long mLen, + // byte[] ad, long adLen, + // byte[] nSec, byte[] nPub, byte[] k + lazySodium.cryptoAeadXChaCha20Poly1305IetfEncrypt( + ciphertext, longArrayOf(48), + secretKey, secretKey.size.toLong(), + byteArrayOf(ksb), 1, + key, + nonce, + key, + ) + + return EncryptedInfo( + EncryptedInfo.V, + logn.toByte(), + salt, + nonce, + ksb, + ciphertext, + ).encodePayload() + } + + class EncryptedInfo( + val version: Byte, + val logn: Byte, + val salt: ByteArray, + val nonce: ByteArray, + val keySecurity: Byte, + val encryptedKey: ByteArray, + ) { + companion object { + const val V: Byte = 0x02 + + const val UNSAFE_HANDLING = 0x00 + const val SAFE_HANDLING = 0x01 + const val CLIENT_DOES_NOT_TRACK = 0x02 + + fun decodePayload(nCryptSec: String): EncryptedInfo? { + val byteArray = + try { + nCryptSec.bechToBytes() + } catch (e: Throwable) { + Log.e("NIP19 Parser", "Issue trying to Decode NIP49 $nCryptSec: ${e.message}", e) + return null + } + + return try { + return EncryptedInfo( + version = byteArray[0], + logn = byteArray[1], + salt = byteArray.copyOfRange(2, 2 + 16), + nonce = byteArray.copyOfRange(2 + 16, 2 + 16 + 24), + keySecurity = byteArray.copyOfRange(2 + 16 + 24, 2 + 16 + 24 + 1).get(0), + encryptedKey = byteArray.copyOfRange(2 + 16 + 24 + 1, byteArray.size), + ) + } catch (e: Exception) { + Log.w("NIP44v2", "Unable to Parse encrypted ncryptsec payload") + null + } + } + } + + // ln(n.toDouble()).toInt().toByte(), + fun encodePayload(): String { + return Bech32.encodeBytes( + hrp = "ncryptsec", + byteArrayOf( + version, + logn, + ) + salt + nonce + keySecurity + encryptedKey, + Bech32.Encoding.Bech32, + ) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19.kt index 03f638351..57bcb9f17 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19.kt @@ -32,6 +32,7 @@ object Nip19 { EVENT, RELAY, ADDRESS, + NSEC, } enum class TlvTypes(val id: Byte) { diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/encoders/NIP19ParserTest.kt b/quartz/src/test/java/com/vitorpamplona/quartz/encoders/NIP19ParserTest.kt index 9bb72ac58..750104269 100644 --- a/quartz/src/test/java/com/vitorpamplona/quartz/encoders/NIP19ParserTest.kt +++ b/quartz/src/test/java/com/vitorpamplona/quartz/encoders/NIP19ParserTest.kt @@ -24,6 +24,18 @@ import org.junit.Assert.assertEquals import org.junit.Test class NIP19ParserTest { + @Test + fun nCryptSecParser() { + val result = + Nip19.uriToRoute( + "nostr:ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p", + ) + assertEquals( + "30023:460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c:", + result?.hex, + ) + } + @Test fun nAddrParser() { val result = From 8bc1c2ec687ae927519e0a39edf17cb07d4d0328 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Feb 2024 20:44:00 -0500 Subject: [PATCH 072/176] Removing nip19 test with ncryptosec --- .../vitorpamplona/quartz/encoders/NIP19ParserTest.kt | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/encoders/NIP19ParserTest.kt b/quartz/src/test/java/com/vitorpamplona/quartz/encoders/NIP19ParserTest.kt index 750104269..9bb72ac58 100644 --- a/quartz/src/test/java/com/vitorpamplona/quartz/encoders/NIP19ParserTest.kt +++ b/quartz/src/test/java/com/vitorpamplona/quartz/encoders/NIP19ParserTest.kt @@ -24,18 +24,6 @@ import org.junit.Assert.assertEquals import org.junit.Test class NIP19ParserTest { - @Test - fun nCryptSecParser() { - val result = - Nip19.uriToRoute( - "nostr:ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p", - ) - assertEquals( - "30023:460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c:", - result?.hex, - ) - } - @Test fun nAddrParser() { val result = From 9c84a538d6084c35295a31eb87781cc4ef4ac3ba Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Feb 2024 20:49:21 -0500 Subject: [PATCH 073/176] Removes the NSEC form nip19 --- quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19.kt index 57bcb9f17..03f638351 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19.kt @@ -32,7 +32,6 @@ object Nip19 { EVENT, RELAY, ADDRESS, - NSEC, } enum class TlvTypes(val id: Byte) { From 47176421e0f05bd52b8ee41507d545f90e369fb5 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 14 Feb 2024 09:17:39 -0500 Subject: [PATCH 074/176] Adds copyright sentence. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 5339adc3a..5b7b00efd 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,10 @@ Translations can be provided via [Crowdin](https://crowdin.com/project/amethyst- You can also send patches through Nostr using [GitStr](https://github.com/fiatjaf/gitstr) to [this nostr address](https://patch34.pages.dev/naddr1qqyxzmt9w358jum5qyg8v6t5daezumn0wd68yvfwvdhk6qg7waehxw309ahx7um5wgkhqatz9emk2mrvdaexgetj9ehx2ap0qy2hwumn8ghj7un9d3shjtnwdaehgu3wvfnj7q3qgcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqxpqqqpmej720gac) +By contributing to this repository, you agree to license your work under the MIT license. Any work +contributed where you are not the original author must contain its license header with the original +author(s) and source. + ## How to Deploy 1. Generate a new signing key From 48f27f656dcb19ff52be57790586eb15f3e7fdc0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 14 Feb 2024 12:54:08 -0500 Subject: [PATCH 075/176] Adds NIP49 to login and back up key screens --- .../ui/screen/AccountStateViewModel.kt | 62 ++++- .../ui/screen/loggedIn/AccountBackupDialog.kt | 240 ++++++++++++++++- .../ui/screen/loggedOff/LoginScreen.kt | 246 ++++++++++++++---- app/src/main/res/values/strings.xml | 16 +- .../quartz/crypto/CryptoUtils.kt | 17 ++ .../com/vitorpamplona/quartz/crypto/Nip49.kt | 12 +- 6 files changed, 519 insertions(+), 74 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index 484943d4a..4ceb37bc2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -30,6 +30,7 @@ import com.vitorpamplona.amethyst.ServiceManager import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.HttpClientManager import com.vitorpamplona.amethyst.service.relays.Client +import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.Hex import com.vitorpamplona.quartz.encoders.Nip19 @@ -193,6 +194,42 @@ class AccountStateViewModel() : ViewModel() { } } + fun login( + key: String, + password: String, + useProxy: Boolean, + proxyPort: Int, + loginWithExternalSigner: Boolean = false, + packageName: String = "", + onError: (String?) -> Unit, + ) { + viewModelScope.launch(Dispatchers.IO) { + if (key.startsWith("ncryptsec")) { + val newKey = + try { + CryptoUtils.decryptNIP49(key, password) + } catch (e: Exception) { + if (e is CancellationException) throw e + onError(e.message) + return@launch + } + + if (newKey == null) { + onError("Could not decrypt key with provided password") + Log.e("Login", "Could not decrypt ncryptsec") + } else { + loginSync(newKey, useProxy, proxyPort, loginWithExternalSigner, packageName) { + onError(null) + } + } + } else { + loginSync(key, useProxy, proxyPort, loginWithExternalSigner, packageName) { + onError(null) + } + } + } + } + fun login( key: String, useProxy: Boolean, @@ -202,13 +239,24 @@ class AccountStateViewModel() : ViewModel() { onError: () -> Unit, ) { viewModelScope.launch(Dispatchers.IO) { - try { - loginAndStartUI(key, useProxy, proxyPort, loginWithExternalSigner, packageName) - } catch (e: Exception) { - if (e is CancellationException) throw e - Log.e("Login", "Could not sign in", e) - onError() - } + loginSync(key, useProxy, proxyPort, loginWithExternalSigner, packageName, onError) + } + } + + suspend fun loginSync( + key: String, + useProxy: Boolean, + proxyPort: Int, + loginWithExternalSigner: Boolean = false, + packageName: String = "", + onError: () -> Unit, + ) { + try { + loginAndStartUI(key, useProxy, proxyPort, loginWithExternalSigner, packageName) + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e("Login", "Could not sign in", e) + onError() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt index 28871c35d..fbed6de3d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt @@ -36,23 +36,48 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Key +import androidx.compose.material.icons.outlined.Visibility +import androidx.compose.material.icons.outlined.VisibilityOff import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.autofill.AutofillNode +import androidx.compose.ui.autofill.AutofillType +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.ClipboardManager +import androidx.compose.ui.platform.LocalAutofill +import androidx.compose.ui.platform.LocalAutofillTree import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties @@ -67,10 +92,14 @@ import com.vitorpamplona.amethyst.ui.actions.CloseButton import com.vitorpamplona.amethyst.ui.note.authenticate import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.ButtonPadding +import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.crypto.CryptoUtils +import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.encoders.toNsec import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +@OptIn(ExperimentalComposeUiApi::class) @Composable fun AccountBackupDialog( accountViewModel: AccountViewModel, @@ -80,12 +109,23 @@ fun AccountBackupDialog( onDismissRequest = onClose, properties = DialogProperties(usePlatformDefaultWidth = false), ) { - Surface(modifier = Modifier.fillMaxSize()) { + Surface( + modifier = + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + ) { Column( - modifier = Modifier.background(MaterialTheme.colorScheme.background).fillMaxSize(), + modifier = + Modifier + .background(MaterialTheme.colorScheme.background) + .fillMaxSize(), ) { Row( - modifier = Modifier.fillMaxWidth().padding(10.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(10.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { @@ -93,7 +133,10 @@ fun AccountBackupDialog( } Column( - modifier = Modifier.fillMaxSize().padding(horizontal = 30.dp), + modifier = + Modifier + .fillMaxSize() + .padding(horizontal = 30.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { @@ -101,13 +144,105 @@ fun AccountBackupDialog( style = RichTextStyle().resolveDefaults(), ) { Markdown( - content = stringResource(R.string.account_backup_tips_md), + content = stringResource(R.string.account_backup_tips2_md), ) } - Spacer(modifier = Modifier.height(30.dp)) + Spacer(modifier = Modifier.height(10.dp)) NSecCopyButton(accountViewModel) + + Spacer(modifier = Modifier.height(30.dp)) + + Material3RichText( + style = RichTextStyle().resolveDefaults(), + ) { + Markdown( + content = stringResource(R.string.account_backup_tips3_md), + ) + } + + Spacer(modifier = Modifier.height(10.dp)) + + val password = remember { mutableStateOf(TextFieldValue("")) } + var errorMessage by remember { mutableStateOf("") } + var showCharsPassword by remember { mutableStateOf(false) } + + val autofillNode = + AutofillNode( + autofillTypes = listOf(AutofillType.Password), + onFill = { password.value = TextFieldValue(it) }, + ) + val autofill = LocalAutofill.current + LocalAutofillTree.current += autofillNode + + OutlinedTextField( + modifier = + Modifier + .onGloballyPositioned { coordinates -> + autofillNode.boundingBox = coordinates.boundsInWindow() + } + .onFocusChanged { focusState -> + autofill?.run { + if (focusState.isFocused) { + requestAutofillForNode(autofillNode) + } else { + cancelAutofillForNode(autofillNode) + } + } + }, + value = password.value, + onValueChange = { + password.value = it + if (errorMessage.isNotEmpty()) { + errorMessage = "" + } + }, + keyboardOptions = + KeyboardOptions( + autoCorrect = false, + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Go, + ), + placeholder = { + Text( + text = stringResource(R.string.ncryptsec_password), + color = MaterialTheme.colorScheme.placeholderText, + ) + }, + trailingIcon = { + Row { + IconButton(onClick = { showCharsPassword = !showCharsPassword }) { + Icon( + imageVector = + if (showCharsPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility, + contentDescription = + if (showCharsPassword) { + stringResource(R.string.show_password) + } else { + stringResource( + R.string.hide_password, + ) + }, + ) + } + } + }, + visualTransformation = + if (showCharsPassword) VisualTransformation.None else PasswordVisualTransformation(), + ) + + if (errorMessage.isNotBlank()) { + Text( + text = errorMessage, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + ) + } + + Spacer(modifier = Modifier.height(10.dp)) + + EncryptNSecCopyButton(accountViewModel, password) } } } @@ -160,6 +295,56 @@ private fun NSecCopyButton(accountViewModel: AccountViewModel) { } } +@Composable +private fun EncryptNSecCopyButton( + accountViewModel: AccountViewModel, + password: MutableState, +) { + val clipboardManager = LocalClipboardManager.current + val context = LocalContext.current + val scope = rememberCoroutineScope() + + val keyguardLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { + result: ActivityResult -> + if (result.resultCode == Activity.RESULT_OK) { + encryptCopyNSec(password, context, scope, accountViewModel, clipboardManager) + } + } + + Button( + modifier = Modifier.padding(horizontal = 3.dp), + onClick = { + authenticate( + title = context.getString(R.string.copy_my_secret_key), + context = context, + keyguardLauncher = keyguardLauncher, + onApproved = { encryptCopyNSec(password, context, scope, accountViewModel, clipboardManager) }, + onError = { title, message -> accountViewModel.toast(title, message) }, + ) + }, + shape = ButtonBorder, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + ), + contentPadding = ButtonPadding, + enabled = password.value.text.isNotBlank(), + ) { + Icon( + tint = MaterialTheme.colorScheme.onPrimary, + imageVector = Icons.Default.Key, + contentDescription = + stringResource(R.string.copies_the_nsec_id_your_password_to_the_clipboard_for_backup), + modifier = Modifier.padding(end = 5.dp), + ) + Text( + stringResource(id = R.string.encrypt_and_copy_my_secret_key), + color = MaterialTheme.colorScheme.onPrimary, + ) + } +} + fun Context.getFragmentActivity(): FragmentActivity? { var currentContext = this while (currentContext is ContextWrapper) { @@ -189,3 +374,46 @@ private fun copyNSec( } } } + +private fun encryptCopyNSec( + password: MutableState, + context: Context, + scope: CoroutineScope, + accountViewModel: AccountViewModel, + clipboardManager: ClipboardManager, +) { + if (password.value.text.isBlank()) { + scope.launch { + Toast.makeText( + context, + context.getString(R.string.password_is_required), + Toast.LENGTH_SHORT, + ) + .show() + } + } else { + accountViewModel.account.keyPair.privKey?.let { + val key = CryptoUtils.encryptNIP49(it.toHexKey(), password.value.text) + if (key != null) { + clipboardManager.setText(AnnotatedString(key)) + scope.launch { + Toast.makeText( + context, + context.getString(R.string.secret_key_copied_to_clipboard), + Toast.LENGTH_SHORT, + ) + .show() + } + } else { + scope.launch { + Toast.makeText( + context, + context.getString(R.string.failed_to_encrypt_key), + Toast.LENGTH_SHORT, + ) + .show() + } + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt index 501347bf3..bd3d29274 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt @@ -55,6 +55,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -90,10 +91,12 @@ import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.PackageUtils import com.vitorpamplona.amethyst.ui.MainActivity +import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation import com.vitorpamplona.amethyst.ui.components.getActivity import com.vitorpamplona.amethyst.ui.qrcode.SimpleQrCodeScanner import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog +import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.Size20dp import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.Size40dp @@ -142,6 +145,16 @@ fun LoginPage( val scope = rememberCoroutineScope() var loginWithExternalSigner by remember { mutableStateOf(false) } + var processingLogin by remember { mutableStateOf(false) } + + val password = remember { mutableStateOf(TextFieldValue("")) } + val needsPassword = + remember { + derivedStateOf { + key.value.text.startsWith("ncryptsec1") + } + } + if (loginWithExternalSigner) { val externalSignerLauncher = remember { ExternalSignerLauncher("", signerPackageName = "") } val id = remember { UUID.randomUUID().toString() } @@ -243,33 +256,47 @@ fun LoginPage( Spacer(modifier = Modifier.height(40.dp)) - var showPassword by remember { mutableStateOf(false) } + var showCharsKey by remember { mutableStateOf(false) } + var showCharsPassword by remember { mutableStateOf(false) } - val autofillNode = + val autofillNodeKey = AutofillNode( autofillTypes = listOf(AutofillType.Password), onFill = { key.value = TextFieldValue(it) }, ) + + val autofillNodePassword = + AutofillNode( + autofillTypes = listOf(AutofillType.Password), + onFill = { key.value = TextFieldValue(it) }, + ) + val autofill = LocalAutofill.current - LocalAutofillTree.current += autofillNode + LocalAutofillTree.current += autofillNodeKey + LocalAutofillTree.current += autofillNodePassword OutlinedTextField( modifier = Modifier .onGloballyPositioned { coordinates -> - autofillNode.boundingBox = coordinates.boundsInWindow() + autofillNodeKey.boundingBox = coordinates.boundsInWindow() } .onFocusChanged { focusState -> autofill?.run { if (focusState.isFocused) { - requestAutofillForNode(autofillNode) + requestAutofillForNode(autofillNodeKey) } else { - cancelAutofillForNode(autofillNode) + cancelAutofillForNode(autofillNodeKey) } } }, value = key.value, - onValueChange = { key.value = it }, + onValueChange = { + key.value = it + if (errorMessage.isNotEmpty()) { + errorMessage = "" + } + }, keyboardOptions = KeyboardOptions( autoCorrect = false, @@ -284,12 +311,12 @@ fun LoginPage( }, trailingIcon = { Row { - IconButton(onClick = { showPassword = !showPassword }) { + IconButton(onClick = { showCharsKey = !showCharsKey }) { Icon( imageVector = - if (showPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility, + if (showCharsKey) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility, contentDescription = - if (showPassword) { + if (showCharsKey) { stringResource(R.string.show_password) } else { stringResource( @@ -322,22 +349,32 @@ fun LoginPage( } }, visualTransformation = - if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), + if (showCharsKey) VisualTransformation.None else PasswordVisualTransformation(), keyboardActions = KeyboardActions( onGo = { if (!acceptedTerms.value) { - termsAcceptanceIsRequired = - context.getString(R.string.acceptance_of_terms_is_required) + termsAcceptanceIsRequired = context.getString(R.string.acceptance_of_terms_is_required) } if (key.value.text.isBlank()) { errorMessage = context.getString(R.string.key_is_required) } - if (acceptedTerms.value && key.value.text.isNotBlank()) { - accountStateViewModel.login(key.value.text, useProxy.value, proxyPort.value.toInt()) { - errorMessage = context.getString(R.string.invalid_key) + if (needsPassword.value && password.value.text.isBlank()) { + errorMessage = context.getString(R.string.password_is_required) + } + + if (acceptedTerms.value && key.value.text.isNotBlank() && !(needsPassword.value && password.value.text.isBlank())) { + processingLogin = true + accountStateViewModel.login(key.value.text, password.value.text, useProxy.value, proxyPort.value.toInt()) { + processingLogin = false + errorMessage = + if (it != null) { + context.getString(R.string.invalid_key_with_message, it) + } else { + context.getString(R.string.invalid_key) + } } } }, @@ -353,39 +390,128 @@ fun LoginPage( Spacer(modifier = Modifier.height(10.dp)) - if (PackageUtils.isOrbotInstalled(context)) { - Row(verticalAlignment = Alignment.CenterVertically) { - Checkbox( - checked = useProxy.value, - onCheckedChange = { - if (it) { - connectOrbotDialogOpen = true + if (needsPassword.value) { + OutlinedTextField( + modifier = + Modifier + .onGloballyPositioned { coordinates -> + autofillNodePassword.boundingBox = coordinates.boundsInWindow() } - }, - ) - - Text(stringResource(R.string.connect_via_tor)) - } - - if (connectOrbotDialogOpen) { - ConnectOrbotDialog( - onClose = { connectOrbotDialogOpen = false }, - onPost = { - connectOrbotDialogOpen = false - useProxy.value = true - }, - onError = { - scope.launch { - Toast.makeText( - context, - it, - Toast.LENGTH_LONG, + .onFocusChanged { focusState -> + autofill?.run { + if (focusState.isFocused) { + requestAutofillForNode(autofillNodePassword) + } else { + cancelAutofillForNode(autofillNodePassword) + } + } + }, + value = password.value, + onValueChange = { + password.value = it + if (errorMessage.isNotEmpty()) { + errorMessage = "" + } + }, + keyboardOptions = + KeyboardOptions( + autoCorrect = false, + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Go, + ), + placeholder = { + Text( + text = stringResource(R.string.ncryptsec_password), + color = MaterialTheme.colorScheme.placeholderText, + ) + }, + trailingIcon = { + Row { + IconButton(onClick = { showCharsPassword = !showCharsPassword }) { + Icon( + imageVector = + if (showCharsPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility, + contentDescription = + if (showCharsPassword) { + stringResource(R.string.show_password) + } else { + stringResource( + R.string.hide_password, + ) + }, ) - .show() } - }, - proxyPort, - ) + } + }, + visualTransformation = + if (showCharsPassword) VisualTransformation.None else PasswordVisualTransformation(), + keyboardActions = + KeyboardActions( + onGo = { + if (!acceptedTerms.value) { + termsAcceptanceIsRequired = context.getString(R.string.acceptance_of_terms_is_required) + } + + if (key.value.text.isBlank()) { + errorMessage = context.getString(R.string.key_is_required) + } + + if (needsPassword.value && password.value.text.isBlank()) { + errorMessage = context.getString(R.string.password_is_required) + } + + if (acceptedTerms.value && key.value.text.isNotBlank() && !(needsPassword.value && password.value.text.isBlank())) { + processingLogin = true + accountStateViewModel.login(key.value.text, password.value.text, useProxy.value, proxyPort.value.toInt()) { + processingLogin = false + errorMessage = + if (it != null) { + context.getString(R.string.invalid_key_with_message, it) + } else { + context.getString(R.string.invalid_key) + } + } + } + }, + ), + ) + + Spacer(modifier = Modifier.height(10.dp)) + + if (PackageUtils.isOrbotInstalled(context)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox( + checked = useProxy.value, + onCheckedChange = { + if (it) { + connectOrbotDialogOpen = true + } + }, + ) + + Text(stringResource(R.string.connect_via_tor)) + } + + if (connectOrbotDialogOpen) { + ConnectOrbotDialog( + onClose = { connectOrbotDialogOpen = false }, + onPost = { + connectOrbotDialogOpen = false + useProxy.value = true + }, + onError = { + scope.launch { + Toast.makeText( + context, + it, + Toast.LENGTH_LONG, + ) + .show() + } + }, + proxyPort, + ) + } } } @@ -448,19 +574,33 @@ fun LoginPage( errorMessage = context.getString(R.string.key_is_required) } - if (acceptedTerms.value && key.value.text.isNotBlank()) { - accountStateViewModel.login(key.value.text, useProxy.value, proxyPort.value.toInt()) { - errorMessage = context.getString(R.string.invalid_key) + if (needsPassword.value && password.value.text.isBlank()) { + errorMessage = context.getString(R.string.password_is_required) + } + + if (acceptedTerms.value && key.value.text.isNotBlank() && !(needsPassword.value && password.value.text.isBlank())) { + processingLogin = true + accountStateViewModel.login(key.value.text, password.value.text, useProxy.value, proxyPort.value.toInt()) { + processingLogin = false + errorMessage = + if (it != null) { + context.getString(R.string.invalid_key_with_message, it) + } else { + context.getString(R.string.invalid_key) + } } } }, shape = RoundedCornerShape(Size35dp), modifier = Modifier.height(50.dp), ) { - Text( - text = stringResource(R.string.login), - modifier = Modifier.padding(horizontal = 40.dp), - ) + Row(modifier = Modifier.padding(horizontal = 40.dp)) { + if (processingLogin) { + LoadingAnimation() + Spacer(modifier = DoubleHorzSpacer) + } + Text(stringResource(R.string.login)) + } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6ad75e819..a84536ad2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -142,12 +142,15 @@ Clear App Logo nsec.. or npub.. + password to open the key Show Password Hide Password Invalid key + Invalid key: %1$s "I accept the " terms of use Acceptance of terms is required + Password is required Key is required A name is required Login @@ -200,15 +203,22 @@ Mark all New as read Mark all as read Backup Keys - + ## Key Backup and Safety Tips - \n\nYour account is secured by a secret key. The key is long random string starting with **nsec1**. Anyone who has access to your secret key can publish content using your identity. + \n\nYour account is secured by a secret key. The key is a long sequence of characters starting with **nsec1**. Anyone who has access to this secret key can post and change your identity. \n\n- Do **not** put your secret key in any website or software you do not trust. - \n- Amethyst developers will **never** ask you for your secret key. + \n- Amethyst developers will **never** ask for your secret key. \n- **Do** keep a secure backup of your secret key for account recovery. We recommend using a password manager. + + For additional security, you can encrypt your key with a password. This key starts with **ncryptsec1** and cannot be used without your password. + \n\nIf you lose your password, you will not be able to recover your key. + + + Failed to encrypt your private key Secret key (nsec) copied to clipboard Copy my secret key + Encrypt and copy my secret key Authentication failed Biometrics failed to authenticate the owner of this phone Biometrics failed to authenticate the owner of this phone. Error: %1$s diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/CryptoUtils.kt b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/CryptoUtils.kt index 65e093780..d0e366d02 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/CryptoUtils.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/CryptoUtils.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.quartz.crypto import android.util.Log +import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.events.Event import fr.acinq.secp256k1.Secp256k1 import java.security.MessageDigest @@ -34,6 +35,7 @@ object CryptoUtils { private val nip04 = Nip04(secp256k1, random) private val nip44v1 = Nip44v1(secp256k1, random) private val nip44v2 = Nip44v2(secp256k1, random) + private val nip49 = Nip49(secp256k1, random) fun clearCache() { nip04.clearCache() @@ -262,6 +264,21 @@ object CryptoUtils { } } + fun decryptNIP49( + payload: String, + password: String, + ): String? { + if (payload.isEmpty() || password.isEmpty()) return null + return nip49.decrypt(payload, password) + } + + fun encryptNIP49( + key: HexKey, + password: String, + ): String? { + return nip49.encrypt(key, password, 16, Nip49.EncryptedInfo.CLIENT_DOES_NOT_TRACK) + } + class EncryptedInfoString( val ciphertext: String, val nonce: String, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip49.kt b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip49.kt index 2d53fc2d7..2a282e5e3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip49.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip49.kt @@ -39,14 +39,14 @@ class Nip49(val secp256k1: Secp256k1, val random: SecureRandom) { fun decrypt( nCryptSec: String, password: String, - ): HexKey? { + ): HexKey { return decrypt(EncryptedInfo.decodePayload(nCryptSec), password) } fun decrypt( encryptedInfo: EncryptedInfo?, password: String = "", - ): String? { + ): String { check(encryptedInfo != null) { "Couldn't decode key" } check(encryptedInfo.version == EncryptedInfo.V) { "invalid version" } @@ -66,6 +66,8 @@ class Nip49(val secp256k1: Secp256k1, val random: SecureRandom) { key, ) + check(m.any { it > 0 }) { "Incorrect password" } + return m.toHexKey() } @@ -129,9 +131,9 @@ class Nip49(val secp256k1: Secp256k1, val random: SecureRandom) { companion object { const val V: Byte = 0x02 - const val UNSAFE_HANDLING = 0x00 - const val SAFE_HANDLING = 0x01 - const val CLIENT_DOES_NOT_TRACK = 0x02 + const val UNSAFE_HANDLING: Byte = 0x00 + const val SAFE_HANDLING: Byte = 0x01 + const val CLIENT_DOES_NOT_TRACK: Byte = 0x02 fun decodePayload(nCryptSec: String): EncryptedInfo? { val byteArray = From 7a213545a2afd9fb8e68559c8dce948d40546f2d Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Wed, 14 Feb 2024 17:56:37 +0000 Subject: [PATCH 076/176] New Crowdin translations by GitHub Action --- app/src/main/res/values-ar/strings.xml | 6 --- app/src/main/res/values-bn-rBD/strings.xml | 6 --- app/src/main/res/values-cs/strings.xml | 6 --- app/src/main/res/values-de/strings.xml | 5 --- app/src/main/res/values-el-rGR/strings.xml | 6 --- app/src/main/res/values-eo/strings.xml | 6 --- app/src/main/res/values-es-rES/strings.xml | 6 --- app/src/main/res/values-es-rMX/strings.xml | 6 --- app/src/main/res/values-es-rUS/strings.xml | 6 --- app/src/main/res/values-fa/strings.xml | 6 --- app/src/main/res/values-fi-rFI/strings.xml | 6 --- app/src/main/res/values-fr/strings.xml | 6 --- app/src/main/res/values-hu/strings.xml | 49 +++++++++++++++++++--- app/src/main/res/values-in-rID/strings.xml | 6 --- app/src/main/res/values-it-rIT/strings.xml | 6 --- app/src/main/res/values-ja/strings.xml | 6 --- app/src/main/res/values-nl/strings.xml | 6 --- app/src/main/res/values-pt-rBR/strings.xml | 6 --- app/src/main/res/values-ru/strings.xml | 6 --- app/src/main/res/values-sv-rSE/strings.xml | 5 --- app/src/main/res/values-sw-rKE/strings.xml | 6 --- app/src/main/res/values-ta/strings.xml | 6 --- app/src/main/res/values-th/strings.xml | 6 --- app/src/main/res/values-uk/strings.xml | 6 --- app/src/main/res/values-zh-rCN/strings.xml | 5 --- app/src/main/res/values-zh-rHK/strings.xml | 6 --- app/src/main/res/values-zh-rTW/strings.xml | 6 --- 27 files changed, 43 insertions(+), 159 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index fd4b04a0f..6242ab259 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -193,12 +193,6 @@ ضع علامة على جميع الرسائل/الاشعارات الجديدة كمقروء ضع علامة على جميع الرسائل/الاشعارات كمقروء قم بعمل نسخة احتياطية للمفاتيح - ## النصائح الرئيسية للنسخ الاحتياطي والسلامة - \n\n- يتم تأمين حسابك بمفتاح سري. المفتاح السري هو سلسلة عشوائية طويلة تبدأ بـ **nsec1**. أي شخص لديه حق الوصول إلى مفتاحك السري يمكنه نشر المحتوى باستخدام هويتك. - \n\n- لا تقم بوضع مفتاحك السري في أي موقع أو برنامج لا تثق به. - \n- مطوري Amethyst لن يطلبوا منك **أبدا** المفتاح السري الخاص بك. - \n- حافظ على نسخة احتياطية آمنة لمفتاحك السري لاسترداد الحساب. نوصي باستخدام برنامج لادارة كلمات المرور. - مفتاحك السري (nsec) قد تم نسخه في الحافظة إنسخ مفتاحي السري فشلت المصادقة diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml index 45ac5238e..952082ed4 100644 --- a/app/src/main/res/values-bn-rBD/strings.xml +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -182,12 +182,6 @@ নতুন সবগুলোকে পঠিত হিসেবে চিহ্নিত করুন সবগুলোকে পঠিত হিসেবে চিহ্নিত করুন ব্যাকআপ চাবিগুলি - ## চাবির ব্যাকআপ এবং নিরাপত্তা বিষয়ক টিপস - \n\nআপনার অ্যাকাউন্ট একটি ব্যক্তিগত চাবি দ্বারা সুরক্ষিত। চাবি হলো এলোমেলো অক্ষর ও সংখ্যার একটি দীর্ঘ তন্তু যা **nsec** দ্বারা শুরু হয়। আপনার ব্যক্তিগত চাবিটি পেলে যে কেউ আপনার পরিচয় ব্যবহার করে যেকোনো আধেয় প্রকাশ করতে পারবে। - \n\n- আপনি বিশ্বাস করেন না এমন কোনো ওয়েবসাইট কিংবা সফটওয়্যারে আপনার ব্যক্তিগত চাবিটি রাখবেন **না**। - \n- অ্যামেথিস্টের ডেভেলপাররা **কখনোই** আপনার কাছে আপনার ব্যক্তিগত চাবিটি জানতে চাইবে না। - \n- আপনার ব্যক্তিগত চাবির একটি নিরাপদ ব্যাকআপ **অবশ্যই** সংরক্ষণ করবেন কারণ এটি আপনার অ্যাকাউন্ট পুনরুদ্ধারে কাজে আসবে। আমরা একটি পাসওয়ার্ড ম্যানেজার ব্যবহারের পরামর্শ দিই। - ব্যক্তিগত চাবি (nsec) ক্লিপবোর্ডে কপি করা হয়েছে আমার ব্যক্তিগত চাবিটি কপি করুন প্রমাণীকরণ ব্যর্থ হয়েছে। diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index dccb96054..cf1677fa3 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -195,12 +195,6 @@ Označit všechny nové jako přečtené Označit všechny jako přečtené Zálohovat klíče - ## Tipy na zálohování a bezpečnost účtu - \n\nVáš účet je zabezpečen tajným klíčem. Klíč je dlouhý náhodný řetězec začínající **nsec1**. Každý, kdo má přístup k vašemu tajnému klíči, může publikovat obsah pod vaší identitou. - \n\n- **Nepoužívejte** svůj tajný klíč na webových stránkách nebo v softwaru, kterému nedůvěřujete. - \n- Vývojáři aplikace Amethyst vás **nikdy** nebudou žádat o váš tajný klíč. - \n- **Uchovávejte** si bezpečnou zálohu svého tajného klíče pro obnovení účtu. Doporučujeme používat správce hesel. - Tajný klíč (nsec) zkopírován do schránky Zkopírovat můj tajný klíč Autentizace se nezdařila diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index dbc869010..108502f82 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -199,11 +199,6 @@ anz der Bedingungen ist erforderlich Alle als neu markieren Alle als gelesen markieren Schlüssel sichern - ## Sicherung und Sicherheitshinweise für Schlüssel - \n\nIhr Konto ist durch einen geheimen Schlüssel geschützt. Der Schlüssel ist eine lange zufällige Zeichenfolge, die mit **nsec1** beginnt. Jeder, der Zugriff auf Ihren geheimen Schlüssel hat, kann Inhalte unter Verwendung Ihrer Identität veröffentlichen. - \n\n- Geben Sie Ihren geheimen Schlüssel **nicht** auf Websites oder Software ein, denen Sie nicht vertrauen. - \n- Die Entwickler von Amethyst werden Sie **niemals** nach Ihrem geheimen Schlüssel fragen. - \n- Bewahren Sie eine sichere Sicherungskopie Ihres geheimen Schlüssels zur Kontowiederherstellung auf. Wir empfehlen die Verwendung eines Passwort-Managers. Geheimer Schlüssel (nsec) in die Zwischenablage kopiert Meinen geheimen Schlüssel kopieren Authentifizierung fehlgeschlagen diff --git a/app/src/main/res/values-el-rGR/strings.xml b/app/src/main/res/values-el-rGR/strings.xml index 9f50c750f..8cd59e916 100644 --- a/app/src/main/res/values-el-rGR/strings.xml +++ b/app/src/main/res/values-el-rGR/strings.xml @@ -185,12 +185,6 @@ Σήμανση όλων των νέων δημοσιεύσεων ως αναγνωσμένων Σήμανση όλων ως αναγνωσμένα Δημιουργία Αντιγράφου Ασφαλείας των Προσωπικών σας Κλειδιών - ## Οδηγίες για τη δημιουργία Αντιγράφου Ασφαλείας του \"Μυστικού Κλειδιού\" σας. - \n\nΟ Λογαριασμός σας προστατεύεται από το \"Μυστικό Κλειδί\" σας. Το \"Μυστικό Κλειδί\"σας αποτελείται από μια μεγάλη σειρά αλφαριθμητικών χαρακτήρων και ξεκινάει πάντα με \"nsec1\". -Οποιοσδήποτε έχει πρόσβαση στο \"Μυστικό Κλειδί\" σας μπορεί να δημοσιεύσει περιεχόμενο και να έχει πλήρη πρόσβαση στον λογαριασμό σας. - \n\n- Μην αντιγράφετε *Ποτέ* το \"Μυστικό Κλειδί\" σας σε ιστοσελίδα ή λογισμικό που δεν εμπιστεύεστε. - \n- Οι προγραμματιστές του Amethyst δεν θα σας ζητήσουν *Ποτέ* το \"Μυστικό Κλειδί\" σας. - \n- Δημιουργείστε ένα Ασφαλές Αντίγραφο του \"Μυστικού Κλειδιού\" σας για να μπορείτε να επαναφέρετε τον λογαριασμό σας. Προτείνουμε να χρησιμοποιήσετε ένα πρόγραμμα διαχειρίσης κωδικών. Το \"Μυστικό Κλειδί\" (nsec) αντιγράφηκε στο πρόχειρο Αντιγραφή του \"Μυστικού Κλειδιού\" μου Αποτυχία ταυτοποίησης diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 0adee9665..90b23a36d 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -182,12 +182,6 @@ Marki ĉiujn Novajn legitaj Marki ĉiujn legitajn Sekurkopii Ŝlosilojn - ## Sekurkopio de Ŝlosiloj kaj Rekomendoj de Sekureco - \n\nVia konto estas sekurigita per sekreta ŝlosilo. La ŝlosilo estas longa hazarda literĉeno komencata kun **nsec1**. Ĉiu, kiu havas aliron al via sekreta ŝlosilo, povas publikigi enhavon per via identeco. - \n\n- **Ne** metu vian sekretan ŝlosilon en ajna retejo aŭ programaro, kiun vi ne fidas. - \n- La programistoj de Amethyst **neniam** petos de vi vian sekretan ŝlosilon. - \n- **Ja** konservu sekurkopion de via sekreta ŝlosilo por reakiro de konto. Ni rekomendas uzi pasvortmanaĝilon. - Sekreta ŝlosilo (nsec) kopiita al tondujo Kopii mian sekretan ŝlosilon Aŭtentikigo malsukcesis diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 2227e3d4f..28e6cf1a0 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -194,12 +194,6 @@ Marcar los nuevos como leídos Marcar todos como leídos Claves de respaldo - ## Consejos sobre la protección y la copia de seguridad de las claves - \n\nTu cuenta está protegida por una clave secreta. Esta clave es una cadena aleatoria y larga que empieza por **nsec1**. Cualquiera que tenga acceso a tu clave secreta puede publicar contenido usando tu identidad. - \n\n- **No** guardes la clave secreta en ningún sitio web o software en los que no confíes. - \n- Los desarrolladores de Amethyst **nunca** te pedirán la clave secreta. - \n- **Sí** puedes realizar una copia de seguridad de la clave secreta para recuperar la cuenta. Para ello, te recomendamos usar un gestor de contraseñas. - Clave secreta (nsec) copiada al portapapeles Copiar mi clave secreta Autenticación fallida diff --git a/app/src/main/res/values-es-rMX/strings.xml b/app/src/main/res/values-es-rMX/strings.xml index 1c00aa805..de402ba42 100644 --- a/app/src/main/res/values-es-rMX/strings.xml +++ b/app/src/main/res/values-es-rMX/strings.xml @@ -194,12 +194,6 @@ Marcar todos los nuevos como leídos Marcar todos como leídos Respaldar claves - ## Consejos sobre la protección y la copia de seguridad de las claves - \n\nTu cuenta está protegida por una clave secreta. Esta clave es una cadena aleatoria y larga que empieza por **nsec1**. Cualquiera que tenga acceso a tu clave secreta puede publicar contenido usando tu identidad. - \n\n- **No** guardes la clave secreta en ningún sitio web o software en los que no confíes. - \n- Los desarrolladores de Amethyst **nunca** te pedirán la clave secreta. - \n- **Sí** puedes realizar una copia de seguridad de la clave secreta para recuperar la cuenta. Para ello, te recomendamos usar un gestor de contraseñas. - Clave secreta (nsec) copiada al portapapeles Copiar mi clave secreta Error de autenticación diff --git a/app/src/main/res/values-es-rUS/strings.xml b/app/src/main/res/values-es-rUS/strings.xml index 126a537c5..173e79d01 100644 --- a/app/src/main/res/values-es-rUS/strings.xml +++ b/app/src/main/res/values-es-rUS/strings.xml @@ -194,12 +194,6 @@ Marcar todos los nuevos como leídos Marcar todos como leídos Respaldar claves - ## Consejos sobre la protección y la copia de seguridad de las claves - \n\nTu cuenta está protegida por una clave secreta. Esta clave es una cadena aleatoria y larga que empieza por **nsec1**. Cualquiera que tenga acceso a tu clave secreta puede publicar contenido usando tu identidad. - \n\n- **No** guardes la clave secreta en ningún sitio web o software en los que no confíes. - \n- Los desarrolladores de Amethyst **nunca** te pedirán la clave secreta. - \n- **Sí** puedes realizar una copia de seguridad de la clave secreta para recuperar la cuenta. Para ello, te recomendamos usar un gestor de contraseñas. - Clave secreta (nsec) copiada al portapapeles Copiar mi clave secreta Error de autenticación diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index a437c1bab..ac0627933 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -194,12 +194,6 @@ همه جدیدها را خوانده شده کن همه را خوانده شده کن بازیابی کلیدها - ## راهنمای ایمنی و بازیابی کلید - \n\n- حساب کاربری شما با یک کلید خصوصی ایمن می شود. این کلید یک رشته تصادفی اعداد و ارقام است که با **nsec1** آغاز می گردد. - \n\n- هر کس به کلید خصوصی شما دست یابد می تواند با هویت شما اقدام به انتشار محتوا نماید. کلید خصوصی خود را در هیچ نرم افزار یا وب سایتی که به آن اعتماد ندارید قرار ندهید. - \n- سازندگان آماتیست **هرگز** از شما کلید خصوصی تان را نمی خواهند. - \n- حتما یک کپی از کلید خصوصی را برای بازیابی حساب کاربری نزد خود نگه دارد. ما استفاده از یک نرم افزار مدیریت پسورد را توصیه می نماییم. - کلید خصوصی در کلیپبورد کپی شد کلید خصوصیم را کپی کن احراز هویت انجام نشد diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index 4f0a28b37..6b403d633 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -185,12 +185,6 @@ Merkitse kaikki Uudet luetuiksi Merkitse kaikki luetuiksi Varmuuskopioi Avaimet - ## Avaimen varmuuskopiointi ja turvallisuusvinkit - \n\nTiliäsi suojaa salainen avain. Avain on pitkä satunnainen merkkijono, joka alkaa **nsec1**. Kaikki, joilla on pääsy salaiseen avaimeesi, voivat julkaista sisältöä käyttäen identiteettiäsi. - \n\n- Älä **koskaan** laita salaista avaintasi mihinkään verkkosivustoon tai ohjelmistoon, johon et luota. - \n- Amethyst-kehittäjät eivät **koskaan** kysy sinulta salaista avaintasi. - \n- **Säilytä** salaisen avaimen varmuuskopio turvallisesti tilin palauttamista varten. Suosittelemme salasanahallintaohjelman käyttämistä. - Salainen avain (nsec) kopioitu leikepöydälle Kopioi salainen avain Tunnistautuminen epäonnistui diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 78f2ca027..d5aa2ee2e 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -195,12 +195,6 @@ Marquer tous les Nouveaux comme lu Tout marquer comme lu Clés de sauvegarde - ## Sauvegarde des clés et conseils de sécurité - \n\nVotre compte est sécurisé par une clé secrète. La clé est une longue chaîne aléatoire commençant par **nsec1**. Toute personne ayant accès à votre clé secrète peut publier du contenu en utilisant votre identité. - \n\n- Ne communiquez **jamais** votre clé secrète à un site Web ou un logiciel auquel vous ne faites pas confiance. - \n- Les développeurs d\'Amethyst ne vous demanderont **jamais** votre clé secrète. - \n- **Conservez** une sauvegarde sécurisée de votre clé secrète pour la récupération de compte. Nous vous recommandons d\'utiliser un gestionnaire de mots de passe. - Clé secrète (nsec) copiée dans le presse-papiers Copier ma clé secrète Échec de l\'authentification diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 73643d7bc..6d18c0e8e 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -3,6 +3,7 @@ Mutass a QR kódra QR kód megjelenítése Profil kép + Profilkép QR kód beolvasása Mutasd A bejegyzést nem megfelelőként jelölte meg @@ -194,12 +195,6 @@ Az összes új megjelölése olvasottként Összes megjelölése olvasottként Biztonsági Kulcsok - ## Kulcs- és biztonsági mentési tippek - \n\nFiókját titkos kulcs védi. A kulcs egy hosszú véletlenszerű karakterlánc, amely **nsec1**-el kezdődik. Bárki, aki az Ön titkos kulcsához hozzáfér, az Ön személyazonosságának használatával bármilyen tartalmat közzétehet. - \n\n- **Ne** helyezze el titkos kulcsát olyan webhelyen vagy szoftverben, amelyben nem bízik. - \n- Az Amethyst fejlesztők a titkos kulcsodat **soha** nem fogják elkérni. - \n- A fiók-helyreállításhoz, titkos kulcsáról **mindig** készítsen biztonságos biztonsági másolatot. Javasoljuk a jelszókezelő használatát. - A titkos kulcs (nsec) a vágólapra másolva A titkos kulcsom másolása Hitelesítés nem sikerült @@ -453,6 +448,7 @@ Az Amethystnek ennek a módnak az aktiválásához NIP-24 üzenetet kell küldenie (GiftWrapped, Zárolt direkt és csoportos üzeneteket). A NIP-24 új, és a legtöbb kliens még nem implementálta. Győződj meg arról, hogy a fogadó fél kompatibilis klienst használ. Aktiválás Publikus + Új nyilvános vagy privát csoport Privát Címzett Téma @@ -469,6 +465,7 @@ URL előnézetek megjelenítése Mikor kell a képeket betölteni Másolás a vágólapra + Npub másolása a vágólapra Az URL vágólapra másolása A bejegyzésazonosító vágólapra másolása Létrehozva @@ -597,4 +594,44 @@ A szerver a feltöltés után nem adott URL-t Nem sikerült a szerverről a feltöltött médiát letölteni Nem sikerült előkészíteni a helyi fájlt a feltöltésre: %1$s + Bejelentkezés QR kóddal + Útvonal + Főoldal + Keresés + Felfedezés + Üzenetek + Értesítések + Globális + Rövidfilmek + Biztonsági szűrők + Új bejegyzés + Új rövidfilmek: képek vagy videók + Új közösségi bejegyzés + Nyissa meg a bejegyzésre adott összes reakciót + Zárja be a bejegyzésre adott összes reakciót + Válasz + Megosztás vagy Idézés + Like + Zap + %1$s profilképe + %1$s csomópont + Csomópont lista kibontása + Bejegyzés opciók + Csomópont lista választó + Szavazás + Szavazás kikapcsolása + Bitcoin számla + Bitcoin számla visszavonása + Egy tétel eladásának visszavonása + ZapGyűjtés + ZapGyűjtés visszavonása + Pozíció + Pozíció törlése + Zap megosztások + Zap megosztások visszavonása + Tartalomra vonatkozó figyelmeztetés + Tartalomra vonatkozó figyelmeztetés visszavonása + Az npub megjelenítése QR-kódként + Érvénytelen cím + Az Amethyst kapott egy URI-t a megnyitáshoz, de az érvénytelen volt: %1$s diff --git a/app/src/main/res/values-in-rID/strings.xml b/app/src/main/res/values-in-rID/strings.xml index 8fb6e9540..70e72e198 100644 --- a/app/src/main/res/values-in-rID/strings.xml +++ b/app/src/main/res/values-in-rID/strings.xml @@ -185,12 +185,6 @@ Tandai semua yang baru sebagai telah dibaca Tandai semua sebagai telah dibaca Kunci Cadangan - ## Kiat Pencadangan dan Keamanan Kunci - \n\nAkun Anda diamankan dengan kunci rahasia. Kunci ini berupa string acak panjang yang dimulai dengan **nsec1**. Siapa pun yang memiliki akses ke kunci rahasia Anda dapat mempublikasikan konten menggunakan identitas Anda. - \n\n- **Jangan** memasukkan kunci rahasia Anda ke situs web atau aplikasi apa pun yang tidak Anda percayai. - \n- Pengembang Amethyst **tidak akan pernah** meminta kunci rahasia Anda. - \n- **Simpanlah** cadangan kunci rahasia Anda dengan aman untuk pemulihan akun. Kami sarankan menggunakan aplikasi pengelola kata sandi. - Kunci rahasia (nsec) telah disalin ke papan klip Salin kunci rahasia saya Autentikasi gagal diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml index 35cbbb8a7..abf7126ae 100644 --- a/app/src/main/res/values-it-rIT/strings.xml +++ b/app/src/main/res/values-it-rIT/strings.xml @@ -182,12 +182,6 @@ Segna tutti i nuovi come letti Segna tutti come letti Esegui backup delle chiavi - ## Backup delle Chiavi e Consigli di Sicurezza - \n\nIl tuo account è reso sicuro da una chiave privata. La chiave è una lunga stringa di caratteri casuali che inizia con **nsec1**. Chiunque abbia accesso alla tua chiave privata puó pubblicare contenuti con la tua identitá. - \n\n- Non devi **mai** inserire la tua chiave privata in un sito o software di cui non ti fidi. - \n- Gli sviluppatori di Amethyst non chiederanno **mai** la tua chiave privata. - \n- Mantieni **sempre** al sicuro un backup della tua chiave privata per recuperare l\'account. Ti suggeriamo di usare un password manager. - Chiave segreta (nsec) copiata negli appunti Copia la mia chiave segreta Autenticazione fallita diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 2aff0899b..ed0a8fe01 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -177,12 +177,6 @@ すべての「新規リクエスト」を既読にする すべて既読にする 鍵をバックアップ - ## 鍵のバックアップと安全に利用するためのTips - \n\nあなたのアカウントは秘密鍵によって保護されています。秘密鍵は **nsec1** から始まる長いランダムな文字列です。秘密鍵にアクセスできる人は、あなたのアカウントを使用してコンテンツを公開することができます。 - \n\n- 信頼できないウェブサイトやソフトウェアには、秘密鍵を **渡さない** でください。 - \n- Amethystの開発者があなたに秘密鍵をお聞きすることは **ありません** 。 - \n- アカウント復旧のために、秘密鍵を安全な方法で **バックアップして** ください 。パスワードマネージャーの利用を推奨しています。 - 秘密鍵 (nsec) をクリップボードにコピーしました 秘密鍵をコピー 認証に失敗しました diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index c212df65a..ea7fb69b2 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -195,12 +195,6 @@ Nieuwe markeren als gelezen Alles markeren als gelezen Back-up sleutels - ## Sleutel back-up en veiligheidstips - \n\nUw account is beveiligd met een privésleutel. De sleutel is een lange, willekeurige reeks die begint met **nsec1**. Iedereen die toegang heeft tot uw privésleutel kan inhoud publiceren met uw identiteit. - \n\n- Voer uw privésleutel **nooit** in een website of software die u niet vertrouwt. - \n- Amethyst ontwikkelaars zullen u **nooit** om uw privésleutel vragen. - \n- **Bewaar** een veilige back-up van uw privésleutel voor accountherstel. Wij raden u aan een wachtwoordmanager te gebruiken. - Privésleutel (nsec) gekopieerd naar klembord Kopieer mijn privésleutel Authenticatie mislukt diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 7fb37dfad..92961c41f 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -195,12 +195,6 @@ Marcar todas novas solicitações como lidas Marcar todas como lidas Copia de segurança das chaves - ## Backup de chaves e dicas de segurança - \n\nSua conta é protegida por uma chave secreta. A chave é uma string aleatória longa começando com **nsec1**. Qualquer pessoa que tenha acesso à sua chave secreta pode publicar conteúdo usando sua identidade. - \n\n- **Não** coloque sua chave secreta em qualquer site ou software em que não confie. - \n- Os desenvolvedores do Amethyst **nunca** pedirão sua chave secreta. - \n- **Faça** um backup seguro de sua chave secreta para recuperação de conta. Recomendamos o uso de um gerenciador de senhas. - Chave secreta (nsec) copiada Copiar minha chave secreta Autenticação falhou diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index d18eee40f..bdc8e3ab0 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -183,12 +183,6 @@ Отметить все новые прочитанными Отметить всё прочитанным Резервное копирование - ## Резервное копирование и советы по безопасности - \n\nВаш аккаунт защищен приватным ключом. Этот ключ – длинная случайная строка с приставкой **nsec1**. Любой, кто владеет вашим приватным ключом, может писать от вашего имени. - \n\n- **Никогда** не вставляйте ваш приватный ключ на сайтах или в приложениях, которым вы не доверяете. - \n- Разработчики Amethyst **никогда** не спросят ваш приватный ключ. - \n- **Сохраните** резервную копию приватного ключа, чтобы иметь возможность восстановить аккаунт. Советуем использовать менеджер паролей. - Приватный ключ (nsec) скопирован Скопировать мой приватный ключ Ошибка входа diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index c4e45a883..1ed54d455 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -195,11 +195,6 @@ Markera alla nya som lästa Markera allt som läst Backup-nycklar - ## Nyckelsäkerhetskopiering och säkerhetstips - \n\nDitt konto är skyddat av en hemlig nyckel. Nyckeln är en lång slumpmässig sträng som börjar med **nsec1**. Alla som har tillgång till din hemliga nyckel kan publicera innehåll med din identitet. - \n\n- Lägg **inte** in din hemliga nyckel på någon webbplats eller programvara som du inte litar på. - \n- Amethyst-utvecklare kommer **aldrig** att be dig om din hemliga nyckel. - \n- **Behåll en säker säkerhetskopia av din hemliga nyckel för kontoåterställning. Vi rekommenderar att du använder en lösenordshanterare. Hemlig nyckel (nsec) kopierad till urklipp Kopiera min hemliga nyckel Autentisering misslyckades diff --git a/app/src/main/res/values-sw-rKE/strings.xml b/app/src/main/res/values-sw-rKE/strings.xml index c56643d93..728922268 100644 --- a/app/src/main/res/values-sw-rKE/strings.xml +++ b/app/src/main/res/values-sw-rKE/strings.xml @@ -182,12 +182,6 @@ Funga kama Mpya Funga kama Zimejulikana Nakili Nakala za Ufunguo - ## Ufundi wa Nakala ya Ufunguo na Usalama - \n\nAkaunti yako inalindwa na ufunguo wa siri. Ufunguo ni mfululizo mrefu wa herufi zisizo na mpangilio, ukitangulia na **nsec1**. Mtu yeyote aliye na ufikiaji wa ufunguo wako wa siri anaweza kuchapisha maudhui kwa kutumia kitambulisho chako. - \n\n- **Usiweke** ufunguo wako wa siri kwenye tovuti au programu usio na imani. - \n- Watengenezaji wa Amethyst **hawata** kuuliza ufunguo wako wa siri. - \n- **Tunashauri** kuwa na nakala rudufu salama ya ufunguo wako wa siri kwa ajili ya kupata akaunti. Tunapendekeza kutumia meneja wa nywila. - Ufunguo wa siri (nsec) umeinakiliwa kwenye ubao wa kunakili Nakili Ufunguo Wangu wa Siri Uthibitishaji umeshindwa diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 99d6913fe..4a82494ad 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -171,12 +171,6 @@ புதியனவற்றைப் படித்ததாகக் குறி அனைத்தையும் படித்ததாகக் குறி காப்புச் சாவிகள் - ## முக்கிய காப்புப்பிரதி மற்றும் பாதுகாப்பு உதவிக்குறிப்புகள் - \n\n உங்கள் கணக்கு ஒரு ரகசியசாவியால் பாதுகாக்கப்படுகிறது. சாவி **nsec1** என்று தொடங்கும் நீண்ட சீரற்ற எழுத்து வடிவம். உங்கள் ரகசிய சாவியை அணுகக்கூடிய எவரும் உங்கள் அடையாளத்தைப் பயன்படுத்தி உள்ளடக்கத்தை வெளியிடலாம். - \n\n- நீங்கள் நம்பாத எந்த வலைத்தளம் அல்லது மென்பொருளிலும் உங்கள் ரகசிய விசையை வைக்க **வேண்டாம்**. - \ n- அமேதிஸ்ட் டெவலப்பர்கள் உங்கள் ரகசியசாவியை உங்களிடம் எப்போதும் கேட்க மாட்டார்கள். - \ n- கணக்கு மீட்டெடுப்பதற்கு உங்கள் ரகசியசாவியின் பாதுகாப்பான காப்புப்பிரதியை **தவறாமல்** வைத்திருங்கள். கடவுச்சொல் நிர்வாகியைப் பயன்படுத்த பரிந்துரைக்கிறோம். - ரகசிய சாவி (nsec) கிளிப்போர்டுக்கு நகலெடுக்கப்பட்டது எனது ரகசிய சாவியை நகலெடுக்கவும் அங்கீகரிப்பு தோல்வியுற்றது diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 211b7c36c..55de47ddb 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -185,12 +185,6 @@ ทำเครื่องหมายว่าอ่านแล้วทั้งหมดสำหรับข้อความใหม่ ทพเครื่องหมายว่าอ่านแล้วทั้งหมด สำรองข้อมูลรหัสลับ - ## เคล็ดลับการสํารองข้อมูลและความปลอดภัยที่สําคัญ - \n\n บัญชีของคุณมีความปลอดภัยด้วยรหัสลับ key คือสตริงสุ่มยาวที่ขึ้นต้นด้วย **nsec1** ทุกคนที่มีสิทธิ์เข้าถึงรหัสลับของคุณสามารถเผยแพร่เนื้อหาโดยใช้บัญชีของคุณได้ - \n\n- **อย่า** ใส่รหัสลับของคุณในเว็บไซต์หรือซอฟต์แวร์ที่คุณไม่เชื่อถือ - \n- นักพัฒนา Amethyst จะ **ไม่เคย** ขอรหัสลับของคุณ - \n- **เก็บ** สําเนาสํารองคีย์ลับของคุณไว้อย่างปลอดภัยสําหรับการกู้คืนบัญชี เราขอแนะนําให้ใช้ password manager. - คัดลอก Secret key (nsec) ลงคลิปบอร์ด คัดลอก secret key ของฉัน เกิดข้อผิดพลาดในการตรวจสอบ diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index b25a4a71e..45f7def09 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -191,12 +191,6 @@ Позначити всі нові прочитаними Позначити все прочитаним Резервне копіювання - ## Резервне копіювання та поради з безпеки - \n\nВаш акаунт захищений приватним ключем. Цей ключ – довгий випадковий рядок із приставкою **nsec1**. Той, хто володіє вашим приватним ключем, може писати від вашого імені. - \n\n- **Ніколи** не вставляйте ваш приватний ключ на сайтах або в додатках, яким ви не довіряєте. - \n- Розробники Amethyst **ніколи** не спитають ваш приватний ключ. - \n- **Збережіть** резервну копію приватного ключа, щоб мати можливість відновити акаунт. Радимо використовувати менеджер паролів. - Приватний ключ (nsec) скопійовано Скопіювати мій приватний ключ Помилка входу diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index ac4f6de64..b6cdbb6e2 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -195,11 +195,6 @@ 将所有新内容标记为已读 将所有内容标记为已读 备份密匙 - ## 备份与安全提示 - \n\n您的帐户由一个私人密钥保护。 密钥是以**nsec1**开头的长随机字符串。任何拥有您的私人密钥的人都可以使用您的身份发布内容。 - \n\n- **不要**将您的私人密钥添加到您不信任的任何网站或软件,亦不要在网上公开。 - \n- Amethyst 开发人员**永远不会**要求您提供私人密钥。 - \n- **请**保留您的私人密钥的安全备份,以备帐户恢复。 我们建议使用密码管理器。 私人密钥(nsec)已复制到剪贴板 复制我的私人密钥 身份验证失败 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 9b4f4ae42..a5afbefe4 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -172,12 +172,6 @@ 將所有新內容標記為已讀 將所有內容標記為已讀 備份密匙 - ## 備份與安全提示 - \n\n您的帳户由一個私人密鑰保護。 密鑰是以**nsec1**開頭的長隨機字符串。任何擁有您的私人密鑰的人都可以使用您的身份發佈內容。 - \n\n- **不要**將您的私人密鑰添加到您不信任的任何網站或軟件,亦不要在網上公開。 - \n- Amethyst 開發人員**永遠不會**要求您提供私人密鑰。 - \n- **請**保留您的私人密鑰的安全備份,以備帳户恢復。 我們建議使用密碼管理器。 - 私人密鑰(nsec)已複製到剪貼板 複製我的私人密鑰 身份驗證失敗 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 13b3ecedf..2d0cb6793 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -195,12 +195,6 @@ 將所有新內容標記為已讀 將所有內容標記為已讀 備份密鑰 - ## 備份與安全提示 - \n\n您的帳户由一個私人密鑰保護。 密鑰是以**nsec1**開頭的長隨機字符串。任何擁有您的私人密鑰的人都可以使用您的身份發佈內容。 - \n\n- **不要**將您的私人密鑰添加到您不信任的任何網站或軟件,亦不要在網上公開。 - \n- Amethyst 開發人員**永遠不會**要求您提供私人密鑰。 - \n- **請**保留您的私人密鑰的安全備份,以備帳户恢復。 我們建議使用密碼管理器。 - 私人密鑰(nsec)已複製到剪貼板 複製我的私人密鑰 身份驗證失敗 From 9479e86b66ec0fd702e57f1de5f04d421e84e782 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 14 Feb 2024 16:52:25 -0500 Subject: [PATCH 077/176] Enabled citation via @ on chats --- .../amethyst/ui/actions/NewPostView.kt | 28 ++----- .../ui/screen/loggedIn/ChannelScreen.kt | 80 +++++++++++++++---- .../ui/screen/loggedIn/ChatroomScreen.kt | 37 ++++++++- 3 files changed, 104 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index b2b9462ed..733b11233 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -50,8 +50,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.shape.RoundedCornerShape @@ -99,7 +97,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterVertically -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester @@ -158,9 +155,9 @@ import com.vitorpamplona.amethyst.ui.note.RegularPostIcon import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.MyTextField +import com.vitorpamplona.amethyst.ui.screen.loggedIn.ShowUserSuggestionList import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer -import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer @@ -526,23 +523,11 @@ fun NewPostView( } } - val userSuggestions = postViewModel.userSuggestions - if (userSuggestions.isNotEmpty()) { - LazyColumn( - contentPadding = - PaddingValues( - top = 10.dp, - ), - modifier = Modifier.heightIn(0.dp, 300.dp), - ) { - itemsIndexed( - userSuggestions, - key = { _, item -> item.pubkeyHex }, - ) { _, item -> - UserLine(item, accountViewModel) { postViewModel.autocompleteWithUser(item) } - } - } - } + ShowUserSuggestionList( + postViewModel, + accountViewModel, + modifier = Modifier.heightIn(0.dp, 300.dp), + ) BottomRowActions(postViewModel) } @@ -649,7 +634,6 @@ private fun PollField(postViewModel: NewPostViewModel) { } } -@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class) @Composable private fun MessageField(postViewModel: NewPostViewModel) { val focusRequester = remember { FocusRequester() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index 82e2ef638..8d2a49c30 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -38,10 +38,12 @@ import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.selection.LocalTextSelectionColors +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Cancel import androidx.compose.material.icons.filled.EditNote @@ -112,6 +114,7 @@ import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel import com.vitorpamplona.amethyst.ui.actions.ServerOption import com.vitorpamplona.amethyst.ui.actions.UploadFromGallery +import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.SensitivityWarning @@ -269,7 +272,13 @@ fun ChannelScreen( val replyTo = remember { mutableStateOf(null) } Column( - modifier = remember { Modifier.fillMaxHeight().padding(vertical = 0.dp).weight(1f, true) }, + modifier = + remember { + Modifier + .fillMaxHeight() + .padding(vertical = 0.dp) + .weight(1f, true) + }, ) { if (channel is LiveActivitiesChannel) { ShowVideoStreaming(channel, accountViewModel) @@ -340,7 +349,11 @@ fun DisplayReplyingToNote( onCancel: () -> Unit, ) { Row( - Modifier.padding(horizontal = 10.dp).animateContentSize(), + Modifier + .padding(horizontal = 10.dp) + .heightIn(max = 100.dp) + .verticalScroll(rememberScrollState()) + .animateContentSize(), verticalAlignment = Alignment.CenterVertically, ) { if (replyingNote != null) { @@ -363,7 +376,10 @@ fun DisplayReplyingToNote( Icon( imageVector = Icons.Default.Cancel, null, - modifier = Modifier.padding(end = 5.dp).size(30.dp), + modifier = + Modifier + .padding(end = 5.dp) + .size(30.dp), tint = MaterialTheme.colorScheme.placeholderText, ) } @@ -379,13 +395,13 @@ fun EditFieldRow( accountViewModel: AccountViewModel, onSendNewMessage: () -> Unit, ) { - Row( + Column( modifier = EditFieldModifier, - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, ) { val context = LocalContext.current + ShowUserSuggestionList(channelScreenModel, accountViewModel) + MyTextField( value = channelScreenModel.message, onValueChange = { channelScreenModel.updateMessage(it) }, @@ -394,7 +410,7 @@ fun EditFieldRow( capitalization = KeyboardCapitalization.Sentences, ), shape = EditFieldBorder, - modifier = Modifier.weight(1f, true), + modifier = Modifier.fillMaxWidth(), placeholder = { Text( text = stringResource(R.string.reply_here), @@ -431,6 +447,7 @@ fun EditFieldRow( focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, ), + visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary), ) } } @@ -725,7 +742,11 @@ fun ShortChannelHeader( } Column( - modifier = Modifier.padding(start = 10.dp).height(35.dp).weight(1f), + modifier = + Modifier + .padding(start = 10.dp) + .height(35.dp) + .weight(1f), verticalArrangement = Arrangement.Center, ) { Row(verticalAlignment = Alignment.CenterVertically) { @@ -738,7 +759,10 @@ fun ShortChannelHeader( } Row( - modifier = Modifier.height(Size35dp).padding(start = 5.dp), + modifier = + Modifier + .height(Size35dp) + .padding(start = 5.dp), verticalAlignment = Alignment.CenterVertically, ) { if (channel is PublicChatChannel) { @@ -1028,7 +1052,12 @@ fun LiveFlag() { fontWeight = FontWeight.Bold, fontSize = 16.sp, modifier = - remember { Modifier.clip(SmallBorder).background(Color.Red).padding(horizontal = 5.dp) }, + remember { + Modifier + .clip(SmallBorder) + .background(Color.Red) + .padding(horizontal = 5.dp) + }, ) } @@ -1039,7 +1068,12 @@ fun EndedFlag() { color = Color.White, fontWeight = FontWeight.Bold, modifier = - remember { Modifier.clip(SmallBorder).background(Color.Black).padding(horizontal = 5.dp) }, + remember { + Modifier + .clip(SmallBorder) + .background(Color.Black) + .padding(horizontal = 5.dp) + }, ) } @@ -1050,7 +1084,12 @@ fun OfflineFlag() { color = Color.White, fontWeight = FontWeight.Bold, modifier = - remember { Modifier.clip(SmallBorder).background(Color.Black).padding(horizontal = 5.dp) }, + remember { + Modifier + .clip(SmallBorder) + .background(Color.Black) + .padding(horizontal = 5.dp) + }, ) } @@ -1064,7 +1103,12 @@ fun ScheduledFlag(starts: Long?) { color = Color.White, fontWeight = FontWeight.Bold, modifier = - remember { Modifier.clip(SmallBorder).background(Color.Black).padding(horizontal = 5.dp) }, + remember { + Modifier + .clip(SmallBorder) + .background(Color.Black) + .padding(horizontal = 5.dp) + }, ) } @@ -1073,7 +1117,10 @@ private fun NoteCopyButton(note: Channel) { var popupExpanded by remember { mutableStateOf(false) } Button( - modifier = Modifier.padding(horizontal = 3.dp).width(50.dp), + modifier = + Modifier + .padding(horizontal = 3.dp) + .width(50.dp), onClick = { popupExpanded = true }, shape = ButtonBorder, colors = @@ -1116,7 +1163,10 @@ private fun EditButton( } Button( - modifier = Modifier.padding(horizontal = 3.dp).width(50.dp), + modifier = + Modifier + .padding(horizontal = 3.dp) + .width(50.dp), onClick = { wantsToPost = true }, contentPadding = ZeroPadding, ) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt index c205ff87c..2606d3a52 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt @@ -25,11 +25,13 @@ import androidx.compose.animation.Crossfade import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -92,6 +94,7 @@ import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel import com.vitorpamplona.amethyst.ui.actions.PostButton import com.vitorpamplona.amethyst.ui.actions.ServerOption import com.vitorpamplona.amethyst.ui.actions.UploadFromGallery +import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.note.DisplayRoomSubject @@ -363,13 +366,13 @@ fun PrivateMessageEditFieldRow( accountViewModel: AccountViewModel, onSendNewMessage: () -> Unit, ) { - Row( + Column( modifier = EditFieldModifier, - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, ) { val context = LocalContext.current + ShowUserSuggestionList(channelScreenModel, accountViewModel) + MyTextField( value = channelScreenModel.message, onValueChange = { channelScreenModel.updateMessage(it) }, @@ -378,7 +381,7 @@ fun PrivateMessageEditFieldRow( capitalization = KeyboardCapitalization.Sentences, ), shape = EditFieldBorder, - modifier = Modifier.weight(1f, true), + modifier = Modifier.fillMaxWidth(), placeholder = { Text( text = stringResource(R.string.reply_here), @@ -461,10 +464,36 @@ fun PrivateMessageEditFieldRow( focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, ), + visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary), ) } } +@Composable +fun ShowUserSuggestionList( + channelScreenModel: NewPostViewModel, + accountViewModel: AccountViewModel, + modifier: Modifier = Modifier.heightIn(0.dp, 200.dp), +) { + val userSuggestions = channelScreenModel.userSuggestions + if (userSuggestions.isNotEmpty()) { + LazyColumn( + contentPadding = + PaddingValues( + top = 10.dp, + ), + modifier = modifier, + ) { + itemsIndexed( + userSuggestions, + key = { _, item -> item.pubkeyHex }, + ) { _, item -> + UserLine(item, accountViewModel) { channelScreenModel.autocompleteWithUser(item) } + } + } + } +} + @Composable fun NewFeatureNIP24AlertDialog( accountViewModel: AccountViewModel, From d9c48d403906a948bcacac9cf40d3c16b5d08d68 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 14 Feb 2024 17:09:10 -0500 Subject: [PATCH 078/176] v0.84.0 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index b267a5d54..df2c735a8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 34 - versionCode 354 - versionName "0.83.13" + versionCode 355 + versionName "0.84.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From 103a61f35c2d12f8d5b2afdf8e4cd15a4a2f94f7 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 6 Feb 2024 07:31:56 -0300 Subject: [PATCH 079/176] useless pt-BR translation changes. --- app/src/main/res/values-pt-rBR/strings.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 92961c41f..3ea0dd877 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -4,7 +4,7 @@ Mostrar QR Imagem de perfil Sua Foto de Perfil - Escanear QR + Ler QR Mostrar de qualquer maneira A postagem foi sinalizada como imprópria por postagem não encontrada @@ -70,7 +70,7 @@ Novo Canal Nome do Canal Meu grupo - Url da foto + URL da foto Descrição "Sobre nós.. " O que você está pensando? @@ -316,7 +316,7 @@ Configuração do Tor/Orbot Conecte-se através da configuração do Orbot Desconectar do Orbot/Tor? - Seus dados serão transferidos imediatamente na rede regular + Seus dados serão transferidos imediatamente na rede aberta Sim Não Lista de seguidores @@ -442,7 +442,7 @@ Encaminhar Zaps para: Os clientes que suportam encaminharão zaps para o endereço lightning ou perfil de usuário abaixo, em vez do seu Expor localização como - Adiciona um Geohash da sua localização à postagem. O público saberá que você está a 5 km (3 milhas) do local atual + Adicione um geohash da sua localização à postagem. O público saberá que você está a 5 km (3 milhas) do local atual Adiciona aviso de conteúdo sensível antes de mostrar seu conteúdo. Isso é ideal para qualquer conteúdo NSFW ou conteúdo que algumas pessoas possam considerar ofensivo ou perturbador Novo recurso Ativando este modo requer o Amethyst para enviar uma mensagem de NIP-24 (GiftWrapped, Sealed Direct and Group Messages). NIP-24 é novo e a maioria dos clientes ainda não o implementaram. Certifique-se de que o destinatário está usando um cliente compatível. @@ -459,7 +459,7 @@ Mudando o nome dos novos objetivos. Colar da área de transferência Para a interface do aplicativo - Tema Escuro, Claro ou Sistema + Tema Escuro, Claro ou Padrão Carregar automaticamente imagens e GIFs Reproduz automaticamente vídeos e GIFs Mostrar visualizações de URL @@ -471,7 +471,7 @@ Criado em Regras Login com Amber - Atualize seu status + O que você está fazendo? Erro ao analisar mensagem de erro Os votos são ponderados pelo valor do zap. Você pode definir um valor mínimo para evitar spammers e um valor máximo para evitar que grandes zappers assumam o controle da enquete. Use o mesmo valor em ambos os campos para garantir que cada voto tenha o mesmo valor. Deixe em branco para aceitar qualquer valor. Não foi possível enviar o Zap From 4746b5b18a6746eed2cb3e9a36bb9497ad036f53 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 14 Feb 2024 17:40:50 -0500 Subject: [PATCH 080/176] removes unneeded translation keys --- app/src/main/res/values-in/strings.xml | 1 - app/src/main/res/values-zh/strings.xml | 5 ----- 2 files changed, 6 deletions(-) diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index b4e2a9a18..13ee2612a 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -182,7 +182,6 @@ Tandai pesan baru telah dibaca Tandai semua telah dibaca Cadangkan Kunci - "\n ## Tip Pencadangan dan Keamanan Utama\n \n\nAkun Anda diamankan dengan kunci rahasia. Kuncinya adalah string acak panjang yang dimulai dengan **nsec1**.. Siapa pun yang memiliki akses ke kunci rahasia Anda dapat mempublikasikan konten menggunakan identitas Anda.\n \n\n- **Jangan** letakkan kunci rahasia Anda di situs web atau perangkat lunak apa pun yang tidak Anda percayai.\n \n- Pengembang Aplikasi Amethyst **tidak akan pernah** meminta kunci rahasia Anda.\n \n- **Pastikan** simpan cadangan aman kunci rahasia Anda untuk pemulihan akun. Kami merekomendasikan menggunakan aplikasi khusus pengelola kata sandi.\n " Kunci rahasia (nsec) disalin ke clipboard Salin kunci rahasia saya Autentikasi gagal diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 834e7d8f0..d1cf305cc 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -176,11 +176,6 @@ 将所有新内容标记为已读 将所有内容标记为已读 备份密匙 - ## 备份与安全提示 - \n\n您的帐户由一个私人密钥保护。 密钥是以**nsec1**开头的长随机字符串。任何拥有您的私人密钥的人都可以使用您的身份发布内容。 - \n\n- **不要**将您的私人密钥添加到您不信任的任何网站或软件,亦不要在网上公开。 - \n- Amethyst 开发人员**永远不会**要求您提供私人密钥。 - \n- **请**保留您的私人密钥的安全备份,以备帐户恢复。 我们建议使用密码管理器。 私人密钥(nsec)已复制到剪贴板 复制我的私人密钥 身份验证失败 From 7074446cae4f646dd76255f184746e89c175c24d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 14 Feb 2024 17:48:57 -0500 Subject: [PATCH 081/176] Removing scrypt native library that doesn't work on Android --- app/build.gradle | 1 + app/proguard-rules.pro | 5 +++++ quartz/build.gradle | 4 ++++ quartz/proguard-rules.pro | 5 +++++ 4 files changed, 15 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index df2c735a8..5664f0d78 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -149,6 +149,7 @@ android { resources { excludes += '/META-INF/{AL2.0,LGPL2.1}' } + exclude '**/libscrypt.dylib' } lint { diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 97496bbf1..5b1f59d16 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -33,6 +33,11 @@ # JNA For Libsodium -keep class com.goterl.lazysodium.** { *; } +# libscrypt +-keep class com.lambdaworks.codec.** { *; } +-keep class com.lambdaworks.crypto.** { *; } +-keep class com.lambdaworks.jni.** { *; } + # JNA also requires AWT, which Android does not have. So the classes are broken down to filter AWT out -keep class com.sun.jna.ToNativeConverter { *; } -keep class com.sun.jna.NativeMapped { *; } diff --git a/quartz/build.gradle b/quartz/build.gradle index 3dd64b7cd..f2422b021 100644 --- a/quartz/build.gradle +++ b/quartz/build.gradle @@ -34,6 +34,10 @@ android { jvmTarget = '17' freeCompilerArgs += "-Xstring-concat=inline" } + + packagingOptions { + exclude '**/libscrypt.dylib' + } } dependencies { diff --git a/quartz/proguard-rules.pro b/quartz/proguard-rules.pro index 4727a86be..f2b5c9297 100644 --- a/quartz/proguard-rules.pro +++ b/quartz/proguard-rules.pro @@ -33,6 +33,11 @@ # JNA For Libsodium -keep class com.goterl.lazysodium.** { *; } +# libscrypt +-keep class com.lambdaworks.codec.** { *; } +-keep class com.lambdaworks.crypto.** { *; } +-keep class com.lambdaworks.jni.** { *; } + # JNA also requires AWT, which Android does not have. So the classes are broken down to filter AWT out -keep class com.sun.jna.ToNativeConverter { *; } -keep class com.sun.jna.NativeMapped { *; } From 02da606461c46f788de3e9b779f9b5885d790ab0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 14 Feb 2024 17:49:07 -0500 Subject: [PATCH 082/176] v0.84.1 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 5664f0d78..188c46661 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 34 - versionCode 355 - versionName "0.84.0" + versionCode 356 + versionName "0.84.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From b79f9c83958477b55d5fbed2daecae658b3bc77d Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Thu, 15 Feb 2024 12:18:19 +0000 Subject: [PATCH 083/176] Updated translations. DE, SV, PT, CS --- app/src/main/res/values-cs/strings.xml | 19 +++++++++++++++++++ app/src/main/res/values-de/strings.xml | 19 +++++++++++++++++++ app/src/main/res/values-pt-rBR/strings.xml | 19 +++++++++++++++++++ app/src/main/res/values-sv-rSE/strings.xml | 19 +++++++++++++++++++ 4 files changed, 76 insertions(+) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index cf1677fa3..240d788d8 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -632,4 +632,23 @@ Přidat upozornění na obsah Odstranit upozornění na obsah Zobrazit npub jako QR kód + + ## Tipy pro zálohování klíčů a bezpečnost + \n\nVaše konto je zabezpečeno tajným klíčem. Klíč je dlouhá posloupnost znaků začínající s **nsec1**. Kdo má přístup k tomuto tajnému klíči, může přispívat a měnit vaši identitu. + \n\n- **Nedávejte** svůj tajný klíč na žádnou webovou stránku nebo do žádného softwaru, kterému nedůvěřujete. + \n- Vývojáři Amethystu nikdy **nebudou** žádat o váš tajný klíč. + \n- **Udržujte** bezpečnou zálohu vašeho tajného klíče pro obnovení účtu. Doporučujeme použití správce hesel. + + + Pro další zabezpečení můžete svůj klíč zašifrovat heslem. Tento klíč začíná s **ncryptsec1** a nelze ho použít bez vašeho hesla. + \n\nPokud zapomenete heslo, nebudete moci obnovit svůj klíč. + + + Nepodařilo se zašifrovat váš privátní klíč + Zašifrovat a zkopírovat můj tajný klíč + Heslo je vyžadováno + Neplatný klíč: %1$s + heslo pro otevření klíče + Neplatná adresa + Amethyst obdržel URI k otevření, ale tento URI byl neplatný: %1$s diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 108502f82..ef6a27114 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -637,4 +637,23 @@ anz der Bedingungen ist erforderlich Inhaltswarnung hinzufügen Inhaltswarnung entfernen Npub als QR-Code anzeigen + + ## Schlüsselsicherheits-Tipps + \n\nIhr Konto ist durch einen geheimen Schlüssel gesichert. Der Schlüssel ist eine lange Zeichenfolge, die mit **nsec1** beginnt. Jeder, der Zugriff auf diesen geheimen Schlüssel hat, kann Ihre Identität posten und ändern. + \n\n- Legen Sie Ihren geheimen Schlüssel **nicht** auf eine Website oder in eine Software, der Sie nicht vertrauen. + \n- Die Entwickler von Amethyst werden Sie **niemals** nach Ihrem geheimen Schlüssel fragen. + \n- **Bewahren Sie** eine sichere Sicherung Ihres geheimen Schlüssels zur Kontowiederherstellung auf. Wir empfehlen die Verwendung eines Passwort-Managers. + + + Für zusätzliche Sicherheit können Sie Ihren Schlüssel mit einem Passwort verschlüsseln. Dieser Schlüssel beginnt mit **ncryptsec1** und kann ohne Ihr Passwort nicht verwendet werden. + \n\nWenn Sie Ihr Passwort verlieren, können Sie Ihren Schlüssel nicht wiederherstellen. + + + Fehler beim Verschlüsseln Ihres privaten Schlüssels + Verschlüsseln und kopieren Sie meinen geheimen Schlüssel + Passwort ist erforderlich + Ungültiger Schlüssel: %1$s + Passwort zum Öffnen des Schlüssels + Ungültige Adresse + Amethyst hat eine URI zum Öffnen erhalten, aber diese URI war ungültig: %1$s diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 3ea0dd877..a25ad6a5e 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -632,4 +632,23 @@ Adicionar aviso de conteúdo Remover aviso de conteúdo Mostrar npub como um código QR + + ## Dicas de Backup de Chave e Segurança + \n\nSua conta é protegida por uma chave secreta. A chave é uma sequência longa de caracteres que começa com **nsec1**. Qualquer pessoa que tenha acesso a esta chave secreta pode postar e alterar sua identidade. + \n\n- **Não** coloque sua chave secreta em nenhum site ou software que você não confie. + \n- Os desenvolvedores da Amethyst **nunca** pedirão sua chave secreta. + \n- **Mantenha** uma cópia de segurança segura de sua chave secreta para recuperação da conta. Recomendamos o uso de um gerenciador de senhas. + + + Para segurança adicional, você pode criptografar sua chave com uma senha. Esta chave começa com **ncryptsec1** e não pode ser usada sem sua senha. + \n\nSe você perder sua senha, não será possível recuperar sua chave. + + + Falha ao criptografar sua chave privada + Criptografar e copiar minha chave secreta + Senha é necessária + Chave inválida: %1$s + senha para abrir a chave + Endereço inválido + O Amethyst recebeu um URI para abrir, mas esse URI era inválido: %1$s diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 1ed54d455..247a5e64d 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -631,4 +631,23 @@ Lägg till varning för innehåll Ta bort varning för innehåll Visa npub som en QR-kod + + ## Nyckelsäkerhetsråd + \n\nDitt konto är säkrat med en hemlig nyckel. Nyckeln är en lång följd av tecken som börjar med **nsec1**. Den som har tillgång till denna hemliga nyckel kan posta och ändra din identitet. + \n\n- **Lägg inte** din hemliga nyckel på någon webbplats eller i någon mjukvara som du inte litar på. + \n- Utvecklarna av Amethyst kommer **aldrig** att be om din hemliga nyckel. + \n- **Spara** en säkerhetskopia av din hemliga nyckel för återställning av kontot. Vi rekommenderar att du använder en lösenordshanterare. + + + För extra säkerhet kan du kryptera din nyckel med ett lösenord. Denna nyckel börjar med **ncryptsec1** och kan inte användas utan ditt lösenord. + \n\nOm du tappar bort ditt lösenord kommer du inte att kunna återställa din nyckel. + + + Misslyckades med att kryptera din privata nyckel + Kryptera och kopiera min hemliga nyckel + Lösenord krävs + Ogiltig nyckel: %1$s + lösenord för att öppna nyckeln + Ogiltig adress + Amethyst fick en URI att öppna men den URI var ogiltig: %1$s From 5ed64bd5b5fe7e051009f1e2cc643a48287e1164 Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Thu, 15 Feb 2024 12:19:12 +0000 Subject: [PATCH 084/176] Change to ellipsis character --- app/src/main/res/values-pt-rBR/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index a25ad6a5e..d1e3566af 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -104,7 +104,7 @@ Imagem salva para a galeria Falha ao salvar a imagem Enviar Imagem - Enviando... + Enviando… Usuário não tem um endereço lightning configurado para receber sats "responda aqui.. " Copia o ID do canal (note) para compartilhar @@ -269,7 +269,7 @@ Postar enquete Campos obrigatórios: Destinatários Zap - Descrição da enquete principal... + Descrição da enquete principal… Opção %s Descrição da opção de enquete Campos opcionais: From c85d74a688f60e3abf8c0e1f057d146b2fb16ae0 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Thu, 15 Feb 2024 13:41:21 +0000 Subject: [PATCH 085/176] New Crowdin translations by GitHub Action --- app/src/main/res/values-cs/strings.xml | 29 +++++++++------------- app/src/main/res/values-de/strings.xml | 29 +++++++++------------- app/src/main/res/values-fr/strings.xml | 16 ++++++++++++ app/src/main/res/values-pt-rBR/strings.xml | 29 +++++++++------------- app/src/main/res/values-sv-rSE/strings.xml | 29 +++++++++------------- 5 files changed, 64 insertions(+), 68 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 240d788d8..bcf27f042 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -139,12 +139,15 @@ Vymazat Logo aplikace nsec / npub / hex privátní klíč + heslo pro otevření klíče Zobrazit heslo Skrýt heslo Neplatný klíč + Neplatný klíč: %1$s "Souhlasím s " podmínkami použití Je vyžadováno přijetí podmínek + Heslo je vyžadováno Klíč je povinný Jméno je povinné Přihlásit se @@ -195,8 +198,17 @@ Označit všechny nové jako přečtené Označit všechny jako přečtené Zálohovat klíče + ## Tipy pro zálohování klíčů a bezpečnost + \n\nVaše konto je zabezpečeno tajným klíčem. Klíč je dlouhá posloupnost znaků začínající s **nsec1**. Kdo má přístup k tomuto tajnému klíči, může přispívat a měnit vaši identitu. + \n\n- **Nedávejte** svůj tajný klíč na žádnou webovou stránku nebo do žádného softwaru, kterému nedůvěřujete. + \n- Vývojáři Amethystu nikdy **nebudou** žádat o váš tajný klíč. + \n- **Udržujte** bezpečnou zálohu vašeho tajného klíče pro obnovení účtu. Doporučujeme použití správce hesel. + Pro další zabezpečení můžete svůj klíč zašifrovat heslem. Tento klíč začíná s **ncryptsec1** a nelze ho použít bez vašeho hesla. + \n\nPokud zapomenete heslo, nebudete moci obnovit svůj klíč. + Nepodařilo se zašifrovat váš privátní klíč Tajný klíč (nsec) zkopírován do schránky Zkopírovat můj tajný klíč + Zašifrovat a zkopírovat můj tajný klíč Autentizace se nezdařila Biometrické údaje se nepodařilo ověřit vlastníka tohoto telefonu Biometrické údaje se nepodařilo ověřit vlastníka tohoto telefonu. Chyba: %1$s @@ -632,23 +644,6 @@ Přidat upozornění na obsah Odstranit upozornění na obsah Zobrazit npub jako QR kód - - ## Tipy pro zálohování klíčů a bezpečnost - \n\nVaše konto je zabezpečeno tajným klíčem. Klíč je dlouhá posloupnost znaků začínající s **nsec1**. Kdo má přístup k tomuto tajnému klíči, může přispívat a měnit vaši identitu. - \n\n- **Nedávejte** svůj tajný klíč na žádnou webovou stránku nebo do žádného softwaru, kterému nedůvěřujete. - \n- Vývojáři Amethystu nikdy **nebudou** žádat o váš tajný klíč. - \n- **Udržujte** bezpečnou zálohu vašeho tajného klíče pro obnovení účtu. Doporučujeme použití správce hesel. - - - Pro další zabezpečení můžete svůj klíč zašifrovat heslem. Tento klíč začíná s **ncryptsec1** a nelze ho použít bez vašeho hesla. - \n\nPokud zapomenete heslo, nebudete moci obnovit svůj klíč. - - - Nepodařilo se zašifrovat váš privátní klíč - Zašifrovat a zkopírovat můj tajný klíč - Heslo je vyžadováno - Neplatný klíč: %1$s - heslo pro otevření klíče Neplatná adresa Amethyst obdržel URI k otevření, ale tento URI byl neplatný: %1$s diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index ef6a27114..a0270effb 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -141,14 +141,17 @@ erie gespeichert Löschen App-Logo Nsec / Npub / Hex-Privatschlüssel + Passwort zum Öffnen des Schlüssels Passwort anzeigen Passwort ausblenden Ungültiger Schlüssel + Ungültiger Schlüssel: %1$s "Ich akzeptiere die " Nutzungsbedingungen Die Akzept anz der Bedingungen ist erforderlich + Passwort ist erforderlich Schlüssel ist erforderlich Ein Name wird benötigt Anmeldung @@ -199,8 +202,17 @@ anz der Bedingungen ist erforderlich Alle als neu markieren Alle als gelesen markieren Schlüssel sichern + ## Schlüsselsicherheits-Tipps + \n\nIhr Konto ist durch einen geheimen Schlüssel gesichert. Der Schlüssel ist eine lange Zeichenfolge, die mit **nsec1** beginnt. Jeder, der Zugriff auf diesen geheimen Schlüssel hat, kann Ihre Identität posten und ändern. + \n\n- Legen Sie Ihren geheimen Schlüssel **nicht** auf eine Website oder in eine Software, der Sie nicht vertrauen. + \n- Die Entwickler von Amethyst werden Sie **niemals** nach Ihrem geheimen Schlüssel fragen. + \n- **Bewahren Sie** eine sichere Sicherung Ihres geheimen Schlüssels zur Kontowiederherstellung auf. Wir empfehlen die Verwendung eines Passwort-Managers. + Für zusätzliche Sicherheit können Sie Ihren Schlüssel mit einem Passwort verschlüsseln. Dieser Schlüssel beginnt mit **ncryptsec1** und kann ohne Ihr Passwort nicht verwendet werden. + \n\nWenn Sie Ihr Passwort verlieren, können Sie Ihren Schlüssel nicht wiederherstellen. + Fehler beim Verschlüsseln Ihres privaten Schlüssels Geheimer Schlüssel (nsec) in die Zwischenablage kopiert Meinen geheimen Schlüssel kopieren + Verschlüsseln und kopieren Sie meinen geheimen Schlüssel Authentifizierung fehlgeschlagen Biometrie konnte den Besitzer dieses Telefons nicht authentifizieren Biometrie konnte den Besitzer dieses Telefons nicht authentifizieren. Fehler: %1$s @@ -637,23 +649,6 @@ anz der Bedingungen ist erforderlich Inhaltswarnung hinzufügen Inhaltswarnung entfernen Npub als QR-Code anzeigen - - ## Schlüsselsicherheits-Tipps - \n\nIhr Konto ist durch einen geheimen Schlüssel gesichert. Der Schlüssel ist eine lange Zeichenfolge, die mit **nsec1** beginnt. Jeder, der Zugriff auf diesen geheimen Schlüssel hat, kann Ihre Identität posten und ändern. - \n\n- Legen Sie Ihren geheimen Schlüssel **nicht** auf eine Website oder in eine Software, der Sie nicht vertrauen. - \n- Die Entwickler von Amethyst werden Sie **niemals** nach Ihrem geheimen Schlüssel fragen. - \n- **Bewahren Sie** eine sichere Sicherung Ihres geheimen Schlüssels zur Kontowiederherstellung auf. Wir empfehlen die Verwendung eines Passwort-Managers. - - - Für zusätzliche Sicherheit können Sie Ihren Schlüssel mit einem Passwort verschlüsseln. Dieser Schlüssel beginnt mit **ncryptsec1** und kann ohne Ihr Passwort nicht verwendet werden. - \n\nWenn Sie Ihr Passwort verlieren, können Sie Ihren Schlüssel nicht wiederherstellen. - - - Fehler beim Verschlüsseln Ihres privaten Schlüssels - Verschlüsseln und kopieren Sie meinen geheimen Schlüssel - Passwort ist erforderlich - Ungültiger Schlüssel: %1$s - Passwort zum Öffnen des Schlüssels Ungültige Adresse Amethyst hat eine URI zum Öffnen erhalten, aber diese URI war ungültig: %1$s diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index d5aa2ee2e..c248787f0 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -139,12 +139,15 @@ Nettoyer Logo de l\'App nsec / npub / hex private key + mot de passe pour ouvrir la clé Révéler le mot de passe Cacher le mot de passe Clé non valide + Clé invalide : %1$s "J'accepte les " conditions d\'utilisation L\'acceptation des conditions est requise + Mot de passe requis La clé est requise Un nom est requis Connexion @@ -195,8 +198,19 @@ Marquer tous les Nouveaux comme lu Tout marquer comme lu Clés de sauvegarde + ## Sauvegarde des Clés et Conseils de Sécurité + \n\nVotre compte est sécurisé par une clé secrète. La clé est une longue séquence de caractères commençant par **nsec1**. Toute personne ayant accès à cette clé secrète peut publier et modifier votre identité. + \n\n- Ne mettez **pas** votre clé secrète sur un site web ou un logiciel auquel vous ne faites pas confiance. + \n- Les développeurs d\'Amethyst ne vous demanderont **jamais** votre clé secrète. + \n- **Faites** une sauvegarde sécurisée de votre clé secrète pour la récupération de votre compte. Nous vous recommandons d\'utiliser un gestionnaire de mots de passe. + + Pour plus de sécurité, vous pouvez chiffrer votre clé avec un mot de passe. Cette clé commence par **ncryptsec1** et ne peut être utilisée sans votre mot de passe. + \n\nSi vous perdez votre mot de passe, vous ne pourrez pas récupérer votre clé. + + Échec du chiffrement de votre clé privée Clé secrète (nsec) copiée dans le presse-papiers Copier ma clé secrète + Chiffrer et copier ma clé secrète Échec de l\'authentification La biométrie n\'a pas pu authentifier le propriétaire de ce téléphone La biométrie n\'a pas pu authentifier le propriétaire de ce téléphone. Erreur : %1$s @@ -632,4 +646,6 @@ Ajouter un avertissement de contenu Retirer l\'avertissement de contenu Afficher npub en tant que QR code + Adresse invalide + Amethyst a reçu une URI à ouvrir mais cette URI était invalide : %1$s diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index d1e3566af..aeddd23cc 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -139,12 +139,15 @@ Limpar Logo do aplicativo nsec / npub / hex chave privada + senha para abrir a chave Mostrar senha Esconder senha Chave inválida + Chave inválida: %1$s "Eu aceito os " termos de uso É necessário aceitar os termos de uso + Senha é necessária Chave é obrigatória Um nome é necessário Entrar @@ -195,8 +198,17 @@ Marcar todas novas solicitações como lidas Marcar todas como lidas Copia de segurança das chaves + ## Dicas de Backup de Chave e Segurança + \n\nSua conta é protegida por uma chave secreta. A chave é uma sequência longa de caracteres que começa com **nsec1**. Qualquer pessoa que tenha acesso a esta chave secreta pode postar e alterar sua identidade. + \n\n- **Não** coloque sua chave secreta em nenhum site ou software que você não confie. + \n- Os desenvolvedores da Amethyst **nunca** pedirão sua chave secreta. + \n- **Mantenha** uma cópia de segurança segura de sua chave secreta para recuperação da conta. Recomendamos o uso de um gerenciador de senhas. + Para segurança adicional, você pode criptografar sua chave com uma senha. Esta chave começa com **ncryptsec1** e não pode ser usada sem sua senha. + \n\nSe você perder sua senha, não será possível recuperar sua chave. + Falha ao criptografar sua chave privada Chave secreta (nsec) copiada Copiar minha chave secreta + Criptografar e copiar minha chave secreta Autenticação falhou A autenticação biométrica falhou ao autenticar o proprietário deste telefone A autenticação biométrica falhou ao autenticar o proprietário deste telefone. Erro: %1$s @@ -632,23 +644,6 @@ Adicionar aviso de conteúdo Remover aviso de conteúdo Mostrar npub como um código QR - - ## Dicas de Backup de Chave e Segurança - \n\nSua conta é protegida por uma chave secreta. A chave é uma sequência longa de caracteres que começa com **nsec1**. Qualquer pessoa que tenha acesso a esta chave secreta pode postar e alterar sua identidade. - \n\n- **Não** coloque sua chave secreta em nenhum site ou software que você não confie. - \n- Os desenvolvedores da Amethyst **nunca** pedirão sua chave secreta. - \n- **Mantenha** uma cópia de segurança segura de sua chave secreta para recuperação da conta. Recomendamos o uso de um gerenciador de senhas. - - - Para segurança adicional, você pode criptografar sua chave com uma senha. Esta chave começa com **ncryptsec1** e não pode ser usada sem sua senha. - \n\nSe você perder sua senha, não será possível recuperar sua chave. - - - Falha ao criptografar sua chave privada - Criptografar e copiar minha chave secreta - Senha é necessária - Chave inválida: %1$s - senha para abrir a chave Endereço inválido O Amethyst recebeu um URI para abrir, mas esse URI era inválido: %1$s diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 247a5e64d..b44e0b4d5 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -139,12 +139,15 @@ Rensa App Logo nsec / npub / hex privat nyckel + lösenord för att öppna nyckeln Visa lösenord Göm lösenord Ogiltig nyckel + Ogiltig nyckel: %1$s "Jag accepterar " villkor Godkännande av villkor krävs + Lösenord krävs Nyckel krävs Namn är obligatoriskt Inloggning @@ -195,8 +198,17 @@ Markera alla nya som lästa Markera allt som läst Backup-nycklar + ## Nyckelsäkerhetsråd + \n\nDitt konto är säkrat med en hemlig nyckel. Nyckeln är en lång följd av tecken som börjar med **nsec1**. Den som har tillgång till denna hemliga nyckel kan posta och ändra din identitet. + \n\n- **Lägg inte** din hemliga nyckel på någon webbplats eller i någon mjukvara som du inte litar på. + \n- Utvecklarna av Amethyst kommer **aldrig** att be om din hemliga nyckel. + \n- **Spara** en säkerhetskopia av din hemliga nyckel för återställning av kontot. Vi rekommenderar att du använder en lösenordshanterare. + För extra säkerhet kan du kryptera din nyckel med ett lösenord. Denna nyckel börjar med **ncryptsec1** och kan inte användas utan ditt lösenord. + \n\nOm du tappar bort ditt lösenord kommer du inte att kunna återställa din nyckel. + Misslyckades med att kryptera din privata nyckel Hemlig nyckel (nsec) kopierad till urklipp Kopiera min hemliga nyckel + Kryptera och kopiera min hemliga nyckel Autentisering misslyckades Biometri misslyckades med att autentisera ägaren av denna telefon Biometri misslyckades med att autentisera ägaren av denna telefon. Fel: %1$s @@ -631,23 +643,6 @@ Lägg till varning för innehåll Ta bort varning för innehåll Visa npub som en QR-kod - - ## Nyckelsäkerhetsråd - \n\nDitt konto är säkrat med en hemlig nyckel. Nyckeln är en lång följd av tecken som börjar med **nsec1**. Den som har tillgång till denna hemliga nyckel kan posta och ändra din identitet. - \n\n- **Lägg inte** din hemliga nyckel på någon webbplats eller i någon mjukvara som du inte litar på. - \n- Utvecklarna av Amethyst kommer **aldrig** att be om din hemliga nyckel. - \n- **Spara** en säkerhetskopia av din hemliga nyckel för återställning av kontot. Vi rekommenderar att du använder en lösenordshanterare. - - - För extra säkerhet kan du kryptera din nyckel med ett lösenord. Denna nyckel börjar med **ncryptsec1** och kan inte användas utan ditt lösenord. - \n\nOm du tappar bort ditt lösenord kommer du inte att kunna återställa din nyckel. - - - Misslyckades med att kryptera din privata nyckel - Kryptera och kopiera min hemliga nyckel - Lösenord krävs - Ogiltig nyckel: %1$s - lösenord för att öppna nyckeln Ogiltig adress Amethyst fick en URI att öppna men den URI var ogiltig: %1$s From ed7dcf94f8286ceb1ebb4b99c3f6bf16c3451f45 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 15 Feb 2024 10:59:52 -0500 Subject: [PATCH 086/176] Fixes extra spaces after urls --- .../vitorpamplona/amethyst/service/CachedRichTextParser.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt index e43f0a138..490cffb53 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt @@ -257,7 +257,7 @@ class RichTextParser() { parseHash(word, tags) } else if (word.contains(".") && schemelessMatcher.find()) { val url = schemelessMatcher.group(1) // url - val additionalChars = schemelessMatcher.group(4) // additional chars + val additionalChars = schemelessMatcher.group(4).ifEmpty { null } // additional chars val pattern = """^([A-Za-z0-9-_]+(\.[A-Za-z0-9-_]+)+)(:[0-9]+)?(/[^?#]*)?(\?[^#]*)?(#.*)?""" .toRegex(RegexOption.IGNORE_CASE) @@ -307,7 +307,7 @@ class RichTextParser() { if (hashtagMatcher.find()) { val hashtag = hashtagMatcher.group(1) if (hashtag != null) { - return HashTagSegment(word, hashtag, hashtagMatcher.group(2)) + return HashTagSegment(word, hashtag, hashtagMatcher.group(2).ifEmpty { null }) } } } catch (e: Exception) { From 215989108bbfeeed743e26eeb3a8d125dd947a55 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 15 Feb 2024 11:00:15 -0500 Subject: [PATCH 087/176] Fixes misalignment of Clickable Texts --- .../amethyst/ui/components/ClickableRoute.kt | 58 +++-- .../amethyst/ui/components/RichTextViewer.kt | 204 +++++++++++------- 2 files changed, 165 insertions(+), 97 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt index a69879fae..af2fce936 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt @@ -24,7 +24,6 @@ import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.BasicText -import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.appendInlineContent import androidx.compose.material3.LocalContentColor @@ -298,27 +297,24 @@ fun CreateClickableText( suffix: String?, maxLines: Int = Int.MAX_VALUE, overrideColor: Color? = null, - fontWeight: FontWeight = FontWeight.Normal, + fontWeight: FontWeight? = null, route: String, nav: (String) -> Unit, ) { - val currentStyle = LocalTextStyle.current val primaryColor = MaterialTheme.colorScheme.primary val onBackgroundColor = MaterialTheme.colorScheme.onBackground val clickablePartStyle = - remember(primaryColor, overrideColor) { - currentStyle - .copy(color = overrideColor ?: primaryColor, fontWeight = fontWeight) - .toSpanStyle() - } + SpanStyle( + color = overrideColor ?: primaryColor, + fontWeight = fontWeight, + ) val nonClickablePartStyle = - remember(onBackgroundColor, overrideColor) { - currentStyle - .copy(color = overrideColor ?: onBackgroundColor, fontWeight = fontWeight) - .toSpanStyle() - } + SpanStyle( + color = overrideColor ?: onBackgroundColor, + fontWeight = fontWeight, + ) val text = remember(clickablePartStyle, nonClickablePartStyle, clickablePart, suffix) { @@ -333,11 +329,45 @@ fun CreateClickableText( ClickableText( text = text, maxLines = maxLines, - style = currentStyle, onClick = { nav(route) }, ) } +@Composable +fun ClickableText( + text: AnnotatedString, + modifier: Modifier = Modifier, + style: TextStyle = LocalTextStyle.current, + softWrap: Boolean = true, + overflow: TextOverflow = TextOverflow.Clip, + maxLines: Int = Int.MAX_VALUE, + onTextLayout: (TextLayoutResult) -> Unit = {}, + onClick: (Int) -> Unit, +) { + val layoutResult = remember { mutableStateOf(null) } + val pressIndicator = + Modifier.pointerInput(onClick) { + detectTapGestures { pos -> + layoutResult.value?.let { layoutResult -> + onClick(layoutResult.getOffsetForPosition(pos)) + } + } + } + + Text( + text = text, + modifier = modifier.then(pressIndicator), + style = style, + softWrap = softWrap, + overflow = overflow, + maxLines = maxLines, + onTextLayout = { + layoutResult.value = it + onTextLayout(it) + }, + ) +} + @Composable fun CreateTextWithEmoji( text: String, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index ddb47afc1..439f6a021 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -28,11 +28,10 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.appendInlineContent import androidx.compose.material3.Icon -import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProvideTextStyle @@ -49,7 +48,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFontFamilyResolver import androidx.compose.ui.platform.LocalLayoutDirection @@ -62,8 +60,10 @@ import androidx.compose.ui.text.TextMeasurer import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em import com.halilibo.richtext.markdown.Markdown import com.halilibo.richtext.markdown.MarkdownParseOptions @@ -174,7 +174,73 @@ fun RichTextViewer( } } -@OptIn(ExperimentalLayoutApi::class) +@Preview() +@Composable +fun RenderRegularPreview() { + val backgroundColor = MaterialTheme.colorScheme.background + val backgroundColorState = + remember { + mutableStateOf(backgroundColor) + } + + val nav: (String) -> Unit = {} + + Column(modifier = Modifier.padding(10.dp)) { + RenderRegular( + "nostr:npub1e0z776cpe0gllgktjk54fuzv8pdfxmq6smsmh8xd7t8s7n474n9smk0txy but i'm Monthly funding" + + " 7 other humans vitor@vitorpamplona.com at the moment so spread #test a bit thin, but won't always be the case.", + EmptyTagList, + ) { word, state -> + when (word) { + // is ImageSegment -> ZoomableContentView(word.segmentText, state, accountViewModel) + // is LinkSegment -> LoadUrlPreview(word.segmentText, word.segmentText, accountViewModel) + is EmojiSegment -> RenderCustomEmoji(word.segmentText, state) + is InvoiceSegment -> MayBeInvoicePreview(word.segmentText) + is WithdrawSegment -> MayBeWithdrawal(word.segmentText) + // is CashuSegment -> CashuPreview(word.segmentText, accountViewModel) + is EmailSegment -> ClickableEmail(word.segmentText) + is PhoneSegment -> ClickablePhone(word.segmentText) + is BechSegment -> { + CreateClickableText( + word.segmentText.substring(0, 10), + "", + 1, + route = "", + nav = nav, + ) + } + is HashTagSegment -> HashTag(word, nav) + // is HashIndexUserSegment -> TagLink(word, accountViewModel, nav) + // is HashIndexEventSegment -> TagLink(word, true, backgroundColorState, accountViewModel, nav) + is SchemelessUrlSegment -> NoProtocolUrlRenderer(word) + is RegularTextSegment -> Text(word.segmentText) + } + } + + RenderRegular( + "#Amethyst v0.84.1: ncryptsec support (NIP-49)", + EmptyTagList, + ) { word, state -> + when (word) { + // is ImageSegment -> ZoomableContentView(word.segmentText, state, accountViewModel) + // is LinkSegment -> LoadUrlPreview(word.segmentText, word.segmentText, accountViewModel) + is EmojiSegment -> RenderCustomEmoji(word.segmentText, state) + is InvoiceSegment -> MayBeInvoicePreview(word.segmentText) + is WithdrawSegment -> MayBeWithdrawal(word.segmentText) + // is CashuSegment -> CashuPreview(word.segmentText, accountViewModel) + is EmailSegment -> ClickableEmail(word.segmentText) + is PhoneSegment -> ClickablePhone(word.segmentText) + // is BechSegment -> BechLink(word.segmentText, true, backgroundColor, accountViewModel, nav) + is HashTagSegment -> HashTag(word, nav) + // is HashIndexUserSegment -> TagLink(word, accountViewModel, nav) + // is HashIndexEventSegment -> TagLink(word, true, backgroundColorState, accountViewModel, nav) + is SchemelessUrlSegment -> NoProtocolUrlRenderer(word) + is RegularTextSegment -> Text(word.segmentText) + } + } + } +} + @Composable private fun RenderRegular( content: String, @@ -183,76 +249,66 @@ private fun RenderRegular( backgroundColor: MutableState, accountViewModel: AccountViewModel, nav: (String) -> Unit, +) { + RenderRegular(content, tags) { word, state -> + if (canPreview) { + RenderWordWithPreview( + word, + state, + backgroundColor, + accountViewModel, + nav, + ) + } else { + RenderWordWithoutPreview( + word, + state, + backgroundColor, + accountViewModel, + nav, + ) + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun RenderRegular( + content: String, + tags: ImmutableListOfLists, + wordRenderer: @Composable (Segment, RichTextViewerState) -> Unit, ) { val state by remember(content, tags) { mutableStateOf(CachedRichTextParser.parseText(content, tags)) } + val spaceWidth = measureSpaceWidth(LocalTextStyle.current) + val currentTextStyle = LocalTextStyle.current - val currentTextColor = LocalContentColor.current val textStyle = - remember(currentTextStyle, currentTextColor) { + remember(currentTextStyle) { currentTextStyle.copy( lineHeight = 1.4.em, - color = currentTextStyle.color.takeOrElse { currentTextColor }, ) } - val spaceWidth = measureSpaceWidth(textStyle) - Column { - if (canPreview) { - // FlowRow doesn't work well with paragraphs. So we need to split them - state.paragraphs.forEach { paragraph -> - val direction = + // FlowRow doesn't work well with paragraphs. So we need to split them + state.paragraphs.forEach { paragraph -> + CompositionLocalProvider( + LocalLayoutDirection provides if (paragraph.isRTL) { LayoutDirection.Rtl } else { LayoutDirection.Ltr - } - - CompositionLocalProvider(LocalLayoutDirection provides direction) { - FlowRow( - modifier = Modifier.align(if (paragraph.isRTL) Alignment.End else Alignment.Start), - horizontalArrangement = Arrangement.spacedBy(spaceWidth), - ) { - paragraph.words.forEach { word -> - RenderWordWithPreview( - word, - state, - backgroundColor, - textStyle, - accountViewModel, - nav, - ) - } - } - } - } - } else { - // FlowRow doesn't work well with paragraphs. So we need to split them - state.paragraphs.forEach { paragraph -> - val direction = - if (paragraph.isRTL) { - LayoutDirection.Rtl - } else { - LayoutDirection.Ltr - } - - CompositionLocalProvider(LocalLayoutDirection provides direction) { - FlowRow( - horizontalArrangement = Arrangement.spacedBy(spaceWidth), - modifier = Modifier.align(if (paragraph.isRTL) Alignment.End else Alignment.Start), - ) { - paragraph.words.forEach { word -> - RenderWordWithoutPreview( - word, - state, - backgroundColor, - textStyle, - accountViewModel, - nav, - ) - } + }, + LocalTextStyle provides textStyle, + ) { + FlowRow( + modifier = Modifier.align(if (paragraph.isRTL) Alignment.End else Alignment.Start), + horizontalArrangement = Arrangement.spacedBy(spaceWidth), + ) { + paragraph.words.forEach { word -> + wordRenderer(word, state) } } } @@ -281,7 +337,6 @@ private fun RenderWordWithoutPreview( word: Segment, state: RichTextViewerState, backgroundColor: MutableState, - style: TextStyle, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { @@ -291,10 +346,10 @@ private fun RenderWordWithoutPreview( is LinkSegment -> ClickableUrl(word.segmentText, word.segmentText) is EmojiSegment -> RenderCustomEmoji(word.segmentText, state) // Don't offer to pay invoices - is InvoiceSegment -> NormalWord(word.segmentText, style) + is InvoiceSegment -> Text(word.segmentText) // Don't offer to withdraw - is WithdrawSegment -> NormalWord(word.segmentText, style) - is CashuSegment -> NormalWord(word.segmentText, style) + is WithdrawSegment -> Text(word.segmentText) + is CashuSegment -> Text(word.segmentText) is EmailSegment -> ClickableEmail(word.segmentText) is PhoneSegment -> ClickablePhone(word.segmentText) is BechSegment -> BechLink(word.segmentText, false, backgroundColor, accountViewModel, nav) @@ -302,7 +357,7 @@ private fun RenderWordWithoutPreview( is HashIndexUserSegment -> TagLink(word, accountViewModel, nav) is HashIndexEventSegment -> TagLink(word, false, backgroundColor, accountViewModel, nav) is SchemelessUrlSegment -> NoProtocolUrlRenderer(word) - is RegularTextSegment -> NormalWord(word.segmentText, style) + is RegularTextSegment -> Text(word.segmentText) } } @@ -311,7 +366,6 @@ private fun RenderWordWithPreview( word: Segment, state: RichTextViewerState, backgroundColor: MutableState, - style: TextStyle, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { @@ -329,7 +383,7 @@ private fun RenderWordWithPreview( is HashIndexUserSegment -> TagLink(word, accountViewModel, nav) is HashIndexEventSegment -> TagLink(word, true, backgroundColor, accountViewModel, nav) is SchemelessUrlSegment -> NoProtocolUrlRenderer(word) - is RegularTextSegment -> NormalWord(word.segmentText, style) + is RegularTextSegment -> Text(word.segmentText) } } @@ -347,23 +401,7 @@ private fun ZoomableContentView( } @Composable -private fun NormalWord( - word: String, - style: TextStyle, -) { - BasicText( - text = word, - style = style, - ) -} - -@Composable -private fun NoProtocolUrlRenderer(word: SchemelessUrlSegment) { - RenderUrl(word) -} - -@Composable -private fun RenderUrl(segment: SchemelessUrlSegment) { +private fun NoProtocolUrlRenderer(segment: SchemelessUrlSegment) { ClickableUrl(segment.url, "https://${segment.url}") segment.extras?.let { it1 -> Text(it1) } } @@ -649,7 +687,7 @@ fun HashTag( } } - segment.extras?.ifBlank { "" }?.let { withStyle(regularText) { append(it) } } + segment.extras?.let { withStyle(regularText) { append(it) } } } } From 133d7d5ce0d63014115a815fb30774f6dfad2ec3 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 15 Feb 2024 11:00:23 -0500 Subject: [PATCH 088/176] v0.84.2 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 188c46661..18c367c77 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 34 - versionCode 356 - versionName "0.84.1" + versionCode 357 + versionName "0.84.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From 03a9ec8ed256b93eef1b49f1f725fa807c6e9aaa Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 15 Feb 2024 11:03:01 -0500 Subject: [PATCH 089/176] Refactors clickable text --- .../amethyst/ui/components/ClickableRoute.kt | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt index af2fce936..9bcfbe5bd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt @@ -304,20 +304,20 @@ fun CreateClickableText( val primaryColor = MaterialTheme.colorScheme.primary val onBackgroundColor = MaterialTheme.colorScheme.onBackground - val clickablePartStyle = - SpanStyle( - color = overrideColor ?: primaryColor, - fontWeight = fontWeight, - ) - - val nonClickablePartStyle = - SpanStyle( - color = overrideColor ?: onBackgroundColor, - fontWeight = fontWeight, - ) - val text = - remember(clickablePartStyle, nonClickablePartStyle, clickablePart, suffix) { + remember(clickablePart, suffix) { + val clickablePartStyle = + SpanStyle( + color = overrideColor ?: primaryColor, + fontWeight = fontWeight, + ) + + val nonClickablePartStyle = + SpanStyle( + color = overrideColor ?: onBackgroundColor, + fontWeight = fontWeight, + ) + buildAnnotatedString { withStyle(clickablePartStyle) { append(clickablePart) } if (!suffix.isNullOrBlank()) { From 6553f3bec5e43446ace0a6221407e898e9d65d69 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 15 Feb 2024 16:05:53 -0500 Subject: [PATCH 090/176] Normalizes passwords to NFKC in NIP49 --- quartz/build.gradle | 4 +++- .../src/main/java/com/vitorpamplona/quartz/crypto/Nip49.kt | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/quartz/build.gradle b/quartz/build.gradle index f2422b021..5145a3faf 100644 --- a/quartz/build.gradle +++ b/quartz/build.gradle @@ -49,15 +49,17 @@ dependencies { // Bitcoin secp256k1 bindings to Android api 'fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.13.0' - // LibSodium for ChaCha encryption + // LibSodium for ChaCha encryption (NIP-44) implementation "com.goterl:lazysodium-android:5.1.0@aar" implementation 'net.java.dev.jna:jna:5.14.0@aar' + // Performant Parser of JSONs into Events api 'com.fasterxml.jackson.module:jackson-module-kotlin:2.16.1' // immutable collections to avoid recomposition api('org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7') + // scrypt for NIP-49 api('com.lambdaworks:scrypt:1.4.0') // Parses URLs from Text: diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip49.kt b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip49.kt index 2a282e5e3..7863fb953 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip49.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip49.kt @@ -31,6 +31,7 @@ import com.vitorpamplona.quartz.encoders.hexToByteArray import com.vitorpamplona.quartz.encoders.toHexKey import fr.acinq.secp256k1.Secp256k1 import java.security.SecureRandom +import java.text.Normalizer class Nip49(val secp256k1: Secp256k1, val random: SecureRandom) { private val libSodium = SodiumAndroid() @@ -50,8 +51,9 @@ class Nip49(val secp256k1: Secp256k1, val random: SecureRandom) { check(encryptedInfo != null) { "Couldn't decode key" } check(encryptedInfo.version == EncryptedInfo.V) { "invalid version" } + val normalizedPassword = Normalizer.normalize(password, Normalizer.Form.NFKC).toByteArray(Charsets.UTF_8) val n = Math.pow(2.0, encryptedInfo.logn.toDouble()).toInt() - val key = SCrypt.scrypt(password.toByteArray(Charsets.UTF_8), encryptedInfo.salt, n, 8, 1, 32) + val key = SCrypt.scrypt(normalizedPassword, encryptedInfo.salt, n, 8, 1, 32) val m = ByteArray(32) lazySodium.cryptoAeadXChaCha20Poly1305IetfDecrypt( @@ -93,8 +95,9 @@ class Nip49(val secp256k1: Secp256k1, val random: SecureRandom) { val nonce = ByteArray(24) random.nextBytes(nonce) + val normalizedPassword = Normalizer.normalize(password, Normalizer.Form.NFKC).toByteArray(Charsets.UTF_8) val n = Math.pow(2.0, logn.toDouble()).toInt() - val key = SCrypt.scrypt(password.toByteArray(Charsets.UTF_8), salt, n, 8, 1, 32) + val key = SCrypt.scrypt(normalizedPassword, salt, n, 8, 1, 32) val ciphertext = ByteArray(48) // byte[] c, long[] cLen, From 52c6ce484913f6efa9a0c20a67f2a1478cc64159 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 15 Feb 2024 17:42:19 -0500 Subject: [PATCH 091/176] Updates scrypt lib to support empty keys. --- quartz/build.gradle | 3 - .../com/vitorpamplona/quartz/NIP49Test.kt | 4 + .../com/vitorpamplona/quartz/crypto/Nip49.kt | 1 - .../vitorpamplona/quartz/crypto/PBKDF.java | 87 +++++++ .../vitorpamplona/quartz/crypto/SCrypt.java | 188 +++++++++++++++ .../quartz/crypto/SecretKeyOrEmptySpec.java | 228 ++++++++++++++++++ 6 files changed, 507 insertions(+), 4 deletions(-) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/crypto/PBKDF.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/crypto/SCrypt.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/crypto/SecretKeyOrEmptySpec.java diff --git a/quartz/build.gradle b/quartz/build.gradle index 5145a3faf..8f24aca24 100644 --- a/quartz/build.gradle +++ b/quartz/build.gradle @@ -59,9 +59,6 @@ dependencies { // immutable collections to avoid recomposition api('org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7') - // scrypt for NIP-49 - api('com.lambdaworks:scrypt:1.4.0') - // Parses URLs from Text: api "io.github.url-detector:url-detector:0.1.23" diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP49Test.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP49Test.kt index 01825904a..3513020d8 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP49Test.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP49Test.kt @@ -46,6 +46,10 @@ public class NIP49Test { Nip49TestCase(".ksjabdk.aselqwe", "14c226dbdd865d5e1645e72c7470fd0a17feb42cc87b750bab6538171b3a3f8a", 7, 0x00), Nip49TestCase("skjdaklrnçurbç l", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 8, 0x01), Nip49TestCase("777z7z7z7z7z7z7z", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 9, 0x02), + Nip49TestCase("", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 4, 0x00), + Nip49TestCase("", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 5, 0x01), + Nip49TestCase("", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 1, 0x00), + Nip49TestCase("", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 9, 0x01), ) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip49.kt b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip49.kt index 7863fb953..199965227 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip49.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip49.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.quartz.crypto import android.util.Log import com.goterl.lazysodium.LazySodiumAndroid import com.goterl.lazysodium.SodiumAndroid -import com.lambdaworks.crypto.SCrypt import com.vitorpamplona.quartz.encoders.Bech32 import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.bechToBytes diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/PBKDF.java b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/PBKDF.java new file mode 100644 index 000000000..638a8b315 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/PBKDF.java @@ -0,0 +1,87 @@ +// Copyright (C) 2011 - Will Glozer. All rights reserved. + +package com.vitorpamplona.quartz.crypto; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.security.GeneralSecurityException; +import static java.lang.System.arraycopy; + +/** + * An implementation of the Password-Based Key Derivation Function as specified + * in RFC 2898. + * + * @author Will Glozer + */ +public class PBKDF { + /** + * Implementation of PBKDF2 (RFC2898). + * + * @param alg HMAC algorithm to use. + * @param P Password. + * @param S Salt. + * @param c Iteration count. + * @param dkLen Intended length, in octets, of the derived key. + * + * @return The derived key. + * + * @throws GeneralSecurityException + */ + public static byte[] pbkdf2(String alg, byte[] P, byte[] S, int c, int dkLen) throws GeneralSecurityException { + Mac mac = Mac.getInstance(alg); + mac.init(new SecretKeySpec(P, alg)); + byte[] DK = new byte[dkLen]; + pbkdf2(mac, S, c, DK, dkLen); + return DK; + } + + /** + * Implementation of PBKDF2 (RFC2898). + * + * @param mac Pre-initialized {@link Mac} instance to use. + * @param S Salt. + * @param c Iteration count. + * @param DK Byte array that derived key will be placed in. + * @param dkLen Intended length, in octets, of the derived key. + * + * @throws GeneralSecurityException + */ + public static void pbkdf2(Mac mac, byte[] S, int c, byte[] DK, int dkLen) throws GeneralSecurityException { + int hLen = mac.getMacLength(); + + if (dkLen > (Math.pow(2, 32) - 1) * hLen) { + throw new GeneralSecurityException("Requested key length too long"); + } + + byte[] U = new byte[hLen]; + byte[] T = new byte[hLen]; + byte[] block1 = new byte[S.length + 4]; + + int l = (int) Math.ceil((double) dkLen / hLen); + int r = dkLen - (l - 1) * hLen; + + arraycopy(S, 0, block1, 0, S.length); + + for (int i = 1; i <= l; i++) { + block1[S.length + 0] = (byte) (i >> 24 & 0xff); + block1[S.length + 1] = (byte) (i >> 16 & 0xff); + block1[S.length + 2] = (byte) (i >> 8 & 0xff); + block1[S.length + 3] = (byte) (i >> 0 & 0xff); + + mac.update(block1); + mac.doFinal(U, 0); + arraycopy(U, 0, T, 0, hLen); + + for (int j = 1; j < c; j++) { + mac.update(U); + mac.doFinal(U, 0); + + for (int k = 0; k < hLen; k++) { + T[k] ^= U[k]; + } + } + + arraycopy(T, 0, DK, (i - 1) * hLen, (i == l ? r : hLen)); + } + } +} \ No newline at end of file diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/SCrypt.java b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/SCrypt.java new file mode 100644 index 000000000..c464013a2 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/SCrypt.java @@ -0,0 +1,188 @@ +// Copyright (C) 2011 - Will Glozer. All rights reserved. + +package com.vitorpamplona.quartz.crypto; + +import javax.crypto.Mac; +import java.security.GeneralSecurityException; + +import static java.lang.Integer.MAX_VALUE; +import static java.lang.System.arraycopy; + +/** + * An implementation of the scrypt + * key derivation function. This class will attempt to load a native library + * containing the optimized C implementation from + * http://www.tarsnap.com/scrypt.html and + * fall back to the pure Java version if that fails. + * + * @author Will Glozer + */ +public class SCrypt { + + /** + * Implementation of the scrypt KDF. + * + * @param passwd Password. + * @param salt Salt. + * @param N CPU cost parameter. + * @param r Memory cost parameter. + * @param p Parallelization parameter. + * @param dkLen Intended length of the derived key. + * + * @return The derived key. + * + * @throws GeneralSecurityException when HMAC_SHA256 is not available. + */ + public static byte[] scrypt(byte[] passwd, byte[] salt, int N, int r, int p, int dkLen) throws GeneralSecurityException { + return scryptJ(passwd, salt, N, r, p, dkLen); + } + + /** + * Pure Java implementation of the scrypt KDF. + * + * @param passwd Password. + * @param salt Salt. + * @param N CPU cost parameter. + * @param r Memory cost parameter. + * @param p Parallelization parameter. + * @param dkLen Intended length of the derived key. + * + * @return The derived key. + * + * @throws GeneralSecurityException when HMAC_SHA256 is not available. + */ + public static byte[] scryptJ(byte[] passwd, byte[] salt, int N, int r, int p, int dkLen) throws GeneralSecurityException { + if (N < 2 || (N & (N - 1)) != 0) throw new IllegalArgumentException("N must be a power of 2 greater than 1"); + + if (N > MAX_VALUE / 128 / r) throw new IllegalArgumentException("Parameter N is too large"); + if (r > MAX_VALUE / 128 / p) throw new IllegalArgumentException("Parameter r is too large"); + + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeyOrEmptySpec(passwd, "HmacSHA256")); + + byte[] DK = new byte[dkLen]; + + byte[] B = new byte[128 * r * p]; + byte[] XY = new byte[256 * r]; + byte[] V = new byte[128 * r * N]; + int i; + + PBKDF.pbkdf2(mac, salt, 1, B, p * 128 * r); + + for (i = 0; i < p; i++) { + smix(B, i * 128 * r, r, N, V, XY); + } + + PBKDF.pbkdf2(mac, B, 1, DK, dkLen); + + return DK; + } + + public static void smix(byte[] B, int Bi, int r, int N, byte[] V, byte[] XY) { + int Xi = 0; + int Yi = 128 * r; + int i; + + arraycopy(B, Bi, XY, Xi, 128 * r); + + for (i = 0; i < N; i++) { + arraycopy(XY, Xi, V, i * (128 * r), 128 * r); + blockmix_salsa8(XY, Xi, Yi, r); + } + + for (i = 0; i < N; i++) { + int j = integerify(XY, Xi, r) & (N - 1); + blockxor(V, j * (128 * r), XY, Xi, 128 * r); + blockmix_salsa8(XY, Xi, Yi, r); + } + + arraycopy(XY, Xi, B, Bi, 128 * r); + } + + public static void blockmix_salsa8(byte[] BY, int Bi, int Yi, int r) { + byte[] X = new byte[64]; + int i; + + arraycopy(BY, Bi + (2 * r - 1) * 64, X, 0, 64); + + for (i = 0; i < 2 * r; i++) { + blockxor(BY, i * 64, X, 0, 64); + salsa20_8(X); + arraycopy(X, 0, BY, Yi + (i * 64), 64); + } + + for (i = 0; i < r; i++) { + arraycopy(BY, Yi + (i * 2) * 64, BY, Bi + (i * 64), 64); + } + + for (i = 0; i < r; i++) { + arraycopy(BY, Yi + (i * 2 + 1) * 64, BY, Bi + (i + r) * 64, 64); + } + } + + public static int R(int a, int b) { + return (a << b) | (a >>> (32 - b)); + } + + public static void salsa20_8(byte[] B) { + int[] B32 = new int[16]; + int[] x = new int[16]; + int i; + + for (i = 0; i < 16; i++) { + B32[i] = (B[i * 4 + 0] & 0xff) << 0; + B32[i] |= (B[i * 4 + 1] & 0xff) << 8; + B32[i] |= (B[i * 4 + 2] & 0xff) << 16; + B32[i] |= (B[i * 4 + 3] & 0xff) << 24; + } + + arraycopy(B32, 0, x, 0, 16); + + for (i = 8; i > 0; i -= 2) { + x[ 4] ^= R(x[ 0]+x[12], 7); x[ 8] ^= R(x[ 4]+x[ 0], 9); + x[12] ^= R(x[ 8]+x[ 4],13); x[ 0] ^= R(x[12]+x[ 8],18); + x[ 9] ^= R(x[ 5]+x[ 1], 7); x[13] ^= R(x[ 9]+x[ 5], 9); + x[ 1] ^= R(x[13]+x[ 9],13); x[ 5] ^= R(x[ 1]+x[13],18); + x[14] ^= R(x[10]+x[ 6], 7); x[ 2] ^= R(x[14]+x[10], 9); + x[ 6] ^= R(x[ 2]+x[14],13); x[10] ^= R(x[ 6]+x[ 2],18); + x[ 3] ^= R(x[15]+x[11], 7); x[ 7] ^= R(x[ 3]+x[15], 9); + x[11] ^= R(x[ 7]+x[ 3],13); x[15] ^= R(x[11]+x[ 7],18); + x[ 1] ^= R(x[ 0]+x[ 3], 7); x[ 2] ^= R(x[ 1]+x[ 0], 9); + x[ 3] ^= R(x[ 2]+x[ 1],13); x[ 0] ^= R(x[ 3]+x[ 2],18); + x[ 6] ^= R(x[ 5]+x[ 4], 7); x[ 7] ^= R(x[ 6]+x[ 5], 9); + x[ 4] ^= R(x[ 7]+x[ 6],13); x[ 5] ^= R(x[ 4]+x[ 7],18); + x[11] ^= R(x[10]+x[ 9], 7); x[ 8] ^= R(x[11]+x[10], 9); + x[ 9] ^= R(x[ 8]+x[11],13); x[10] ^= R(x[ 9]+x[ 8],18); + x[12] ^= R(x[15]+x[14], 7); x[13] ^= R(x[12]+x[15], 9); + x[14] ^= R(x[13]+x[12],13); x[15] ^= R(x[14]+x[13],18); + } + + for (i = 0; i < 16; ++i) B32[i] = x[i] + B32[i]; + + for (i = 0; i < 16; i++) { + B[i * 4 + 0] = (byte) (B32[i] >> 0 & 0xff); + B[i * 4 + 1] = (byte) (B32[i] >> 8 & 0xff); + B[i * 4 + 2] = (byte) (B32[i] >> 16 & 0xff); + B[i * 4 + 3] = (byte) (B32[i] >> 24 & 0xff); + } + } + + public static void blockxor(byte[] S, int Si, byte[] D, int Di, int len) { + for (int i = 0; i < len; i++) { + D[Di + i] ^= S[Si + i]; + } + } + + public static int integerify(byte[] B, int Bi, int r) { + int n; + + Bi += (2 * r - 1) * 64; + + n = (B[Bi + 0] & 0xff) << 0; + n |= (B[Bi + 1] & 0xff) << 8; + n |= (B[Bi + 2] & 0xff) << 16; + n |= (B[Bi + 3] & 0xff) << 24; + + return n; + } +} \ No newline at end of file diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/SecretKeyOrEmptySpec.java b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/SecretKeyOrEmptySpec.java new file mode 100644 index 000000000..7e3766bdc --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/SecretKeyOrEmptySpec.java @@ -0,0 +1,228 @@ +/* + * Copyright (c) 1998, 2015, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.vitorpamplona.quartz.crypto; + +import java.security.MessageDigest; +import java.security.spec.KeySpec; +import java.util.Locale; +import javax.crypto.SecretKey; + +/** + * This class specifies a secret key in a provider-independent fashion. + * + *

It can be used to construct a SecretKey from a byte array, + * without having to go through a (provider-based) + * SecretKeyFactory. + * + *

This class is only useful for raw secret keys that can be represented as + * a byte array and have no key parameters associated with them, e.g., DES or + * Triple DES keys. + * + * @author Jan Luehe + * + * @see javax.crypto.SecretKey + * @see javax.crypto.SecretKeyFactory + * @since 1.4 + */ +public class SecretKeyOrEmptySpec implements KeySpec, SecretKey { + + private static final long serialVersionUID = 6577238317307289933L; + + /** + * The secret key. + * + * @serial + */ + private byte[] key; + + /** + * The name of the algorithm associated with this key. + * + * @serial + */ + private String algorithm; + + /** + * Constructs a secret key from the given byte array. + * + *

This constructor does not check if the given bytes indeed specify a + * secret key of the specified algorithm. For example, if the algorithm is + * DES, this constructor does not check if key is 8 bytes + * long, and also does not check for weak or semi-weak keys. + * In order for those checks to be performed, an algorithm-specific + * key specification class (in this case: + * {@link DESKeySpec DESKeySpec}) + * should be used. + * + * @param key the key material of the secret key. The contents of + * the array are copied to protect against subsequent modification. + * @param algorithm the name of the secret-key algorithm to be associated + * with the given key material. + * See Appendix A in the + * Java Cryptography Architecture Reference Guide + * for information about standard algorithm names. + * @exception IllegalArgumentException if algorithm + * is null or key is null or empty. + */ + public SecretKeyOrEmptySpec(byte[] key, String algorithm) { + if (key == null || algorithm == null) { + throw new IllegalArgumentException("Missing argument"); + } + this.key = key.clone(); + this.algorithm = algorithm; + } + + /** + * Constructs a secret key from the given byte array, using the first + * len bytes of key, starting at + * offset inclusive. + * + *

The bytes that constitute the secret key are + * those between key[offset] and + * key[offset+len-1] inclusive. + * + *

This constructor does not check if the given bytes indeed specify a + * secret key of the specified algorithm. For example, if the algorithm is + * DES, this constructor does not check if key is 8 bytes + * long, and also does not check for weak or semi-weak keys. + * In order for those checks to be performed, an algorithm-specific key + * specification class (in this case: + * {@link DESKeySpec DESKeySpec}) + * must be used. + * + * @param key the key material of the secret key. The first + * len bytes of the array beginning at + * offset inclusive are copied to protect + * against subsequent modification. + * @param offset the offset in key where the key material + * starts. + * @param len the length of the key material. + * @param algorithm the name of the secret-key algorithm to be associated + * with the given key material. + * See Appendix A in the + * Java Cryptography Architecture Reference Guide + * for information about standard algorithm names. + * @exception IllegalArgumentException if algorithm + * is null or key is null, empty, or too short, + * i.e. {@code key.length-offsetoffset or len index bytes outside the + * key. + */ + public SecretKeyOrEmptySpec(byte[] key, int offset, int len, String algorithm) { + if (key == null || algorithm == null) { + throw new IllegalArgumentException("Missing argument"); + } + if (key.length-offset < len) { + throw new IllegalArgumentException + ("Invalid offset/length combination"); + } + if (len < 0) { + throw new ArrayIndexOutOfBoundsException("len is negative"); + } + this.key = new byte[len]; + System.arraycopy(key, offset, this.key, 0, len); + this.algorithm = algorithm; + } + + /** + * Returns the name of the algorithm associated with this secret key. + * + * @return the secret key algorithm. + */ + public String getAlgorithm() { + return this.algorithm; + } + + /** + * Returns the name of the encoding format for this secret key. + * + * @return the string "RAW". + */ + public String getFormat() { + return "RAW"; + } + + /** + * Returns the key material of this secret key. + * + * @return the key material. Returns a new array + * each time this method is called. + */ + public byte[] getEncoded() { + return this.key.clone(); + } + + /** + * Calculates a hash code value for the object. + * Objects that are equal will also have the same hashcode. + */ + public int hashCode() { + int retval = 0; + for (int i = 1; i < this.key.length; i++) { + retval += this.key[i] * i; + } + if (this.algorithm.equalsIgnoreCase("TripleDES")) + return (retval ^= "desede".hashCode()); + else + return (retval ^= + this.algorithm.toLowerCase(Locale.ENGLISH).hashCode()); + } + + /** + * Tests for equality between the specified object and this + * object. Two SecretKeySpec objects are considered equal if + * they are both SecretKey instances which have the + * same case-insensitive algorithm name and key encoding. + * + * @param obj the object to test for equality with this object. + * + * @return true if the objects are considered equal, false if + * obj is null or otherwise. + */ + public boolean equals(Object obj) { + if (this == obj) + return true; + + if (!(obj instanceof SecretKey)) + return false; + + String thatAlg = ((SecretKey)obj).getAlgorithm(); + if (!(thatAlg.equalsIgnoreCase(this.algorithm))) { + if ((!(thatAlg.equalsIgnoreCase("DESede")) + || !(this.algorithm.equalsIgnoreCase("TripleDES"))) + && (!(thatAlg.equalsIgnoreCase("TripleDES")) + || !(this.algorithm.equalsIgnoreCase("DESede")))) + return false; + } + + byte[] thatKey = ((SecretKey)obj).getEncoded(); + + return MessageDigest.isEqual(this.key, thatKey); + } +} From 42cc04c194af443e10d84e84d76bbd44092a1ed6 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 15 Feb 2024 18:31:26 -0500 Subject: [PATCH 092/176] updates license notice year --- .../java/com/vitorpamplona/amethyst/ImageUploadTesting.kt | 2 +- .../java/com/vitorpamplona/amethyst/RichTextParserTest.kt | 2 +- .../java/com/vitorpamplona/amethyst/ThreadAssemblerTest.kt | 2 +- .../com/vitorpamplona/amethyst/UrlUserTagTransformationTest.kt | 2 +- .../java/com/vitorpamplona/amethyst/TranslationsTest.kt | 2 +- .../amethyst/service/lang/LanguageTranslatorService.kt | 2 +- .../amethyst/service/notifications/PushDistributorHandler.kt | 2 +- .../amethyst/service/notifications/PushMessageReceiver.kt | 2 +- .../amethyst/service/notifications/PushNotificationUtils.kt | 2 +- .../amethyst/ui/components/SelectNotificationProvider.kt | 2 +- .../amethyst/ui/components/TranslatableRichTextViewer.kt | 2 +- .../java/androidx/compose/material3/pullrefresh/PullRefresh.kt | 2 +- .../compose/material3/pullrefresh/PullRefreshIndicator.kt | 2 +- .../material3/pullrefresh/PullRefreshIndicatorTransform.kt | 2 +- .../androidx/compose/material3/pullrefresh/PullRefreshState.kt | 2 +- app/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt | 2 +- .../main/java/com/vitorpamplona/amethyst/EncryptedStorage.kt | 2 +- .../main/java/com/vitorpamplona/amethyst/LocalPreferences.kt | 2 +- app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt | 2 +- app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt | 2 +- .../java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt | 2 +- app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt | 2 +- app/src/main/java/com/vitorpamplona/amethyst/model/Chatroom.kt | 2 +- .../main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt | 2 +- .../main/java/com/vitorpamplona/amethyst/model/LocalCache.kt | 2 +- app/src/main/java/com/vitorpamplona/amethyst/model/Nip47URI.kt | 2 +- app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt | 2 +- .../com/vitorpamplona/amethyst/model/ParticipantListBuilder.kt | 2 +- .../java/com/vitorpamplona/amethyst/model/RelayInformation.kt | 2 +- .../java/com/vitorpamplona/amethyst/model/RelaySetupInfo.kt | 2 +- app/src/main/java/com/vitorpamplona/amethyst/model/Settings.kt | 2 +- .../java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt | 2 +- .../java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt | 2 +- app/src/main/java/com/vitorpamplona/amethyst/model/User.kt | 2 +- .../java/com/vitorpamplona/amethyst/service/BlurHashDecoder.kt | 2 +- .../java/com/vitorpamplona/amethyst/service/BlurHashImage.kt | 2 +- .../com/vitorpamplona/amethyst/service/CachedRichTextParser.kt | 2 +- .../java/com/vitorpamplona/amethyst/service/CashuProcessor.kt | 2 +- .../main/java/com/vitorpamplona/amethyst/service/EmojiUtils.kt | 2 +- .../main/java/com/vitorpamplona/amethyst/service/FileHeader.kt | 2 +- .../com/vitorpamplona/amethyst/service/HttpClientManager.kt | 2 +- .../java/com/vitorpamplona/amethyst/service/LocationUtil.kt | 2 +- .../com/vitorpamplona/amethyst/service/MainThreadChecker.kt | 2 +- .../vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt | 2 +- .../vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt | 2 +- .../java/com/vitorpamplona/amethyst/service/Nip30CustomEmoji.kt | 2 +- .../vitorpamplona/amethyst/service/Nip47WalletConnectParser.kt | 2 +- .../com/vitorpamplona/amethyst/service/Nip96MediaServers.kt | 2 +- .../java/com/vitorpamplona/amethyst/service/Nip96Uploader.kt | 2 +- .../vitorpamplona/amethyst/service/NostrAccountDataSource.kt | 2 +- .../vitorpamplona/amethyst/service/NostrChannelDataSource.kt | 2 +- .../vitorpamplona/amethyst/service/NostrChatroomDataSource.kt | 2 +- .../amethyst/service/NostrChatroomListDataSource.kt | 2 +- .../vitorpamplona/amethyst/service/NostrCommunityDataSource.kt | 2 +- .../java/com/vitorpamplona/amethyst/service/NostrDataSource.kt | 2 +- .../vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt | 2 +- .../vitorpamplona/amethyst/service/NostrGeohashDataSource.kt | 2 +- .../vitorpamplona/amethyst/service/NostrHashtagDataSource.kt | 2 +- .../com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt | 2 +- .../amethyst/service/NostrLnZapPaymentResponseDataSource.kt | 2 +- .../amethyst/service/NostrSearchEventOrUserDataSource.kt | 2 +- .../amethyst/service/NostrSingleChannelDataSource.kt | 2 +- .../amethyst/service/NostrSingleEventDataSource.kt | 2 +- .../vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt | 2 +- .../com/vitorpamplona/amethyst/service/NostrThreadDataSource.kt | 2 +- .../amethyst/service/NostrUserProfileDataSource.kt | 2 +- .../com/vitorpamplona/amethyst/service/NostrVideoDataSource.kt | 2 +- .../main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt | 2 +- .../java/com/vitorpamplona/amethyst/service/PackageUtils.kt | 2 +- .../com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt | 2 +- .../amethyst/service/lnurl/LightningAddressResolver.kt | 2 +- .../com/vitorpamplona/amethyst/service/model/zaps/UserZaps.kt | 2 +- .../amethyst/service/notifications/EventNotificationConsumer.kt | 2 +- .../amethyst/service/notifications/NotificationUtils.kt | 2 +- .../amethyst/service/notifications/RegisterAccounts.kt | 2 +- .../amethyst/service/playback/MultiPlayerPlaybackManager.kt | 2 +- .../amethyst/service/playback/PlaybackClientController.kt | 2 +- .../vitorpamplona/amethyst/service/playback/PlaybackService.kt | 2 +- .../com/vitorpamplona/amethyst/service/playback/VideoCache.kt | 2 +- .../amethyst/service/playback/VideoViewedPositionCache.kt | 2 +- .../vitorpamplona/amethyst/service/previews/BahaUrlPreview.kt | 2 +- .../amethyst/service/previews/IUrlPreviewCallback.kt | 2 +- .../com/vitorpamplona/amethyst/service/previews/UrlInfoItem.kt | 2 +- .../vitorpamplona/amethyst/service/previews/UrlPreviewUtils.kt | 2 +- .../java/com/vitorpamplona/amethyst/service/relays/Client.kt | 2 +- .../java/com/vitorpamplona/amethyst/service/relays/Constants.kt | 2 +- .../main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt | 2 +- .../com/vitorpamplona/amethyst/service/relays/JsonFilter.kt | 2 +- .../java/com/vitorpamplona/amethyst/service/relays/Relay.kt | 2 +- .../java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt | 2 +- .../com/vitorpamplona/amethyst/service/relays/Subscription.kt | 2 +- .../com/vitorpamplona/amethyst/service/relays/TypedFilter.kt | 2 +- .../vitorpamplona/amethyst/service/tts/TextToSpeechEngine.kt | 2 +- .../vitorpamplona/amethyst/service/tts/TextToSpeechHelper.kt | 2 +- app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt | 2 +- .../com/vitorpamplona/amethyst/ui/actions/ImageDownloader.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/actions/ImageSaver.kt | 2 +- .../com/vitorpamplona/amethyst/ui/actions/InformationDialog.kt | 2 +- .../vitorpamplona/amethyst/ui/actions/JoinUserOrChannelView.kt | 2 +- .../com/vitorpamplona/amethyst/ui/actions/NewChannelView.kt | 2 +- .../vitorpamplona/amethyst/ui/actions/NewChannelViewModel.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt | 2 +- .../com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt | 2 +- .../com/vitorpamplona/amethyst/ui/actions/NewPollClosing.kt | 2 +- .../amethyst/ui/actions/NewPollConsensusThreshold.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/actions/NewPollOption.kt | 2 +- .../amethyst/ui/actions/NewPollPrimaryDescription.kt | 2 +- .../vitorpamplona/amethyst/ui/actions/NewPollRecipientsField.kt | 2 +- .../vitorpamplona/amethyst/ui/actions/NewPollVoteValueRange.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt | 2 +- .../com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt | 2 +- .../com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt | 2 +- .../vitorpamplona/amethyst/ui/actions/NewRelayListViewModel.kt | 2 +- .../vitorpamplona/amethyst/ui/actions/NewUserMetadataView.kt | 2 +- .../amethyst/ui/actions/NewUserMetadataViewModel.kt | 2 +- .../vitorpamplona/amethyst/ui/actions/NotifyRequestDialog.kt | 2 +- .../vitorpamplona/amethyst/ui/actions/RelayInformationDialog.kt | 2 +- .../vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/actions/SaveToGallery.kt | 2 +- .../com/vitorpamplona/amethyst/ui/actions/UploadFromGallery.kt | 2 +- .../amethyst/ui/actions/UrlUserTagTransformation.kt | 2 +- .../com/vitorpamplona/amethyst/ui/buttons/ChannelFabColumn.kt | 2 +- .../com/vitorpamplona/amethyst/ui/buttons/NewChannelButton.kt | 2 +- .../vitorpamplona/amethyst/ui/buttons/NewCommunityNoteButton.kt | 2 +- .../com/vitorpamplona/amethyst/ui/buttons/NewImageButton.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/buttons/NewNoteButton.kt | 2 +- .../amethyst/ui/components/AudioWaveformReadOnly.kt | 2 +- .../com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt | 2 +- .../com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt | 2 +- .../com/vitorpamplona/amethyst/ui/components/ClickableEmail.kt | 2 +- .../vitorpamplona/amethyst/ui/components/ClickableNoteTag.kt | 2 +- .../com/vitorpamplona/amethyst/ui/components/ClickablePhone.kt | 2 +- .../com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt | 2 +- .../com/vitorpamplona/amethyst/ui/components/ClickableUrl.kt | 2 +- .../vitorpamplona/amethyst/ui/components/ClickableUserTag.kt | 2 +- .../vitorpamplona/amethyst/ui/components/ClickableWithdrawal.kt | 2 +- .../amethyst/ui/components/ExpandableRichTextViewer.kt | 2 +- .../com/vitorpamplona/amethyst/ui/components/GenericLoadable.kt | 2 +- .../com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt | 2 +- .../com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt | 2 +- .../com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt | 2 +- .../com/vitorpamplona/amethyst/ui/components/MarkdownParser.kt | 2 +- .../com/vitorpamplona/amethyst/ui/components/MediaCompressor.kt | 2 +- .../com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/components/Robohash.kt | 2 +- .../vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt | 2 +- .../vitorpamplona/amethyst/ui/components/SelectTextDialog.kt | 2 +- .../vitorpamplona/amethyst/ui/components/SensitivityWarning.kt | 2 +- .../com/vitorpamplona/amethyst/ui/components/SlidingCarousel.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/components/SplitItem.kt | 2 +- .../com/vitorpamplona/amethyst/ui/components/TextSpinner.kt | 2 +- .../vitorpamplona/amethyst/ui/components/TranslationConfig.kt | 2 +- .../com/vitorpamplona/amethyst/ui/components/UrlPreviewCard.kt | 2 +- .../com/vitorpamplona/amethyst/ui/components/UrlPreviewState.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/components/VideoView.kt | 2 +- .../vitorpamplona/amethyst/ui/components/ZapRaiserRequest.kt | 2 +- .../vitorpamplona/amethyst/ui/components/ZoomableContentView.kt | 2 +- .../vitorpamplona/amethyst/ui/dal/BookmarkPrivateFeedFilter.kt | 2 +- .../vitorpamplona/amethyst/ui/dal/BookmarkPublicFeedFilter.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/dal/ChannelFeedFilter.kt | 2 +- .../com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt | 2 +- .../amethyst/ui/dal/ChatroomListKnownFeedFilter.kt | 2 +- .../vitorpamplona/amethyst/ui/dal/ChatroomListNewFeedFilter.kt | 2 +- .../com/vitorpamplona/amethyst/ui/dal/CommunityFeedFilter.kt | 2 +- .../com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt | 2 +- .../amethyst/ui/dal/DiscoverCommunityFeedFilter.kt | 2 +- .../com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt | 2 +- .../vitorpamplona/amethyst/ui/dal/DiscoverLiveNowFeedFilter.kt | 2 +- .../amethyst/ui/dal/DiscoverMarketplaceFeedFilter.kt | 2 +- .../main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/dal/GeoHashFeedFilter.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt | 2 +- .../vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt | 2 +- .../amethyst/ui/dal/HomeConversationsFeedFilter.kt | 2 +- .../vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt | 2 +- .../com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/dal/ThreadFeedFilter.kt | 2 +- .../amethyst/ui/dal/UserProfileAppRecommendationsFeedFilter.kt | 2 +- .../amethyst/ui/dal/UserProfileBookmarksFeedFilter.kt | 2 +- .../amethyst/ui/dal/UserProfileConversationsFeedFilter.kt | 2 +- .../amethyst/ui/dal/UserProfileFollowersFeedFilter.kt | 2 +- .../amethyst/ui/dal/UserProfileFollowsFeedFilter.kt | 2 +- .../amethyst/ui/dal/UserProfileNewThreadFeedFilter.kt | 2 +- .../amethyst/ui/dal/UserProfileReportsFeedFilter.kt | 2 +- .../vitorpamplona/amethyst/ui/dal/UserProfileZapsFeedFilter.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt | 2 +- .../com/vitorpamplona/amethyst/ui/elements/AddRemoveButtons.kt | 2 +- .../com/vitorpamplona/amethyst/ui/elements/DisplayCommunity.kt | 2 +- .../com/vitorpamplona/amethyst/ui/elements/DisplayHashtags.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/elements/DisplayPoW.kt | 2 +- .../com/vitorpamplona/amethyst/ui/elements/DisplayReward.kt | 2 +- .../amethyst/ui/elements/DisplayUncitedHashtags.kt | 2 +- .../com/vitorpamplona/amethyst/ui/elements/DisplayZapSplits.kt | 2 +- .../com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayout.kt | 2 +- .../com/vitorpamplona/amethyst/ui/layouts/LeftPictureLayout.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/layouts/RepostLayout.kt | 2 +- .../amethyst/ui/navigation/AccountSwitchBottomSheet.kt | 2 +- .../com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt | 2 +- .../com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt | 2 +- .../com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt | 2 +- .../main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt | 2 +- .../com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt | 2 +- .../com/vitorpamplona/amethyst/ui/note/ChatroomHeaderCompose.kt | 2 +- .../vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt | 2 +- app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt | 2 +- .../com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt | 2 +- .../vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt | 2 +- .../main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt | 2 +- .../com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt | 2 +- .../main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt | 2 +- .../com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/note/PubKeyFormatter.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/note/TimeAgoFormatter.kt | 2 +- .../vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt | 2 +- .../com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt | 2 +- .../main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt | 2 +- .../com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt | 2 +- .../com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeDrawer.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeScanner.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/qrcode/ShowQRDialog.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/screen/AccountState.kt | 2 +- .../vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt | 2 +- .../com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt | 2 +- .../com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt | 2 +- .../vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt | 2 +- .../main/java/com/vitorpamplona/amethyst/ui/screen/FeedState.kt | 2 +- .../main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedState.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedView.kt | 2 +- .../com/vitorpamplona/amethyst/ui/screen/LnZapFeedViewModel.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt | 2 +- .../vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt | 2 +- .../amethyst/ui/screen/SharedPreferencesViewModel.kt | 2 +- .../com/vitorpamplona/amethyst/ui/screen/StringFeedState.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/screen/StringFeedView.kt | 2 +- .../com/vitorpamplona/amethyst/ui/screen/StringFeedViewModel.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/screen/UserFeedState.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt | 2 +- .../com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt | 2 +- .../amethyst/ui/screen/loggedIn/AccountBackupDialog.kt | 2 +- .../amethyst/ui/screen/loggedIn/AccountViewModel.kt | 2 +- .../amethyst/ui/screen/loggedIn/BookmarkListScreen.kt | 2 +- .../vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt | 2 +- .../amethyst/ui/screen/loggedIn/ChatroomListScreen.kt | 2 +- .../vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt | 2 +- .../amethyst/ui/screen/loggedIn/CommunityScreen.kt | 2 +- .../amethyst/ui/screen/loggedIn/ConnectOrbotDialog.kt | 2 +- .../vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt | 2 +- .../vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt | 2 +- .../vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt | 2 +- .../amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt | 2 +- .../com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt | 2 +- .../amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt | 2 +- .../com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt | 2 +- .../amethyst/ui/screen/loggedIn/NotificationScreen.kt | 2 +- .../vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt | 2 +- .../amethyst/ui/screen/loggedIn/ReportNoteDialog.kt | 2 +- .../vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt | 2 +- .../vitorpamplona/amethyst/ui/screen/loggedIn/SettingsScreen.kt | 2 +- .../vitorpamplona/amethyst/ui/screen/loggedIn/ThreadScreen.kt | 2 +- .../vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt | 2 +- .../amethyst/ui/screen/loggedOff/LoginOrSignupScreen.kt | 2 +- .../vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt | 2 +- .../vitorpamplona/amethyst/ui/screen/loggedOff/SignUpScreen.kt | 2 +- app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt | 2 +- app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt | 2 +- app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt | 2 +- app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Type.kt | 2 +- .../amethyst/service/lang/LanguageTranslatorService.kt | 2 +- .../service/notifications/PushNotificationReceiverService.kt | 2 +- .../amethyst/service/notifications/PushNotificationUtils.kt | 2 +- .../amethyst/ui/components/SelectNotificationProvider.kt | 2 +- .../amethyst/ui/components/TranslatableRichTextViewer.kt | 2 +- app/src/test/java/com/vitorpamplona/amethyst/CharsetTest.kt | 2 +- .../com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt | 2 +- app/src/test/java/com/vitorpamplona/amethyst/SplitterTest.kt | 2 +- .../amethyst/service/Nip05NostrAddressVerifierTest.kt | 2 +- .../test/java/com/vitorpamplona/amethyst/service/Nip30Test.kt | 2 +- .../test/java/com/vitorpamplona/amethyst/service/Nip96Test.kt | 2 +- .../com/vitorpamplona/amethyst/service/zaps/UserZapsTest.kt | 2 +- .../amethyst/benchmark/ExpandableViewComputationBenchmark.kt | 2 +- .../java/com/vitorpamplona/quartz/benchmark/BechBenchmark.kt | 2 +- .../com/vitorpamplona/quartz/benchmark/ContainsBenchmark.kt | 2 +- .../java/com/vitorpamplona/quartz/benchmark/CryptoBenchmark.kt | 2 +- .../java/com/vitorpamplona/quartz/benchmark/EventBenchmark.kt | 2 +- .../com/vitorpamplona/quartz/benchmark/GiftWrapBenchmark.kt | 2 +- .../quartz/benchmark/GiftWrapReceivingBenchmark.kt | 2 +- .../vitorpamplona/quartz/benchmark/GiftWrapSigningBenchmark.kt | 2 +- .../java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt | 2 +- .../com/vitorpamplona/quartz/benchmark/RobohashBenchmark.kt | 2 +- .../com/vitorpamplona/amethyst/commons/ExpandableTextParser.kt | 2 +- .../java/com/vitorpamplona/quartz/ChatroomKeyTest.kt | 2 +- .../androidTest/java/com/vitorpamplona/quartz/CitationTests.kt | 2 +- .../java/com/vitorpamplona/quartz/CryptoUtilsTest.kt | 2 +- .../androidTest/java/com/vitorpamplona/quartz/EventSigCheck.kt | 2 +- .../java/com/vitorpamplona/quartz/GiftWrapEventTest.kt | 2 +- .../java/com/vitorpamplona/quartz/HexEncodingTest.kt | 2 +- .../java/com/vitorpamplona/quartz/LargeDBSignatureCheck.kt | 2 +- .../java/com/vitorpamplona/quartz/LnInvoiceUtilTest.kt | 2 +- .../androidTest/java/com/vitorpamplona/quartz/NIP44v2Test.kt | 2 +- .../src/androidTest/java/com/vitorpamplona/quartz/NIP49Test.kt | 2 +- .../java/com/vitorpamplona/quartz/PrivateZapTests.kt | 2 +- .../main/java/com/vitorpamplona/quartz/crypto/CryptoUtils.kt | 2 +- quartz/src/main/java/com/vitorpamplona/quartz/crypto/Hkdf.kt | 2 +- quartz/src/main/java/com/vitorpamplona/quartz/crypto/KeyPair.kt | 2 +- quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip04.kt | 2 +- quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip44v1.kt | 2 +- quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip44v2.kt | 2 +- quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip49.kt | 2 +- .../main/java/com/vitorpamplona/quartz/crypto/SharedKeyCache.kt | 2 +- .../main/java/com/vitorpamplona/quartz/crypto/SodiumUtils.kt | 2 +- quartz/src/main/java/com/vitorpamplona/quartz/encoders/ATag.kt | 2 +- .../main/java/com/vitorpamplona/quartz/encoders/Bech32Util.kt | 2 +- .../src/main/java/com/vitorpamplona/quartz/encoders/HexUtils.kt | 2 +- .../java/com/vitorpamplona/quartz/encoders/LnInvoiceUtil.kt | 2 +- .../java/com/vitorpamplona/quartz/encoders/LnWithdrawalUtil.kt | 2 +- quartz/src/main/java/com/vitorpamplona/quartz/encoders/Lud06.kt | 2 +- quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19.kt | 2 +- quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip54.kt | 2 +- quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip92.kt | 2 +- quartz/src/main/java/com/vitorpamplona/quartz/encoders/Tlv.kt | 2 +- .../com/vitorpamplona/quartz/events/AdvertisedRelayListEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/AppDefinitionEvent.kt | 2 +- .../com/vitorpamplona/quartz/events/AppRecommendationEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/AudioHeaderEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/AudioTrackEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/BadgeAwardEvent.kt | 2 +- .../com/vitorpamplona/quartz/events/BadgeDefinitionEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/BadgeProfilesEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/BookmarkListEvent.kt | 2 +- .../com/vitorpamplona/quartz/events/CalendarDateSlotEvent.kt | 2 +- .../main/java/com/vitorpamplona/quartz/events/CalendarEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/CalendarRSVPEvent.kt | 2 +- .../com/vitorpamplona/quartz/events/CalendarTimeSlotEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/ChannelCreateEvent.kt | 2 +- .../com/vitorpamplona/quartz/events/ChannelHideMessageEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/ChannelListEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/ChannelMessageEvent.kt | 2 +- .../com/vitorpamplona/quartz/events/ChannelMetadataEvent.kt | 2 +- .../com/vitorpamplona/quartz/events/ChannelMuteUserEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/ClassifiedsEvent.kt | 2 +- .../com/vitorpamplona/quartz/events/CommunityDefinitionEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/CommunityListEvent.kt | 2 +- .../vitorpamplona/quartz/events/CommunityPostApprovalEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/ContactListEvent.kt | 2 +- .../main/java/com/vitorpamplona/quartz/events/DeletionEvent.kt | 2 +- .../main/java/com/vitorpamplona/quartz/events/EmojiPackEvent.kt | 2 +- .../com/vitorpamplona/quartz/events/EmojiPackSelectionEvent.kt | 2 +- quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt | 2 +- .../main/java/com/vitorpamplona/quartz/events/EventFactory.kt | 2 +- .../main/java/com/vitorpamplona/quartz/events/EventInterface.kt | 2 +- .../java/com/vitorpamplona/quartz/events/FileHeaderEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/FileServersEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/FileStorageEvent.kt | 2 +- .../com/vitorpamplona/quartz/events/FileStorageHeaderEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/GeneralListEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/GenericRepostEvent.kt | 2 +- .../main/java/com/vitorpamplona/quartz/events/GiftWrapEvent.kt | 2 +- .../src/main/java/com/vitorpamplona/quartz/events/GoalEvent.kt | 2 +- .../com/vitorpamplona/quartz/events/HTTPAuthorizationEvent.kt | 2 +- .../main/java/com/vitorpamplona/quartz/events/HighlightEvent.kt | 2 +- .../quartz/events/LiveActivitiesChatMessageEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/LiveActivitiesEvent.kt | 2 +- .../src/main/java/com/vitorpamplona/quartz/events/LnZapEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/LnZapEventInterface.kt | 2 +- .../com/vitorpamplona/quartz/events/LnZapPaymentRequestEvent.kt | 2 +- .../vitorpamplona/quartz/events/LnZapPaymentResponseEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/LnZapPrivateEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/LnZapRequestEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/LongTextNoteEvent.kt | 2 +- .../main/java/com/vitorpamplona/quartz/events/MetadataEvent.kt | 2 +- .../main/java/com/vitorpamplona/quartz/events/MuteListEvent.kt | 2 +- .../main/java/com/vitorpamplona/quartz/events/NIP24Factory.kt | 2 +- .../src/main/java/com/vitorpamplona/quartz/events/NNSEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/PeopleListEvent.kt | 2 +- .../main/java/com/vitorpamplona/quartz/events/PinListEvent.kt | 2 +- .../main/java/com/vitorpamplona/quartz/events/PollNoteEvent.kt | 2 +- .../main/java/com/vitorpamplona/quartz/events/PrivateDmEvent.kt | 2 +- .../main/java/com/vitorpamplona/quartz/events/ReactionEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/RecommendRelayEvent.kt | 2 +- .../main/java/com/vitorpamplona/quartz/events/RelayAuthEvent.kt | 2 +- .../main/java/com/vitorpamplona/quartz/events/RelaySetEvent.kt | 2 +- .../main/java/com/vitorpamplona/quartz/events/ReportEvent.kt | 2 +- .../main/java/com/vitorpamplona/quartz/events/RepostEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/SealedGossipEvent.kt | 2 +- .../main/java/com/vitorpamplona/quartz/events/StatusEvent.kt | 2 +- .../main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt | 2 +- .../src/main/java/com/vitorpamplona/quartz/events/VideoEvent.kt | 2 +- .../com/vitorpamplona/quartz/events/VideoHorizontalEvent.kt | 2 +- .../java/com/vitorpamplona/quartz/events/VideoVerticalEvent.kt | 2 +- .../main/java/com/vitorpamplona/quartz/events/VideoViewEvent.kt | 2 +- .../com/vitorpamplona/quartz/signers/ExternalSignerLauncher.kt | 2 +- .../main/java/com/vitorpamplona/quartz/signers/NostrSigner.kt | 2 +- .../com/vitorpamplona/quartz/signers/NostrSignerExternal.kt | 2 +- .../com/vitorpamplona/quartz/signers/NostrSignerInternal.kt | 2 +- quartz/src/main/java/com/vitorpamplona/quartz/utils/Robohash.kt | 2 +- .../src/main/java/com/vitorpamplona/quartz/utils/StringUtils.kt | 2 +- .../src/main/java/com/vitorpamplona/quartz/utils/TimeUtils.kt | 2 +- .../test/java/com/vitorpamplona/quartz/encoders/Lud06Test.kt | 2 +- .../java/com/vitorpamplona/quartz/encoders/NIP19ParserTest.kt | 2 +- .../test/java/com/vitorpamplona/quartz/encoders/Nip19Test.kt | 2 +- .../java/com/vitorpamplona/quartz/encoders/TlvIntegerTest.kt | 2 +- spotless/copyright.kt | 2 +- 426 files changed, 426 insertions(+), 426 deletions(-) diff --git a/app/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt b/app/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt index d9d10c024..c87fb572a 100644 --- a/app/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt +++ b/app/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/androidTest/java/com/vitorpamplona/amethyst/RichTextParserTest.kt b/app/src/androidTest/java/com/vitorpamplona/amethyst/RichTextParserTest.kt index 1634bfb96..b7ae07036 100644 --- a/app/src/androidTest/java/com/vitorpamplona/amethyst/RichTextParserTest.kt +++ b/app/src/androidTest/java/com/vitorpamplona/amethyst/RichTextParserTest.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/androidTest/java/com/vitorpamplona/amethyst/ThreadAssemblerTest.kt b/app/src/androidTest/java/com/vitorpamplona/amethyst/ThreadAssemblerTest.kt index 9c878d65e..39cf69711 100644 --- a/app/src/androidTest/java/com/vitorpamplona/amethyst/ThreadAssemblerTest.kt +++ b/app/src/androidTest/java/com/vitorpamplona/amethyst/ThreadAssemblerTest.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/androidTest/java/com/vitorpamplona/amethyst/UrlUserTagTransformationTest.kt b/app/src/androidTest/java/com/vitorpamplona/amethyst/UrlUserTagTransformationTest.kt index 3fa4da42a..92f0af91b 100644 --- a/app/src/androidTest/java/com/vitorpamplona/amethyst/UrlUserTagTransformationTest.kt +++ b/app/src/androidTest/java/com/vitorpamplona/amethyst/UrlUserTagTransformationTest.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/androidTestPlay/java/com/vitorpamplona/amethyst/TranslationsTest.kt b/app/src/androidTestPlay/java/com/vitorpamplona/amethyst/TranslationsTest.kt index ae00af4db..3d22f9ae0 100644 --- a/app/src/androidTestPlay/java/com/vitorpamplona/amethyst/TranslationsTest.kt +++ b/app/src/androidTestPlay/java/com/vitorpamplona/amethyst/TranslationsTest.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/lang/LanguageTranslatorService.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/lang/LanguageTranslatorService.kt index 8c12f0172..3b9ddaebe 100644 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/lang/LanguageTranslatorService.kt +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/lang/LanguageTranslatorService.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt index f08af58f9..d7a28413f 100644 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt index a10656414..b6313df50 100644 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt index 7405779aa..0b8f85f93 100644 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt index 6fba6039e..c008764ca 100644 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt index 7d211335a..fe225dcaf 100644 --- a/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt +++ b/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/androidx/compose/material3/pullrefresh/PullRefresh.kt b/app/src/main/java/androidx/compose/material3/pullrefresh/PullRefresh.kt index c308bab1a..3e750ff23 100644 --- a/app/src/main/java/androidx/compose/material3/pullrefresh/PullRefresh.kt +++ b/app/src/main/java/androidx/compose/material3/pullrefresh/PullRefresh.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/androidx/compose/material3/pullrefresh/PullRefreshIndicator.kt b/app/src/main/java/androidx/compose/material3/pullrefresh/PullRefreshIndicator.kt index 8283d1735..5402915b8 100644 --- a/app/src/main/java/androidx/compose/material3/pullrefresh/PullRefreshIndicator.kt +++ b/app/src/main/java/androidx/compose/material3/pullrefresh/PullRefreshIndicator.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/androidx/compose/material3/pullrefresh/PullRefreshIndicatorTransform.kt b/app/src/main/java/androidx/compose/material3/pullrefresh/PullRefreshIndicatorTransform.kt index 7bf737ad9..68bfcd489 100644 --- a/app/src/main/java/androidx/compose/material3/pullrefresh/PullRefreshIndicatorTransform.kt +++ b/app/src/main/java/androidx/compose/material3/pullrefresh/PullRefreshIndicatorTransform.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/androidx/compose/material3/pullrefresh/PullRefreshState.kt b/app/src/main/java/androidx/compose/material3/pullrefresh/PullRefreshState.kt index cfadc039f..42a6a90f4 100644 --- a/app/src/main/java/androidx/compose/material3/pullrefresh/PullRefreshState.kt +++ b/app/src/main/java/androidx/compose/material3/pullrefresh/PullRefreshState.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt b/app/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt index 74927d4a1..f5e7a28eb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/EncryptedStorage.kt b/app/src/main/java/com/vitorpamplona/amethyst/EncryptedStorage.kt index 130658ecf..0768bfec1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/EncryptedStorage.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/EncryptedStorage.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 178c1a514..ab2498580 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt index 4cf478185..d86cd26bb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 8ef1ab90c..bc830d6ce 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt index 31dcb1924..3bf6e1060 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt index 1d211d28b..8a5e53afb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Chatroom.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Chatroom.kt index a331fece9..7a22e1e24 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Chatroom.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Chatroom.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt index c40b37d43..f3da2c1f1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 499dd5ef4..36ae81022 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Nip47URI.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Nip47URI.kt index e76c2fffb..6ed687695 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Nip47URI.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Nip47URI.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 560deafa2..29a0cc61f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/ParticipantListBuilder.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/ParticipantListBuilder.kt index fd4a63b9a..db99be861 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/ParticipantListBuilder.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/ParticipantListBuilder.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/RelayInformation.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/RelayInformation.kt index a07b807f5..f32bb4fb6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/RelayInformation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/RelayInformation.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/RelaySetupInfo.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/RelaySetupInfo.kt index 1aa95119c..d0b945791 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/RelaySetupInfo.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/RelaySetupInfo.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Settings.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Settings.kt index e1f0881ff..3da6deae5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Settings.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Settings.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt index a24c2cfbe..2dfb7d9cd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt index f55e0ec12..09edd78e0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt index 23f68e951..7a60c6d04 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/BlurHashDecoder.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/BlurHashDecoder.kt index 7ab7f8735..c39688628 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/BlurHashDecoder.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/BlurHashDecoder.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/BlurHashImage.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/BlurHashImage.kt index 94025bee9..fd18a2415 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/BlurHashImage.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/BlurHashImage.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt index 490cffb53..07c34eaf5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt index ced0fa315..ecd1918f7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/EmojiUtils.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/EmojiUtils.kt index 5ee9de38b..8f1e1ab60 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/EmojiUtils.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/EmojiUtils.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/FileHeader.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/FileHeader.kt index de73fc0f5..cdb33e6d6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/FileHeader.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/FileHeader.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/HttpClientManager.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/HttpClientManager.kt index c89b40399..ea504dae2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/HttpClientManager.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/HttpClientManager.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/LocationUtil.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/LocationUtil.kt index 67ce8bc03..cd741c52a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/LocationUtil.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/LocationUtil.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/MainThreadChecker.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/MainThreadChecker.kt index 67c78f792..8d027dbdb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/MainThreadChecker.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/MainThreadChecker.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt index 4ba8c34ac..674e8fa0f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt index db98738df..62ae49cb4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip30CustomEmoji.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip30CustomEmoji.kt index 38d75338d..bbe1903b2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip30CustomEmoji.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip30CustomEmoji.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip47WalletConnectParser.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip47WalletConnectParser.kt index d7186895d..2569f1534 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip47WalletConnectParser.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip47WalletConnectParser.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip96MediaServers.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip96MediaServers.kt index f45d59b9f..8c5ed317d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip96MediaServers.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip96MediaServers.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip96Uploader.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip96Uploader.kt index e66133d44..d4c51113d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip96Uploader.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip96Uploader.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt index f9fd7daba..cedf0bd4f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChannelDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChannelDataSource.kt index b72f8ecd6..8b820d284 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChannelDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChannelDataSource.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt index 0adc07472..b6680fc9b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt index f567be888..386d78b6f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrCommunityDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrCommunityDataSource.kt index 955ee622f..a4e0aacec 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrCommunityDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrCommunityDataSource.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt index 74eb9957c..47cb0af5d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt index 07a795216..2a94fd7aa 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGeohashDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGeohashDataSource.kt index 2daaef580..bba1e8c59 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGeohashDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGeohashDataSource.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHashtagDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHashtagDataSource.kt index fa3f87f36..49f245d6a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHashtagDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHashtagDataSource.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt index c3a0254be..402227cb0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrLnZapPaymentResponseDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrLnZapPaymentResponseDataSource.kt index adb340c95..0cd136ccb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrLnZapPaymentResponseDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrLnZapPaymentResponseDataSource.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt index 64804f126..3d5ac80f5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleChannelDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleChannelDataSource.kt index 364460ab9..beacb0564 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleChannelDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleChannelDataSource.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt index 3b4239a6d..08f8001f4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt index cf7adbb73..472be2ace 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrThreadDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrThreadDataSource.kt index b7d71fa81..4d6d730a1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrThreadDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrThreadDataSource.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt index 5f0631cde..2104df183 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrVideoDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrVideoDataSource.kt index 393c81fd7..8d810907f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrVideoDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrVideoDataSource.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt index cc6e4ed71..fd508b905 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/PackageUtils.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/PackageUtils.kt index 75e17676b..07a978d2e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/PackageUtils.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/PackageUtils.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt index ea1d1f096..06de8548b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt index fb471d86d..48c0d4acc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/zaps/UserZaps.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/zaps/UserZaps.kt index 8ba7f66cc..fcf530c0d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/zaps/UserZaps.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/zaps/UserZaps.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt index edaa7ff4c..5dddfe075 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationUtils.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationUtils.kt index a0f8c4b36..834c04052 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationUtils.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationUtils.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt index 8d0bac6ee..2049d817d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/playback/MultiPlayerPlaybackManager.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/playback/MultiPlayerPlaybackManager.kt index 83daaecd2..8a9ef89bb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/playback/MultiPlayerPlaybackManager.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/playback/MultiPlayerPlaybackManager.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/playback/PlaybackClientController.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/playback/PlaybackClientController.kt index 13616eeac..d2c9b7bec 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/playback/PlaybackClientController.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/playback/PlaybackClientController.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/playback/PlaybackService.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/playback/PlaybackService.kt index ab9f276f9..5ea55e77d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/playback/PlaybackService.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/playback/PlaybackService.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/playback/VideoCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/playback/VideoCache.kt index 5f2bf1083..c4db43820 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/playback/VideoCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/playback/VideoCache.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/playback/VideoViewedPositionCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/playback/VideoViewedPositionCache.kt index df600ec98..e0867cd1c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/playback/VideoViewedPositionCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/playback/VideoViewedPositionCache.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/previews/BahaUrlPreview.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/previews/BahaUrlPreview.kt index e794100cb..24685a097 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/previews/BahaUrlPreview.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/previews/BahaUrlPreview.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/previews/IUrlPreviewCallback.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/previews/IUrlPreviewCallback.kt index db5e6f2ba..6bfdacd8d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/previews/IUrlPreviewCallback.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/previews/IUrlPreviewCallback.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlInfoItem.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlInfoItem.kt index 3135f2285..5f5fb186d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlInfoItem.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlInfoItem.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreviewUtils.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreviewUtils.kt index 14e36c2d8..ab0e1f9b4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreviewUtils.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreviewUtils.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt index e16a07c30..3f0283fc7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt index 7c1cb3285..3cf37d874 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt index 964bf777c..a99a83878 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/JsonFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/JsonFilter.kt index f2e6d6a43..dc7d66d9b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/JsonFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/JsonFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt index 5a8e02523..cdb428eae 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt index be90cb9ba..89fe955be 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Subscription.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Subscription.kt index 0c7ac6535..eefe41b99 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Subscription.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Subscription.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/TypedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/TypedFilter.kt index a7b42588e..84ef11d26 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/TypedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/TypedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/tts/TextToSpeechEngine.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/tts/TextToSpeechEngine.kt index ffa864fa4..5c920ed8b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/tts/TextToSpeechEngine.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/tts/TextToSpeechEngine.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/tts/TextToSpeechHelper.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/tts/TextToSpeechHelper.kt index c6922198d..26de2fed8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/tts/TextToSpeechHelper.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/tts/TextToSpeechHelper.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 9ffec6bae..92737e29b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageDownloader.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageDownloader.kt index 674108bf4..6fa169566 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageDownloader.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageDownloader.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageSaver.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageSaver.kt index 11a989a60..997a94608 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageSaver.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageSaver.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/InformationDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/InformationDialog.kt index 4d6c87adb..f2cc82d6c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/InformationDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/InformationDialog.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/JoinUserOrChannelView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/JoinUserOrChannelView.kt index a88464bf9..8406c09bd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/JoinUserOrChannelView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/JoinUserOrChannelView.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelView.kt index 104e0ca66..15f5cf8f4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelView.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelViewModel.kt index 29fa75b87..25b42c4fe 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelViewModel.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt index 703938397..5f89c0d01 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt index 847131e4f..dcb217e38 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt index 86f531645..2200c9b28 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollClosing.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollClosing.kt index 7f6145daf..b17ade6d8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollClosing.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollClosing.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollConsensusThreshold.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollConsensusThreshold.kt index 51c1f65c8..d181c873d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollConsensusThreshold.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollConsensusThreshold.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollOption.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollOption.kt index e078a506f..6f8378cc0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollOption.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollOption.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollPrimaryDescription.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollPrimaryDescription.kt index b969ba8bc..4c355c6b2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollPrimaryDescription.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollPrimaryDescription.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollRecipientsField.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollRecipientsField.kt index a5c02bdcf..10cb12bc3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollRecipientsField.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollRecipientsField.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollVoteValueRange.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollVoteValueRange.kt index 8e6b85fe7..806eee50b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollVoteValueRange.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollVoteValueRange.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index 733b11233..392f17741 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index e137d1cfd..6dc44868d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt index 53449fc1c..731ced4ca 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListViewModel.kt index 7718c72ca..eabe2b51a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListViewModel.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataView.kt index 8ff81dfdc..dd0446d20 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataView.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt index 86695c9b6..d0af1b125 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NotifyRequestDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NotifyRequestDialog.kt index d5ff4f92e..c19d7531c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NotifyRequestDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NotifyRequestDialog.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelayInformationDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelayInformationDialog.kt index 96f45a127..a80bd30bf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelayInformationDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelayInformationDialog.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt index a82544c26..1435daa20 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/SaveToGallery.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/SaveToGallery.kt index ad2ad7830..10e8ec0db 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/SaveToGallery.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/SaveToGallery.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/UploadFromGallery.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/UploadFromGallery.kt index 89a9e98be..0384ca652 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/UploadFromGallery.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/UploadFromGallery.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagTransformation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagTransformation.kt index d44f1f6a2..de8140fbe 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagTransformation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagTransformation.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/ChannelFabColumn.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/ChannelFabColumn.kt index 38025fe1b..d2544a659 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/ChannelFabColumn.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/ChannelFabColumn.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewChannelButton.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewChannelButton.kt index 0b92bd7fa..2ca79c684 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewChannelButton.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewChannelButton.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewCommunityNoteButton.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewCommunityNoteButton.kt index 13fd9bd13..63f132eeb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewCommunityNoteButton.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewCommunityNoteButton.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewImageButton.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewImageButton.kt index 0c9c19fde..16973d1c3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewImageButton.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewImageButton.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewNoteButton.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewNoteButton.kt index 0c0a17cc5..a0e8fe440 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewNoteButton.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewNoteButton.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/AudioWaveformReadOnly.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/AudioWaveformReadOnly.kt index 9ad036ff6..7002a976e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/AudioWaveformReadOnly.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/AudioWaveformReadOnly.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt index eec719159..be77cf5f6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt index b318a4a3f..c77dd6782 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableEmail.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableEmail.kt index 841e35864..2ad3d1f64 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableEmail.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableEmail.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableNoteTag.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableNoteTag.kt index 92ee3fea5..93e279e1c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableNoteTag.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableNoteTag.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickablePhone.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickablePhone.kt index e52aa1c3d..990ee9137 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickablePhone.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickablePhone.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt index 9bcfbe5bd..93fc8ee20 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableUrl.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableUrl.kt index 30f6f5b8b..8089226d5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableUrl.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableUrl.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableUserTag.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableUserTag.kt index 38db30210..aa40a20a6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableUserTag.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableUserTag.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableWithdrawal.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableWithdrawal.kt index 9e59b88ba..c9c2b3250 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableWithdrawal.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableWithdrawal.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt index 81d234ae4..5bcc7b6c0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/GenericLoadable.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/GenericLoadable.kt index 4e9a06161..6817b873f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/GenericLoadable.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/GenericLoadable.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt index 5ed03955b..497f70f51 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt index 786aa0cd8..12a91502a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt index dc8b0b5ab..82441101c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/MarkdownParser.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/MarkdownParser.kt index e3518a55b..a45aa6c81 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/MarkdownParser.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/MarkdownParser.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/MediaCompressor.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/MediaCompressor.kt index 176e5c14e..bbafab0de 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/MediaCompressor.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/MediaCompressor.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 439f6a021..8efebac48 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/Robohash.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/Robohash.kt index 4cca27d38..ca8680946 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/Robohash.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/Robohash.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt index cae0ea2aa..031fbf160 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SelectTextDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SelectTextDialog.kt index e7bf72a08..afda98789 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SelectTextDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SelectTextDialog.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt index 770a681bc..fd19d837d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SlidingCarousel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SlidingCarousel.kt index 41d19c1af..047bb7bec 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SlidingCarousel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SlidingCarousel.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SplitItem.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SplitItem.kt index 569e0c6b7..389cb358a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SplitItem.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SplitItem.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt index d95aab102..8c2141b05 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TranslationConfig.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TranslationConfig.kt index e06228df8..379b171a3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TranslationConfig.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TranslationConfig.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewCard.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewCard.kt index 983613ee6..4eb4e5681 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewCard.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewCard.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewState.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewState.kt index a37b3e116..beef6d771 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewState.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewState.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt index 530c95c08..40001604b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZapRaiserRequest.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZapRaiserRequest.kt index c2b5c5114..428e1086f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZapRaiserRequest.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZapRaiserRequest.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index 504136bbb..c2c01f147 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPrivateFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPrivateFeedFilter.kt index 0af660d01..f9f4de701 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPrivateFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPrivateFeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPublicFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPublicFeedFilter.kt index d6ea8cad1..351a7347d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPublicFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPublicFeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChannelFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChannelFeedFilter.kt index 6a5ad0238..0e69e94b5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChannelFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChannelFeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt index 6776068f9..112c59f0f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt index 72a58bfb6..0ce003ce4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListNewFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListNewFeedFilter.kt index a09f841fa..2e20f0483 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListNewFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListNewFeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/CommunityFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/CommunityFeedFilter.kt index 9cfa7bf8c..5a6cc7288 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/CommunityFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/CommunityFeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt index 5c902730b..2d4d7302d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverCommunityFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverCommunityFeedFilter.kt index ff6065c8c..e4e32df9b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverCommunityFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverCommunityFeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt index 825878aa2..0b8fc858c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveNowFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveNowFeedFilter.kt index ebbcc996d..7cb58d8b9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveNowFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveNowFeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverMarketplaceFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverMarketplaceFeedFilter.kt index 267a07e97..1f59fd1b9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverMarketplaceFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverMarketplaceFeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt index a247c4f42..0bb8ca70a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GeoHashFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GeoHashFeedFilter.kt index 479242948..b902abbb6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GeoHashFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GeoHashFeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt index f0be67cb9..b51d4c2a4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt index 7843c396c..58fedc1d0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt index 6cc46044e..178b970f7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt index fe83e7364..07cc506ed 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt index 451cf33de..da4c09467 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ThreadFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ThreadFeedFilter.kt index ff5a663ab..a4655eed7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ThreadFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ThreadFeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileAppRecommendationsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileAppRecommendationsFeedFilter.kt index 2bf886287..44c93d7a5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileAppRecommendationsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileAppRecommendationsFeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileBookmarksFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileBookmarksFeedFilter.kt index 8004bc44a..075d8a4be 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileBookmarksFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileBookmarksFeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileConversationsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileConversationsFeedFilter.kt index bea08fe34..1a4171d1c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileConversationsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileConversationsFeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileFollowersFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileFollowersFeedFilter.kt index 72e8b3994..3c7a11e79 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileFollowersFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileFollowersFeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileFollowsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileFollowsFeedFilter.kt index 2d6472ace..9ec0cad68 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileFollowsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileFollowsFeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNewThreadFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNewThreadFeedFilter.kt index 1ff82dff1..04dac8a9d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNewThreadFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNewThreadFeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileReportsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileReportsFeedFilter.kt index ee703b87e..96b718549 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileReportsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileReportsFeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileZapsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileZapsFeedFilter.kt index cbc459f6a..075f03f2d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileZapsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileZapsFeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt index 321c3417f..721cb736e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/AddRemoveButtons.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/AddRemoveButtons.kt index 6618f95ad..e656751b1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/AddRemoveButtons.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/AddRemoveButtons.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayCommunity.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayCommunity.kt index 3db228e20..0685e1090 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayCommunity.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayCommunity.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayHashtags.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayHashtags.kt index a434db446..cd9f71574 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayHashtags.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayHashtags.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayPoW.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayPoW.kt index 5705eb2e7..184ebe35c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayPoW.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayPoW.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayReward.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayReward.kt index f8258f7df..ae6e5409a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayReward.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayReward.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayUncitedHashtags.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayUncitedHashtags.kt index c102b88b6..e185ab24f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayUncitedHashtags.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayUncitedHashtags.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayZapSplits.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayZapSplits.kt index 68d34b904..6fab24206 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayZapSplits.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayZapSplits.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayout.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayout.kt index b7c1280c8..d40a92afe 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayout.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayout.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/layouts/LeftPictureLayout.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/layouts/LeftPictureLayout.kt index 31477f48c..9df366786 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/layouts/LeftPictureLayout.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/layouts/LeftPictureLayout.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/layouts/RepostLayout.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/layouts/RepostLayout.kt index c0e3abd19..4539b0d8f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/layouts/RepostLayout.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/layouts/RepostLayout.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt index 82d50cf33..e5ad82775 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt index 8bae02284..bfc43e73a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 3d7a696fc..69a478645 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt index f48443400..3e314bfbc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index 457be36d0..4471144f1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt index bddb662c0..e27c3d5be 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index 03de452d3..ecdc18bf2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt index 01b80009c..007f5ea66 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt index dd0ea16ee..3511ff2fd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt index a12e5a2e6..e1c59eede 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomHeaderCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomHeaderCompose.kt index 2a8689102..689e10414 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomHeaderCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomHeaderCompose.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index 0951de209..626c5fc12 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt index 465551a07..1e0616bd3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt index 08deac29f..3e75719d9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index f1848eab4..31f905e94 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt index 78562ba91..913cc66f4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 7abbabe58..297789c98 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt index 8cb89cf64..3f4d2d513 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt index bd604d56f..e40d7ac40 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt index fa9fceae8..965dc95b3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PubKeyFormatter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PubKeyFormatter.kt index 98e25dc39..1ef039515 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PubKeyFormatter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PubKeyFormatter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index e95c26965..6750bf8e6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt index ec9d50417..55a1a16e7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt index 184a44334..71c661083 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt index 3a5b95c8e..a3366c503 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt index f3c095cea..2be4a0770 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/TimeAgoFormatter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/TimeAgoFormatter.kt index a415a0768..aa381ac24 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/TimeAgoFormatter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/TimeAgoFormatter.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt index cf07e8e91..4e098e6ca 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt index 2d5a0fd78..bf30555c4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt index 0622fadd0..d808056d3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index 04a8f29d1..a645395c9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt index dc8c30ca5..e0f79881f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt index 48b7d5667..3c2e42730 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt index a603689a5..77581d245 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt index 21ef9ac84..91d114954 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt index 47ee1ccd0..9f93e498e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeDrawer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeDrawer.kt index a5f992666..3ede38d98 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeDrawer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeDrawer.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeScanner.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeScanner.kt index abca33706..bd787ca93 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeScanner.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeScanner.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/ShowQRDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/ShowQRDialog.kt index ae4e6ac1c..5c4f33f0f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/ShowQRDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/ShowQRDialog.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt index 29134685d..973d531ef 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountState.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountState.kt index 3d4e3d591..c92cc8ed6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountState.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountState.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index 4ceb37bc2..b16e74941 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt index f23a78a73..a492d570a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt index 6232e04e4..a68e3a719 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt index 3bc23190d..d80b7f326 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt index bca0cb9af..bf5720b83 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt index 5c1309b32..ffa1a6f7e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedState.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedState.kt index bae5ad4e2..ea9d5ac64 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedState.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedState.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt index c1e25758e..f1064990c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index c7d6340b6..d2610f923 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedState.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedState.kt index 1002208f8..387dc1f9b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedState.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedState.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedView.kt index a245e3829..5c0c70736 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedView.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedViewModel.kt index 1ad83429d..1465d73ea 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedViewModel.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt index d3d8c1c6b..5203b1b8b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt index 969426642..b2d928fcc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedPreferencesViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedPreferencesViewModel.kt index 0037aa303..29ce5e5d4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedPreferencesViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedPreferencesViewModel.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/StringFeedState.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/StringFeedState.kt index 40914cba6..14aec737f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/StringFeedState.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/StringFeedState.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/StringFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/StringFeedView.kt index 526272cda..f4c355fdc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/StringFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/StringFeedView.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/StringFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/StringFeedViewModel.kt index 6425aad40..7db649be8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/StringFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/StringFeedViewModel.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index 91e90d159..ae1de817b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedState.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedState.kt index 6b687e67e..2feffee92 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedState.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedState.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt index b82da0e36..6fc453d92 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt index 352674e31..4804b3cb9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt index fbed6de3d..5faaeb570 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index adbed38b5..58b6fd3fb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BookmarkListScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BookmarkListScreen.kt index 9e1244322..60f286ea7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BookmarkListScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BookmarkListScreen.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index 8d2a49c30..ed4fb042b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt index 2c29bcc8d..94d4fa306 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt index 2606d3a52..41df0fd51 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/CommunityScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/CommunityScreen.kt index dbc623aa2..9a5cb0b12 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/CommunityScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/CommunityScreen.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ConnectOrbotDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ConnectOrbotDialog.kt index 9fe64bdf7..5815c9fc7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ConnectOrbotDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ConnectOrbotDialog.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt index c9d74cf9a..e42c2fe6c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt index 3ba380791..33b0dc61c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt index 5caaa7b69..de08249ff 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt index 06364986e..1381ee22b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt index 87155161d..7773764f6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt index d7d3c52f9..51143cfc3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoadRedirectScreen.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt index cbe5114e6..44c613ee2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt index 6a741006e..99269225e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index 6899e929b..217eac68b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ReportNoteDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ReportNoteDialog.kt index 41753ac08..69c921b61 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ReportNoteDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ReportNoteDialog.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt index 8a0df8ac4..57cc6bbca 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SettingsScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SettingsScreen.kt index e103be0f4..fbc8e0db7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SettingsScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SettingsScreen.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ThreadScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ThreadScreen.kt index fc0eac715..78219e9b0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ThreadScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ThreadScreen.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt index b1951014d..0ac705db3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginOrSignupScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginOrSignupScreen.kt index 874eb9337..9957886c6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginOrSignupScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginOrSignupScreen.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt index bd3d29274..57f32043f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/SignUpScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/SignUpScreen.kt index e4b03e6d8..a12965b80 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/SignUpScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/SignUpScreen.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt index 7e0d21bb9..44eef1170 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index 1f8ba22ff..0d10f938d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt index 288893215..fc2512b9b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Type.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Type.kt index 482985f5c..f30ebf75d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Type.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Type.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/play/java/com/vitorpamplona/amethyst/service/lang/LanguageTranslatorService.kt b/app/src/play/java/com/vitorpamplona/amethyst/service/lang/LanguageTranslatorService.kt index 3c020b888..f2921c4be 100644 --- a/app/src/play/java/com/vitorpamplona/amethyst/service/lang/LanguageTranslatorService.kt +++ b/app/src/play/java/com/vitorpamplona/amethyst/service/lang/LanguageTranslatorService.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationReceiverService.kt b/app/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationReceiverService.kt index a3dd0067f..3b28a3843 100644 --- a/app/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationReceiverService.kt +++ b/app/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationReceiverService.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt b/app/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt index 03721e0b0..caccf4374 100644 --- a/app/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt +++ b/app/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/play/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt b/app/src/play/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt index 74d0ae0bd..10a9ad259 100644 --- a/app/src/play/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt +++ b/app/src/play/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt b/app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt index cc2e2c5b2..fb87fc20b 100644 --- a/app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt +++ b/app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/test/java/com/vitorpamplona/amethyst/CharsetTest.kt b/app/src/test/java/com/vitorpamplona/amethyst/CharsetTest.kt index 311ddb9f3..e06bdc333 100644 --- a/app/src/test/java/com/vitorpamplona/amethyst/CharsetTest.kt +++ b/app/src/test/java/com/vitorpamplona/amethyst/CharsetTest.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt b/app/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt index e3fd477e9..030f7bd36 100644 --- a/app/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt +++ b/app/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/test/java/com/vitorpamplona/amethyst/SplitterTest.kt b/app/src/test/java/com/vitorpamplona/amethyst/SplitterTest.kt index 7912d7703..839fed1e7 100644 --- a/app/src/test/java/com/vitorpamplona/amethyst/SplitterTest.kt +++ b/app/src/test/java/com/vitorpamplona/amethyst/SplitterTest.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/test/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifierTest.kt b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifierTest.kt index 1e2abe7ed..a8ee63a46 100644 --- a/app/src/test/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifierTest.kt +++ b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifierTest.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/test/java/com/vitorpamplona/amethyst/service/Nip30Test.kt b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip30Test.kt index 071119be9..8f84a7daf 100644 --- a/app/src/test/java/com/vitorpamplona/amethyst/service/Nip30Test.kt +++ b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip30Test.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/test/java/com/vitorpamplona/amethyst/service/Nip96Test.kt b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip96Test.kt index 307294ebf..44a350500 100644 --- a/app/src/test/java/com/vitorpamplona/amethyst/service/Nip96Test.kt +++ b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip96Test.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/app/src/test/java/com/vitorpamplona/amethyst/service/zaps/UserZapsTest.kt b/app/src/test/java/com/vitorpamplona/amethyst/service/zaps/UserZapsTest.kt index e2e2cebe2..7d4475efd 100644 --- a/app/src/test/java/com/vitorpamplona/amethyst/service/zaps/UserZapsTest.kt +++ b/app/src/test/java/com/vitorpamplona/amethyst/service/zaps/UserZapsTest.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/ExpandableViewComputationBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/ExpandableViewComputationBenchmark.kt index dc97a4449..7a9918a14 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/ExpandableViewComputationBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/ExpandableViewComputationBenchmark.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/BechBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/BechBenchmark.kt index fa63a5afa..879200c91 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/BechBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/BechBenchmark.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/ContainsBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/ContainsBenchmark.kt index 151e9e421..112132d3b 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/ContainsBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/ContainsBenchmark.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CryptoBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CryptoBenchmark.kt index 4f484dbed..67332f084 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CryptoBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CryptoBenchmark.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventBenchmark.kt index 75140de66..60ad22c9c 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventBenchmark.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapBenchmark.kt index ab8791e8c..53848c4b5 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapBenchmark.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapReceivingBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapReceivingBenchmark.kt index ec38a52dd..54ec15353 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapReceivingBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapReceivingBenchmark.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapSigningBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapSigningBenchmark.kt index 5d352c10d..e2e76db2f 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapSigningBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapSigningBenchmark.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt index 1c6d904e9..7a8e0331c 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/RobohashBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/RobohashBenchmark.kt index d5682a81a..79fe0f293 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/RobohashBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/RobohashBenchmark.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/ExpandableTextParser.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/ExpandableTextParser.kt index 601993357..613b27bd4 100644 --- a/commons/src/main/java/com/vitorpamplona/amethyst/commons/ExpandableTextParser.kt +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/ExpandableTextParser.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/ChatroomKeyTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/ChatroomKeyTest.kt index 6b9506621..2e3758c28 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/ChatroomKeyTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/ChatroomKeyTest.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/CitationTests.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/CitationTests.kt index 7fff34d93..2a080e954 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/CitationTests.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/CitationTests.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/CryptoUtilsTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/CryptoUtilsTest.kt index b7e6f3132..c0ea6f82a 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/CryptoUtilsTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/CryptoUtilsTest.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/EventSigCheck.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/EventSigCheck.kt index 338f08bf3..74dbdfc54 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/EventSigCheck.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/EventSigCheck.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/GiftWrapEventTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/GiftWrapEventTest.kt index 27712a34c..3639dd281 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/GiftWrapEventTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/GiftWrapEventTest.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/HexEncodingTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/HexEncodingTest.kt index 9c20dba68..2bb95b09a 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/HexEncodingTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/HexEncodingTest.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/LargeDBSignatureCheck.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/LargeDBSignatureCheck.kt index ad670a54a..f558bd606 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/LargeDBSignatureCheck.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/LargeDBSignatureCheck.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/LnInvoiceUtilTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/LnInvoiceUtilTest.kt index 0c7e0d0ac..9c570927e 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/LnInvoiceUtilTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/LnInvoiceUtilTest.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP44v2Test.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP44v2Test.kt index 081141154..60740cfce 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP44v2Test.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP44v2Test.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP49Test.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP49Test.kt index 3513020d8..006af2b7b 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP49Test.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP49Test.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/PrivateZapTests.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/PrivateZapTests.kt index f2f7b7ac3..a13c8768d 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/PrivateZapTests.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/PrivateZapTests.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/CryptoUtils.kt b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/CryptoUtils.kt index d0e366d02..2344e2435 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/CryptoUtils.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/CryptoUtils.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Hkdf.kt b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Hkdf.kt index e36f6d9c3..55db69dde 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Hkdf.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Hkdf.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/KeyPair.kt b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/KeyPair.kt index 27aebaee4..d8afcfc3f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/KeyPair.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/KeyPair.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip04.kt b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip04.kt index f02858277..857432663 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip04.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip04.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip44v1.kt b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip44v1.kt index d0c730adb..5872c5d43 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip44v1.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip44v1.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip44v2.kt b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip44v2.kt index 8b7ed939c..88fd5ba96 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip44v2.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip44v2.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip49.kt b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip49.kt index 199965227..f537ef836 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip49.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip49.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/SharedKeyCache.kt b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/SharedKeyCache.kt index dd39dec2a..766841128 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/SharedKeyCache.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/SharedKeyCache.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/SodiumUtils.kt b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/SodiumUtils.kt index 3025b4aea..8d5c748a0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/SodiumUtils.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/SodiumUtils.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/ATag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/ATag.kt index 9b2cec4fa..861706bbe 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/ATag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/ATag.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Bech32Util.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Bech32Util.kt index fa2bd194c..59ca60fa0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Bech32Util.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Bech32Util.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/HexUtils.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/HexUtils.kt index a410453a8..b995bd6ba 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/HexUtils.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/HexUtils.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/LnInvoiceUtil.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/LnInvoiceUtil.kt index 986020854..6fadab056 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/LnInvoiceUtil.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/LnInvoiceUtil.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/LnWithdrawalUtil.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/LnWithdrawalUtil.kt index 0d88d2d41..e56cc00bf 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/LnWithdrawalUtil.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/LnWithdrawalUtil.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Lud06.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Lud06.kt index f19556812..6d58633ed 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Lud06.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Lud06.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19.kt index 03f638351..992735a14 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip54.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip54.kt index 1994cf71a..2522ed757 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip54.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip54.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip92.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip92.kt index cd8191348..04a702a1c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip92.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip92.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Tlv.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Tlv.kt index e99a673bb..3d452da55 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Tlv.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Tlv.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/AdvertisedRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/AdvertisedRelayListEvent.kt index 4510c390f..a2b6e1bda 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/AdvertisedRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/AdvertisedRelayListEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/AppDefinitionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/AppDefinitionEvent.kt index 1843ea7ce..8374c518d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/AppDefinitionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/AppDefinitionEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/AppRecommendationEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/AppRecommendationEvent.kt index 265a30717..6bc49b09d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/AppRecommendationEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/AppRecommendationEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/AudioHeaderEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/AudioHeaderEvent.kt index 27c4b0785..db596d0c0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/AudioHeaderEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/AudioHeaderEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/AudioTrackEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/AudioTrackEvent.kt index 68b087692..3724b2b13 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/AudioTrackEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/AudioTrackEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/BadgeAwardEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/BadgeAwardEvent.kt index 545acc363..a4ebdb3d0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/BadgeAwardEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/BadgeAwardEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/BadgeDefinitionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/BadgeDefinitionEvent.kt index 1ed4596ce..41d76d2d8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/BadgeDefinitionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/BadgeDefinitionEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/BadgeProfilesEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/BadgeProfilesEvent.kt index 10cc2e147..0c235d6b2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/BadgeProfilesEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/BadgeProfilesEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt index 1317d6ae9..005cbfe2b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/BookmarkListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/BookmarkListEvent.kt index 86ed70ebe..ccd2392be 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/BookmarkListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/BookmarkListEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarDateSlotEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarDateSlotEvent.kt index 5df244d29..4adbf7908 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarDateSlotEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarDateSlotEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarEvent.kt index 40eb82a87..68095789c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarRSVPEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarRSVPEvent.kt index 3c1fc2264..fe83c6d5b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarRSVPEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarRSVPEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarTimeSlotEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarTimeSlotEvent.kt index 047fb8b6b..4ecf31c4f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarTimeSlotEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/CalendarTimeSlotEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelCreateEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelCreateEvent.kt index 78b1797f1..0d081e9fb 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelCreateEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelCreateEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelHideMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelHideMessageEvent.kt index ee2f47d07..5611deb0d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelHideMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelHideMessageEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelListEvent.kt index 49e7f1191..1837019ee 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelListEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMessageEvent.kt index 31d690b38..2236ed944 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMessageEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMetadataEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMetadataEvent.kt index 9868b91dc..8b7b9fae3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMetadataEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMetadataEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMuteUserEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMuteUserEvent.kt index a96a5a898..fe9265495 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMuteUserEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMuteUserEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt index d07c40d05..b141833af 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ClassifiedsEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ClassifiedsEvent.kt index 0d477f589..5b8597400 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ClassifiedsEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ClassifiedsEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/CommunityDefinitionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/CommunityDefinitionEvent.kt index 2978b34cf..38a72b2a0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/CommunityDefinitionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/CommunityDefinitionEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/CommunityListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/CommunityListEvent.kt index cb26353b5..21d692a90 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/CommunityListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/CommunityListEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/CommunityPostApprovalEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/CommunityPostApprovalEvent.kt index 36ff66816..430ccbaae 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/CommunityPostApprovalEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/CommunityPostApprovalEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ContactListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ContactListEvent.kt index ece905876..496fe4fd9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ContactListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ContactListEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/DeletionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/DeletionEvent.kt index cb9bd04c5..7892b80a4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/DeletionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/DeletionEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackEvent.kt index 33d92aae4..73a09f013 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackSelectionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackSelectionEvent.kt index b294d6cad..b8b449ba3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackSelectionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackSelectionEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt index 219283c45..7dda67fc6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt index 3e5e3b5dd..b2fd06e2e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt index d8c977288..a847eae0b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/FileHeaderEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/FileHeaderEvent.kt index 1d760fe14..5d7e1863d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/FileHeaderEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/FileHeaderEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/FileServersEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/FileServersEvent.kt index 25aef0b06..a2825b805 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/FileServersEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/FileServersEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/FileStorageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/FileStorageEvent.kt index e0a5aaddc..f5f6df63e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/FileStorageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/FileStorageEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/FileStorageHeaderEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/FileStorageHeaderEvent.kt index 5b8ca0ef4..8ba7649ac 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/FileStorageHeaderEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/FileStorageHeaderEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt index 3ac1f4080..add6767f9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GenericRepostEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GenericRepostEvent.kt index afca0716d..0609f4739 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GenericRepostEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GenericRepostEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GiftWrapEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GiftWrapEvent.kt index f63c0b7b2..f844ee68d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GiftWrapEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GiftWrapEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GoalEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GoalEvent.kt index dc7fbf7cf..1ee839c04 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GoalEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GoalEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/HTTPAuthorizationEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/HTTPAuthorizationEvent.kt index d4c380406..c51470ecd 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/HTTPAuthorizationEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/HTTPAuthorizationEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/HighlightEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/HighlightEvent.kt index 179c1ccb7..66b528e0d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/HighlightEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/HighlightEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesChatMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesChatMessageEvent.kt index d840d57bf..a3711daf9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesChatMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesChatMessageEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesEvent.kt index bd1f0f5a6..131d34c6e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapEvent.kt index 6bff48110..50407d634 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapEventInterface.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapEventInterface.kt index b0df739a7..f7fce1fd2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapEventInterface.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapEventInterface.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPaymentRequestEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPaymentRequestEvent.kt index 63259472a..8b1bde062 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPaymentRequestEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPaymentRequestEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPaymentResponseEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPaymentResponseEvent.kt index 5d260776b..7bb9c1db4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPaymentResponseEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPaymentResponseEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPrivateEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPrivateEvent.kt index 20b7268ba..c9d20d852 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPrivateEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapPrivateEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapRequestEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapRequestEvent.kt index 009ed26e9..f003caa4e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapRequestEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapRequestEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/LongTextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/LongTextNoteEvent.kt index 97fabab49..a64b56fc4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/LongTextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/LongTextNoteEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/MetadataEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/MetadataEvent.kt index cdc3b7f72..a72995ae1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/MetadataEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/MetadataEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/MuteListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/MuteListEvent.kt index 80d4c650e..195819b6b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/MuteListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/MuteListEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP24Factory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP24Factory.kt index a99a3e2d6..5ff107a50 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP24Factory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP24Factory.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/NNSEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/NNSEvent.kt index 17316d851..73223548d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/NNSEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/NNSEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/PeopleListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/PeopleListEvent.kt index ec9bae9bd..8c35d3410 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/PeopleListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/PeopleListEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/PinListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/PinListEvent.kt index 155ef83eb..185df6818 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/PinListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/PinListEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/PollNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/PollNoteEvent.kt index fbf512fdf..cffe30e28 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/PollNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/PollNoteEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/PrivateDmEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/PrivateDmEvent.kt index 5929a6e5d..52e0ca1dd 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/PrivateDmEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/PrivateDmEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ReactionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ReactionEvent.kt index da1ca943a..a3c9b53d6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ReactionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ReactionEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/RecommendRelayEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/RecommendRelayEvent.kt index 63b2c39d2..e217c86a1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/RecommendRelayEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/RecommendRelayEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/RelayAuthEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/RelayAuthEvent.kt index 9aa0060ea..8520d2fbf 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/RelayAuthEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/RelayAuthEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/RelaySetEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/RelaySetEvent.kt index a8da5f7dd..80ec8586c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/RelaySetEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/RelaySetEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ReportEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ReportEvent.kt index 255a1641e..1b827917b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ReportEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ReportEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/RepostEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/RepostEvent.kt index ec12ce9f8..056182829 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/RepostEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/RepostEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/SealedGossipEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/SealedGossipEvent.kt index f3d93b45c..4fd6194ef 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/SealedGossipEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/SealedGossipEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/StatusEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/StatusEvent.kt index 962e8bd91..aa897e286 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/StatusEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/StatusEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt index 91104aaa8..d74a36248 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/VideoEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/VideoEvent.kt index 698d17a42..54a438953 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/VideoEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/VideoEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/VideoHorizontalEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/VideoHorizontalEvent.kt index ff6a5f87f..b9acc1cd7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/VideoHorizontalEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/VideoHorizontalEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/VideoVerticalEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/VideoVerticalEvent.kt index 412c9e542..6787627f8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/VideoVerticalEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/VideoVerticalEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/VideoViewEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/VideoViewEvent.kt index 1a7d0ebdb..2254924a2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/VideoViewEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/VideoViewEvent.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/signers/ExternalSignerLauncher.kt b/quartz/src/main/java/com/vitorpamplona/quartz/signers/ExternalSignerLauncher.kt index 7d5449ddb..951494889 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/signers/ExternalSignerLauncher.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/signers/ExternalSignerLauncher.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSigner.kt b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSigner.kt index cca9980e2..5014db2f2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSigner.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSigner.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerExternal.kt b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerExternal.kt index 6952b8b98..73a55cb8c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerExternal.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerExternal.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerInternal.kt b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerInternal.kt index 3a04392c9..2537287b5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerInternal.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerInternal.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/Robohash.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/Robohash.kt index 28fa3c3e2..d3f9835e7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/Robohash.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/Robohash.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/StringUtils.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/StringUtils.kt index a6f1e4947..5bf3e568a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/StringUtils.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/StringUtils.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/TimeUtils.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/TimeUtils.kt index d5669942b..5216e8ca2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/TimeUtils.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/TimeUtils.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/encoders/Lud06Test.kt b/quartz/src/test/java/com/vitorpamplona/quartz/encoders/Lud06Test.kt index c7fadde3f..541a1d3c7 100644 --- a/quartz/src/test/java/com/vitorpamplona/quartz/encoders/Lud06Test.kt +++ b/quartz/src/test/java/com/vitorpamplona/quartz/encoders/Lud06Test.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/encoders/NIP19ParserTest.kt b/quartz/src/test/java/com/vitorpamplona/quartz/encoders/NIP19ParserTest.kt index 9bb72ac58..3252c0e38 100644 --- a/quartz/src/test/java/com/vitorpamplona/quartz/encoders/NIP19ParserTest.kt +++ b/quartz/src/test/java/com/vitorpamplona/quartz/encoders/NIP19ParserTest.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/encoders/Nip19Test.kt b/quartz/src/test/java/com/vitorpamplona/quartz/encoders/Nip19Test.kt index f807527a6..099b7d42e 100644 --- a/quartz/src/test/java/com/vitorpamplona/quartz/encoders/Nip19Test.kt +++ b/quartz/src/test/java/com/vitorpamplona/quartz/encoders/Nip19Test.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/encoders/TlvIntegerTest.kt b/quartz/src/test/java/com/vitorpamplona/quartz/encoders/TlvIntegerTest.kt index 82e0f6011..db2a99c03 100644 --- a/quartz/src/test/java/com/vitorpamplona/quartz/encoders/TlvIntegerTest.kt +++ b/quartz/src/test/java/com/vitorpamplona/quartz/encoders/TlvIntegerTest.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 diff --git a/spotless/copyright.kt b/spotless/copyright.kt index 01566bc35..f46bcc74d 100644 --- a/spotless/copyright.kt +++ b/spotless/copyright.kt @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Vitor Pamplona + * Copyright (c) 2024 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 From 8034fd09757e389a99ecd05cf2c8236cfcbad10e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 16 Feb 2024 13:53:06 -0500 Subject: [PATCH 093/176] Adds normalizing test for emoji passwords in NIP49 --- .../com/vitorpamplona/quartz/NIP49Test.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP49Test.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP49Test.kt index 006af2b7b..4cb1cfb7b 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP49Test.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP49Test.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey import fr.acinq.secp256k1.Secp256k1 import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.fail import org.junit.Test import org.junit.runner.RunWith import java.security.SecureRandom @@ -98,6 +99,24 @@ public class NIP49Test { } } + @Test + fun normalization() { + val samePassword1 = String(byteArrayOf(0xE2.toByte(), 0x84.toByte(), 0xAB.toByte(), 0xE2.toByte(), 0x84.toByte(), 0xA6.toByte(), 0xE1.toByte(), 0xBA.toByte(), 0x9B.toByte(), 0xCC.toByte(), 0xA3.toByte())) + val samePassword2 = String(byteArrayOf(0xC3.toByte(), 0x85.toByte(), 0xCE.toByte(), 0xA9.toByte(), 0xE1.toByte(), 0xB9.toByte(), 0xA9.toByte())) + + if (samePassword1.toByteArray().contentEquals(samePassword2.toByteArray())) { + fail("Passwords should have a different byte representation") + } + + val encrypted = nip49.encrypt(TEST_CASE_EXPECTED, samePassword1, 8, 0) + + assertNotNull(encrypted) + + val decrypted = nip49.decrypt(encrypted!!, samePassword2) + + assertEquals(TEST_CASE_EXPECTED, decrypted) + } + class Nip49TestCase( val password: String, val secretKey: String, From 7d9784ee44a4f0681ec812ef80659e93fb7ffafc Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 16 Feb 2024 15:40:53 -0500 Subject: [PATCH 094/176] Updates secp256k1 --- quartz/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/build.gradle b/quartz/build.gradle index 8f24aca24..834d8b4fe 100644 --- a/quartz/build.gradle +++ b/quartz/build.gradle @@ -47,7 +47,7 @@ dependencies { implementation "androidx.compose.runtime:runtime:$compose_ui_version" // Bitcoin secp256k1 bindings to Android - api 'fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.13.0' + api 'fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.14.0' // LibSodium for ChaCha encryption (NIP-44) implementation "com.goterl:lazysodium-android:5.1.0@aar" From b88ab06154c52c8434f5ec92ce5276df23461a00 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 16 Feb 2024 15:48:54 -0500 Subject: [PATCH 095/176] Counts quotes as retweets in the notification stats bar --- .../amethyst/ui/note/UserReactionsRow.kt | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt index e0f79881f..5358eab70 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt @@ -68,11 +68,11 @@ import com.vitorpamplona.amethyst.ui.theme.Size24Modifier import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.events.BaseTextNoteEvent import com.vitorpamplona.quartz.events.GenericRepostEvent import com.vitorpamplona.quartz.events.LnZapEvent import com.vitorpamplona.quartz.events.ReactionEvent import com.vitorpamplona.quartz.events.RepostEvent -import com.vitorpamplona.quartz.events.TextNoteEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow @@ -256,10 +256,19 @@ class UserReactionsViewModel(val account: Account) : ViewModel() { (zaps[netDate] ?: BigDecimal.ZERO) + (noteEvent.amount ?: BigDecimal.ZERO) takenIntoAccount.add(noteEvent.id()) } - } else if (noteEvent is TextNoteEvent) { + } else if (noteEvent is BaseTextNoteEvent) { if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) { + val isCitation = + noteEvent.findCitations().any { + LocalCache.getNoteIfExists(it)?.author?.pubkeyHex == currentUser + } + val netDate = formatDate(noteEvent.createdAt) - replies[netDate] = (replies[netDate] ?: 0) + 1 + if (isCitation) { + boosts[netDate] = (boosts[netDate] ?: 0) + 1 + } else { + replies[netDate] = (replies[netDate] ?: 0) + 1 + } takenIntoAccount.add(noteEvent.id()) } } @@ -315,10 +324,19 @@ class UserReactionsViewModel(val account: Account) : ViewModel() { takenIntoAccount.add(noteEvent.id()) hasNewElements = true } - } else if (noteEvent is TextNoteEvent) { + } else if (noteEvent is BaseTextNoteEvent) { if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) { + val isCitation = + noteEvent.findCitations().any { + LocalCache.getNoteIfExists(it)?.author?.pubkeyHex == currentUser + } + val netDate = formatDate(noteEvent.createdAt) - replies[netDate] = (replies[netDate] ?: 0) + 1 + if (isCitation) { + boosts[netDate] = (boosts[netDate] ?: 0) + 1 + } else { + replies[netDate] = (replies[netDate] ?: 0) + 1 + } takenIntoAccount.add(noteEvent.id()) hasNewElements = true } From 67a1aae1d044249b6bac9176d319c756cadea47c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 16 Feb 2024 15:49:42 -0500 Subject: [PATCH 096/176] Avoids the need to tag a cited user to count as a citation. --- .../quartz/events/BaseTextNoteEvent.kt | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt index 005cbfe2b..ae6cb5d0c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt @@ -64,9 +64,9 @@ open class BaseTextNoteEvent( return newStyleReply ?: newStyleRoot ?: oldStylePositional } - @Transient private var citedUsersCache: Set? = null + @Transient private var citedUsersCache: Set? = null - @Transient private var citedNotesCache: Set? = null + @Transient private var citedNotesCache: Set? = null fun citedUsers(): Set { citedUsersCache?.let { @@ -96,10 +96,8 @@ open class BaseTextNoteEvent( val parsed = Nip19.parseComponents(uriScheme, type, key, additionalChars) if (parsed != null) { - val tag = tags.firstOrNull { it.size > 1 && it[1] == parsed.hex } - - if (tag != null && tag[0] == "p") { - returningList.add(tag[1]) + if (parsed.type == Nip19.Type.USER) { + returningList.add(parsed.hex) } } } catch (e: Exception) { @@ -142,16 +140,8 @@ open class BaseTextNoteEvent( val parsed = Nip19.parseComponents(uriScheme, type, key, additionalChars) if (parsed != null) { - try { - val tag = tags.firstOrNull { it.size > 1 && it[1] == parsed.hex } - - if (tag != null && tag[0] == "e") { - citations.add(tag[1]) - } - if (tag != null && tag[0] == "a") { - citations.add(tag[1]) - } - } catch (e: Exception) { + if (parsed.type == Nip19.Type.EVENT || parsed.type == Nip19.Type.ADDRESS || parsed.type == Nip19.Type.NOTE) { + citations.add(parsed.hex) } } } From 56bc10b51324f995948c4ecb1466047d82c4b3e3 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 16 Feb 2024 15:49:57 -0500 Subject: [PATCH 097/176] Speeds up set building --- .../vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt index da4c09467..3965b3821 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt @@ -69,7 +69,7 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter() val loggedInUserHex = loggedInUser.pubkeyHex return collection - .filter { + .filterTo(HashSet()) { it.event !is ChannelCreateEvent && it.event !is ChannelMetadataEvent && it.event !is LnZapRequestEvent && @@ -82,7 +82,6 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter() (isHiddenList || it.author == null || !account.isHidden(it.author!!.pubkeyHex)) && tagsAnEventByUser(it, loggedInUserHex) } - .toSet() } override fun sort(collection: Set): List { From 26356d6060e3265502888fc772b39b40a79f0177 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 16 Feb 2024 15:51:43 -0500 Subject: [PATCH 098/176] simple refactoring --- .../amethyst/ui/dal/NotificationFeedFilter.kt | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt index 3965b3821..d4acf44f0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt @@ -95,17 +95,12 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter() val event = note.event if (event is BaseTextNoteEvent) { - val isAuthoredPostCited = - event.findCitations().any { - LocalCache.notes[it]?.author?.pubkeyHex == authorHex || - LocalCache.addressables[it]?.author?.pubkeyHex == authorHex - } + if (note.replyTo?.any { it.author?.pubkeyHex == authorHex } == true) return true - return isAuthoredPostCited || - ( - event.citedUsers().contains(authorHex) || - note.replyTo?.any { it.author?.pubkeyHex == authorHex } == true - ) + val isAuthoredPostCited = event.findCitations().any { LocalCache.getNoteIfExists(it)?.author?.pubkeyHex == authorHex } + val isAuthorDirectlyCited = event.citedUsers().contains(authorHex) + + return isAuthoredPostCited || isAuthorDirectlyCited } if (event is ReactionEvent) { From 9424147a08a338fa339650984f647ea7704a8570 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 16 Feb 2024 16:00:00 -0500 Subject: [PATCH 099/176] v0.84.3 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 18c367c77..d9932b68d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 34 - versionCode 357 - versionName "0.84.2" + versionCode 358 + versionName "0.84.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From 1fa8cd95dcc61b402bd2293246f3ed6b2467e8fd Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 17 Feb 2024 11:05:40 -0500 Subject: [PATCH 100/176] Improves Layout of Readme --- README.md | 88 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 48 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 5b7b00efd..6fd74a812 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,22 @@ -# Amethyst: Nostr client for Android +

+ + + Amethyst Logo + - +# Amethyst -Amethyst brings the best social network to your Android phone. Just insert your Nostr private key and start posting. +### Nostr Client for Android + +Bringing the best social network to your Android phone. Just insert your Nostr private key and start posting. + +[![GitHub downloads](https://img.shields.io/github/downloads/vitorpamplona/amethyst/total?label=downloads&labelColor=27303D&color=0D1117&logo=github&logoColor=FFFFFF&style=flat)](https://github.com/vitorpamplona/amethyst/releases) + +[![Last Version](https://img.shields.io/github/release/vitorpamplona/amethyst.svg?maxAge=3600&label=Stable&labelColor=06599d&color=043b69)](https://github.com/mihonapp/vitorpamplona/amethyst) +[![CI](https://img.shields.io/github/actions/workflow/status/vitorpamplona/amethyst/build.yml?labelColor=27303D)](https://github.com/vitorpamplona/amethyst/actions/workflows/build.yml) +[![License: Apache-2.0](https://img.shields.io/github/license/vitorpamplona/amethyst?labelColor=27303D&color=0877d2)](/LICENSE) + +## Download [Get it on Obtaininum](https://github.com/vitorpamplona/amethyst/releases) alt="Get it on Google Play" height="80">](https://play.google.com/store/apps/details?id=com.vitorpamplona.amethyst) +
+ # Current Features + + - [x] Events / Relay Subscriptions (NIP-01) - [x] Follow List (NIP-02) - [ ] OpenTimestamps Attestations (NIP-03) @@ -103,31 +121,41 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases) - [ ] Workspaces - [ ] Infinity Scroll +# Privacy and Information Permanence + +Relays know your IP address, your name, your location (guessed from IP), your pub key, all your contacts, and other relays, and can read every action you do (post, like, boost, quote, report, etc) except for Private Zaps and Private DMs. While the content of direct messages (DMs) is only visible to you and your DM counterparty, everyone can see when you and your counterparty DM each other. + +If you want to improve your privacy, consider utilizing a service that masks your IP address (e.g. a VPN or Tor) from trackers online. + +The relay also learns which public keys you are requesting, meaning your public key will be tied to your IP address. + +Information shared on Nostr can be re-broadcasted to other servers and should be assumed permanent for privacy purposes. There is no way to guarantee the deletion of any content once posted. + # Development Overview -## Overall Architecture +This repository is split between Amethyst and Quartz: +- Amethyst is a native Android app made with Kotlin and Jetpack Compose. +- Quartz is our own Nostr-commons library to host classes that are of interest to other Nostr Clients. -This is a native Android app made with Kotlin and Jetpack Compose. -The app uses a modified version of the [nostrpostrlib](https://github.com/Giszmo/NostrPostr/tree/master/nostrpostrlib) to talk to Nostr relays. -The overall architecture consists of the UI, which uses the usual State/ViewModel/Composition, the service layer that connects with Nostr relays, +The app architecture consists of the UI, which uses the usual State/ViewModel/Composition, the service layer that connects with Nostr relays, and the model/repository layer, which keeps all Nostr objects in memory, in a full OO graph. -The repository layer stores Nostr Events as Notes and Users separately. Those classes use LiveData objects to +The repository layer stores Nostr Events as Notes and Users separately. Those classes use LiveData and Flow objects to allow the UI and other parts of the app to subscribe to each Note/User and receive updates when they happen. -They are also responsible for updating viewModels when needed. Filters react to changes in the screen. As the user -sees different Events, the Datasource classes are used to receive more information about those particular Events. +They are also responsible for updating viewModels when needed. As the user scrolls through Events, the Datasource classes +are updated to receive more information about those particular Events. Most of the UI is reactive to changes in the repository classes. The service layer assembles Nostr filters for each need of the app, receives the data from the Relay, and sends it to the repository. Connection with relays is never closed during the use of the app. The UI receives a notification that objects have been updated. Instances of User and Notes are mutable directly. There will never be two Notes with the same ID or two User instances with the same pubkey. -Lastly, the user's account information (priv key/pub key) is stored in the Android KeyStore for security. +Lastly, the user's account information (private key/pub key) is stored in the Android KeyStore for security. ## Setup Make sure to have the following pre-requisites installed: -1. Java 17 +1. Java 17+ 2. Android Studio 3. Android 8.0+ Phone or Emulation setup @@ -136,7 +164,7 @@ Fork and clone this repository and import it into Android Studio git clone https://github.com/vitorpamplona/amethyst.git ``` -Use one of the Android Studio builds to install and run the app in your device or a simulator. +Use an Android Studio build action to install and run the app on your device or a simulator. ## Building Build the app: @@ -168,19 +196,7 @@ For the Play build: ./gradlew installPlayDebug ``` -## Contributing - -[Issues](https://github.com/vitorpamplona/amethyst/issues) and [pull requests](https://github.com/vitorpamplona/amethyst/pulls) here are also very welcome. - -Translations can be provided via [Crowdin](https://crowdin.com/project/amethyst-social) - -You can also send patches through Nostr using [GitStr](https://github.com/fiatjaf/gitstr) to [this nostr address](https://patch34.pages.dev/naddr1qqyxzmt9w358jum5qyg8v6t5daezumn0wd68yvfwvdhk6qg7waehxw309ahx7um5wgkhqatz9emk2mrvdaexgetj9ehx2ap0qy2hwumn8ghj7un9d3shjtnwdaehgu3wvfnj7q3qgcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqxpqqqpmej720gac) - -By contributing to this repository, you agree to license your work under the MIT license. Any work -contributed where you are not the original author must contain its license header with the original -author(s) and source. - -## How to Deploy +## Deploying 1. Generate a new signing key ``` @@ -199,23 +215,13 @@ openssl base64 < | tr -d '\n' | tee some_signing_key.j 7. Add your CHANGE LOG to the description of the new release 8. Download the `aab` file and upload it to the PlayStore. -# Privacy on Relays & nostr -Your internet protocol (IP) address is exposed to the relays you connect to. If you want to improve your privacy, consider utilizing a service that masks your IP address (e.g. a VPN) from trackers online. +## Contributing -The relay also learns which public keys you are requesting, meaning your public key will be tied to your IP address. +[Issues](https://github.com/vitorpamplona/amethyst/issues) and [pull requests](https://github.com/vitorpamplona/amethyst/pulls) here are very welcome. Translations can be provided via [Crowdin](https://crowdin.com/project/amethyst-social) -Relays have all your data in raw text. They know your IP, your name, your location (guessed from IP), your pub key, all your contacts, and other relays, and can read every action you do (post, like, boost, quote, report, etc) with the exception of Private Zaps and Private DMs. +You can also send patches through Nostr using [GitStr](https://github.com/fiatjaf/gitstr) to [this nostr address](https://patch34.pages.dev/naddr1qqyxzmt9w358jum5qyg8v6t5daezumn0wd68yvfwvdhk6qg7waehxw309ahx7um5wgkhqatz9emk2mrvdaexgetj9ehx2ap0qy2hwumn8ghj7un9d3shjtnwdaehgu3wvfnj7q3qgcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqxpqqqpmej720gac) -# DM Privacy # -While the content of direct messages (DMs) is only visible to you and your DM counterparty, everyone can see when you and your counterparty DM each other. - -# Visibility & Permanence of Your Content on nostr -## Information Visibility ## -Content that you share can be shared to other relays. -Information that you share publicly is visible to anyone reading from relays that have your information. Your information may also be visible to nostr users who do not share relays with you. - -## Information Permanence ## -Information shared on nostr should be assumed permanent for privacy purposes. There is no way to guarantee edit or deletion of any content once posted. +By contributing to this repository, you agree to license your work under the MIT license. Any work contributed where you are not the original author must contain its license header with the original author(s) and source. # Screenshots @@ -231,6 +237,7 @@ Information shared on nostr should be assumed permanent for privacy purposes. Th # MIT License +
 Copyright (c) 2023 Vitor Pamplona
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -250,3 +257,4 @@ 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.
+
From e7240c0acfe2669f37d5a2e83bc52bf4e7df0f44 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 17 Feb 2024 11:09:40 -0500 Subject: [PATCH 101/176] Using an SVG as readme logo --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6fd74a812..da4e72067 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@
- - Amethyst Logo + + Amethyst Logo # Amethyst @@ -12,7 +12,7 @@ Bringing the best social network to your Android phone. Just insert your Nostr p [![GitHub downloads](https://img.shields.io/github/downloads/vitorpamplona/amethyst/total?label=downloads&labelColor=27303D&color=0D1117&logo=github&logoColor=FFFFFF&style=flat)](https://github.com/vitorpamplona/amethyst/releases) -[![Last Version](https://img.shields.io/github/release/vitorpamplona/amethyst.svg?maxAge=3600&label=Stable&labelColor=06599d&color=043b69)](https://github.com/mihonapp/vitorpamplona/amethyst) +[![Last Version](https://img.shields.io/github/release/vitorpamplona/amethyst.svg?maxAge=3600&label=Stable&labelColor=06599d&color=043b69)](https://github.com/vitorpamplona/amethyst) [![CI](https://img.shields.io/github/actions/workflow/status/vitorpamplona/amethyst/build.yml?labelColor=27303D)](https://github.com/vitorpamplona/amethyst/actions/workflows/build.yml) [![License: Apache-2.0](https://img.shields.io/github/license/vitorpamplona/amethyst?labelColor=27303D&color=0877d2)](/LICENSE) From e7fc5545ba073829918856f0a0acea2809b8c4b0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 17 Feb 2024 11:15:19 -0500 Subject: [PATCH 102/176] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index da4e72067..151bdf073 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ### Nostr Client for Android -Bringing the best social network to your Android phone. Just insert your Nostr private key and start posting. +Join the social network you control. [![GitHub downloads](https://img.shields.io/github/downloads/vitorpamplona/amethyst/total?label=downloads&labelColor=27303D&color=0D1117&logo=github&logoColor=FFFFFF&style=flat)](https://github.com/vitorpamplona/amethyst/releases) From 88033c2acd15c5288d84aff9f67fdea07b5c33d7 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 17 Feb 2024 11:16:45 -0500 Subject: [PATCH 103/176] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 151bdf073..52d55547e 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ Join the social network you control. [![CI](https://img.shields.io/github/actions/workflow/status/vitorpamplona/amethyst/build.yml?labelColor=27303D)](https://github.com/vitorpamplona/amethyst/actions/workflows/build.yml) [![License: Apache-2.0](https://img.shields.io/github/license/vitorpamplona/amethyst?labelColor=27303D&color=0877d2)](/LICENSE) + + ## Download [](https://github.com/vitorpamplona/amethyst/releases) # Current Features - - - [x] Events / Relay Subscriptions (NIP-01) - [x] Follow List (NIP-02) - [ ] OpenTimestamps Attestations (NIP-03) @@ -229,7 +229,7 @@ By contributing to this repository, you agree to license your work under the MIT |-------------------------------------------|----------------------------------------------|-------------------------------------------------|--------------------------------------------------------| | ![Home Feed](./docs/screenshots/home.png) | ![Messages](./docs/screenshots/messages.png) | ![Live Streams](./docs/screenshots/replies.png) | ![Notifications](./docs/screenshots/notifications.png) | -## Contributors +# Contributors From 879f7cf8e302653c158a4c056d81cc499224d143 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 17 Feb 2024 11:29:18 -0500 Subject: [PATCH 104/176] Smaller download badges --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 52d55547e..52e81c0cf 100644 --- a/README.md +++ b/README.md @@ -16,25 +16,25 @@ Join the social network you control. [![CI](https://img.shields.io/github/actions/workflow/status/vitorpamplona/amethyst/build.yml?labelColor=27303D)](https://github.com/vitorpamplona/amethyst/actions/workflows/build.yml) [![License: Apache-2.0](https://img.shields.io/github/license/vitorpamplona/amethyst?labelColor=27303D&color=0877d2)](/LICENSE) - - ## Download [Get it on Obtaininum](https://github.com/ImranR98/Obtainium) +height="70">](https://github.com/ImranR98/Obtainium) [Get it on GitHub](https://github.com/vitorpamplona/amethyst/releases) +height="70">](https://github.com/vitorpamplona/amethyst/releases) [Get it on F-Droid](https://f-droid.org/packages/com.vitorpamplona.amethyst/) + height="70">](https://f-droid.org/packages/com.vitorpamplona.amethyst/) [Get it on Google Play](https://play.google.com/store/apps/details?id=com.vitorpamplona.amethyst) + height="70">](https://play.google.com/store/apps/details?id=com.vitorpamplona.amethyst)
-# Current Features +## Supported Features + + - [x] Events / Relay Subscriptions (NIP-01) - [x] Follow List (NIP-02) @@ -121,7 +121,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases) - [ ] Workspaces - [ ] Infinity Scroll -# Privacy and Information Permanence +## Privacy and Information Permanence Relays know your IP address, your name, your location (guessed from IP), your pub key, all your contacts, and other relays, and can read every action you do (post, like, boost, quote, report, etc) except for Private Zaps and Private DMs. While the content of direct messages (DMs) is only visible to you and your DM counterparty, everyone can see when you and your counterparty DM each other. From 7af2fcdb2776d7c8b614cfc06f485292dadda1ce Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 17 Feb 2024 11:51:42 -0500 Subject: [PATCH 105/176] Adds playstore badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 52e81c0cf..255fe0731 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Join the social network you control. [![GitHub downloads](https://img.shields.io/github/downloads/vitorpamplona/amethyst/total?label=downloads&labelColor=27303D&color=0D1117&logo=github&logoColor=FFFFFF&style=flat)](https://github.com/vitorpamplona/amethyst/releases) +[![PlayStore downloads](https://img.shields.io/endpoint?color=green&logo=google-play&logoColor=green&url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Dcom.vitorpamplona.amethyst%26gl%3DUS%26hl%3Den%26l%3DPlayStore%26m%3D%24shortinstalls)](https://play.google.com/store/apps/details?id=com.vitorpamplona.amethyst) [![Last Version](https://img.shields.io/github/release/vitorpamplona/amethyst.svg?maxAge=3600&label=Stable&labelColor=06599d&color=043b69)](https://github.com/vitorpamplona/amethyst) [![CI](https://img.shields.io/github/actions/workflow/status/vitorpamplona/amethyst/build.yml?labelColor=27303D)](https://github.com/vitorpamplona/amethyst/actions/workflows/build.yml) From d715ea44df8d15729965bf11135a1733f55edfd1 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 17 Feb 2024 12:39:48 -0500 Subject: [PATCH 106/176] Typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 255fe0731..a95a8fa8d 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Join the social network you control. -[![GitHub downloads](https://img.shields.io/github/downloads/vitorpamplona/amethyst/total?label=downloads&labelColor=27303D&color=0D1117&logo=github&logoColor=FFFFFF&style=flat)](https://github.com/vitorpamplona/amethyst/releases) +[![GitHub downloads](https://img.shields.io/github/downloads/vitorpamplona/amethyst/total?label=Downloads&labelColor=27303D&color=0D1117&logo=github&logoColor=FFFFFF&style=flat)](https://github.com/vitorpamplona/amethyst/releases) [![PlayStore downloads](https://img.shields.io/endpoint?color=green&logo=google-play&logoColor=green&url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Dcom.vitorpamplona.amethyst%26gl%3DUS%26hl%3Den%26l%3DPlayStore%26m%3D%24shortinstalls)](https://play.google.com/store/apps/details?id=com.vitorpamplona.amethyst) [![Last Version](https://img.shields.io/github/release/vitorpamplona/amethyst.svg?maxAge=3600&label=Stable&labelColor=06599d&color=043b69)](https://github.com/vitorpamplona/amethyst) From dd7427971c98f9a7cf59a939ef824a29dfa1fa0f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 17 Feb 2024 13:53:53 -0500 Subject: [PATCH 107/176] refactoring --- .../main/java/com/vitorpamplona/quartz/encoders/Bech32Util.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Bech32Util.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Bech32Util.kt index 59ca60fa0..a327bd131 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Bech32Util.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Bech32Util.kt @@ -51,7 +51,7 @@ object Bech32 { const val ALPHABET: String = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" const val ALPHABET_UPPERCASE: String = "QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L" - enum class Encoding(public val constant: Int) { + enum class Encoding(val constant: Int) { Bech32(1), Bech32m(0x2bc830a3), Beck32WithoutChecksum(0), From dc55d71486a85509d03802d8cde75257fb529721 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 19 Feb 2024 08:17:44 -0500 Subject: [PATCH 108/176] Removing the need to recreate modifiers on BottomBar --- .../amethyst/ui/navigation/AppBottomBar.kt | 20 ++++--------------- .../vitorpamplona/amethyst/ui/theme/Theme.kt | 15 ++++++++++++++ 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt index bfc43e73a..532a5bb60 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt @@ -23,13 +23,9 @@ package com.vitorpamplona.amethyst.ui.navigation import android.graphics.Rect import android.view.View import android.view.ViewTreeObserver -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -45,7 +41,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource @@ -59,7 +54,7 @@ import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Font12SP import com.vitorpamplona.amethyst.ui.theme.Size0dp import com.vitorpamplona.amethyst.ui.theme.Size10Modifier -import com.vitorpamplona.amethyst.ui.theme.Size10dp +import com.vitorpamplona.amethyst.ui.theme.bottomIconModifier import kotlinx.collections.immutable.persistentListOf val bottomNavigationItems = @@ -205,11 +200,9 @@ fun AddNotifIconIfNeeded( @Composable private fun NotificationDotIcon(modifier: Modifier) { - Box(modifier.size(Size10dp)) { + Box(Size10Modifier) { Box( - modifier = - remember { Size10Modifier.clip(shape = CircleShape) } - .background(MaterialTheme.colorScheme.primary), + modifier = MaterialTheme.colorScheme.bottomIconModifier, contentAlignment = Alignment.TopEnd, ) { Text( @@ -217,12 +210,7 @@ private fun NotificationDotIcon(modifier: Modifier) { color = Color.White, textAlign = TextAlign.Center, fontSize = Font12SP, - modifier = - remember { - Modifier - .wrapContentHeight() - .align(Alignment.TopEnd) - }, + // modifier = Modifier.wrapContentHeight().align(Alignment.TopEnd), ) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt index fc2512b9b..2fcdc92b9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt @@ -217,6 +217,18 @@ val DarkLargeRelayIconModifier = .clip(shape = CircleShape) .background(DarkColorPalette.background) +val LightBottomIconModifier = + Modifier + .size(Size10dp) + .clip(shape = CircleShape) + .background(LightColorPalette.primary) + +val DarkBottomIconModifier = + Modifier + .size(Size10dp) + .clip(shape = CircleShape) + .background(DarkColorPalette.primary) + val RichTextDefaults = RichTextStyle().resolveDefaults() val MarkDownStyleOnDark = @@ -381,6 +393,9 @@ val ColorScheme.relayIconModifier: Modifier val ColorScheme.largeRelayIconModifier: Modifier get() = if (isLight) LightLargeRelayIconModifier else DarkLargeRelayIconModifier +val ColorScheme.bottomIconModifier: Modifier + get() = if (isLight) LightBottomIconModifier else DarkBottomIconModifier + val ColorScheme.chartStyle: ChartStyle get() { val defaultColors = if (isLight) DefaultColors.Light else DefaultColors.Dark From b170d04d5eb08473719e93f79e254e20d179dccb Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Mon, 19 Feb 2024 13:20:13 +0000 Subject: [PATCH 109/176] New Crowdin translations by GitHub Action --- app/src/main/res/values-es-rES/strings.xml | 328 +++++++++++++++++++-- app/src/main/res/values-es-rMX/strings.xml | 65 +++- app/src/main/res/values-es-rUS/strings.xml | 65 +++- app/src/main/res/values-hu/strings.xml | 13 + 4 files changed, 446 insertions(+), 25 deletions(-) diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 28e6cf1a0..c9e4bba80 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -3,6 +3,7 @@ Apunta al código QR Mostrar el QR Imagen de Perfil + Tu imagen de perfil Escanear el QR Mostrar de todos modos Post marcado como inapropiado por @@ -42,12 +43,12 @@ Total vistas Impulsar Impulsada - Cita - Nueva cantidad en Sats + Citar + Nueva cantidad en sats Añadir "respondiendo a " " y " - "en canal " + "en el canal " Banner de perfil El pago se ha realizado correctamente Error al analizar el mensaje de error @@ -62,8 +63,8 @@ Donación Lightning Nota al receptor ¡Muchas gracias! - Cantidad en Sats - Enviar Sats + Cantidad en sats + Enviar sats "Error al analizar la vista previa de %1$s : %2$s" "Imagen de vista previa para %1$s" Nuevo canal @@ -73,12 +74,12 @@ Descripción "Sobre nosotros… " ¿Qué tienes en mente? - Enviar + Publicar Guardar Crear Cancelar No se pudo cargar la imagen - Dirección del transmisor + Dirección del relé Publicaciones Bytes Errores @@ -87,7 +88,7 @@ Fuente de chat público Fuente global Fuente de búsqueda - Anadir transmisor + Añadir transmisor Nombre para mostrar Mi nombre para mostrar Avestruz Maravillosa @@ -99,15 +100,15 @@ URL del banner Dirección web Dirección LN - Dirección LN (antiguo) + Dirección LN (antigua) Imagen guardada en la galería Error al guardar la imagen Cargar imagen Cargando… El usuario no tiene una configuración de dirección Lightning para recibir sats "responde aquí… " - Copia la nota-ID para compartir - Copia la nota-ID del canal + Copia el ID de la nota al portapapeles para compartirla en Nostr + Copiar el ID del canal (nota) al portapapeles Edita los metadatos del canal Unirse Conocidos @@ -123,10 +124,10 @@ " Retransmisores" Sitio web Dirección Lightning - Copia la ID de Nsec (su contraseña) en el portapapeles para hacer una copia de seguridad + Copia el ID Nsec (tu contraseña) en el portapapeles para hacer una copia de seguridad Copiar clave privada al portapapeles Copia la clave pública al portapapeles para compartir - Copie la clave pública (NPub) al portapapeles + Copiar la clave pública (NPub) al portapapeles Enviar un mensaje directo Edita los metadatos del usuario Seguir @@ -137,13 +138,16 @@ "npub, hex, username " Limpiar Logo de la app - nsec / npub / hex private key + nsec o npub + contraseña para abrir la clave Mostrar contraseña Ocultar contraseña - Clave invalida + Clave inválida + Clave inválida: %1$s "Acepto los " términos de uso Se requiere la aceptación de los términos + Se requiere una contraseña Se requiere la clave Se requiere un nombre Acceso @@ -166,7 +170,7 @@ cambió el nombre del chat a descripción a e imagen a - Dejar + Salir Dejar de seguir Canal creado "La información del canal cambió a" @@ -194,8 +198,19 @@ Marcar los nuevos como leídos Marcar todos como leídos Claves de respaldo + ## Consejos sobre la protección y la copia de seguridad de las claves + \n\nTu cuenta está protegida por una clave secreta. La clave es una larga secuencia de caracteres que empieza por **nsec1**. Cualquiera que tenga acceso a esta clave secreta puede publicar y cambiar tu identidad. + \n\n- **No** guardes la clave secreta en ningún sitio web o software en los que no confíes. + \n- Los desarrolladores de Amethyst **nunca** te pedirán la clave secreta. + \n- **Sí** puedes realizar una copia de seguridad de la clave secreta para recuperar la cuenta. Para ello, te recomendamos usar un gestor de contraseñas. + + Para mayor seguridad, puedes cifrar la clave con una contraseña. Esta clave empieza por **ncryptsec1** y no puede utilizarse sin tu contraseña. + \n\nSi pierdes la contraseña, no podrás recuperar tu clave. + + Error al cifrar la clave privada Clave secreta (nsec) copiada al portapapeles Copiar mi clave secreta + Cifrar y copiar mi clave secreta Autenticación fallida El sistema biométrico no pudo autenticar al propietario de este teléfono El sistema biométrico no pudo autenticar al propietario de este teléfono. Error: %1$s @@ -217,7 +232,7 @@ Tiene clave privada Solo de lectura, sin clave privada Volver - Selecciona + Seleccionar Compartir enlace del navegador Compartir ID del autor @@ -228,8 +243,23 @@ Seguir Solicitar eliminación Amethyst solicitará que se elimine su nota de los relays a los que está conectado actualmente. No hay garantía de que su nota se elimine permanentemente de esos relays, o de otros relays donde pueda almacenarse. + Bloquear + Eliminar + Bloquear + Reportar + Eliminar + No mostrar de nuevo + Spam o estafas + Groserías o conducta que incita al odio Suplantación de identidad malintencionada + Desnudos o contenido gráfico + Comportamiento ilegal + Si bloqueas a un usuario, se ocultará su contenido en tu app. Tus notas todavía son visibles públicamente, incluso para las personas que bloquees. Los usuarios bloqueados aparecen en la pantalla \"Filtros de seguridad\". + + Reportar abuso Todos los reportes publicados serán visibles públicamente. + Si quieres puedes agregar más contexto al reporte… + Contexto adicional Motivo Selecciona un motivo… Publicar reporte @@ -289,8 +319,23 @@ Todos pueden ver la transacción y el mensaje Privado El remitente y el destinatario pueden verse entre sí y leer el mensaje + Anónimo + El receptor y el público no saben quién envió el pago Non-Zap No hay rastro en Nostr, solo en Lightning + Servidor de archivos + Dirección o @usuario de Lightning + Tus relés (NIP-95) + Los archivos están alojados en tus relés. Nuevo NIP: comprueba si son compatibles. + Configuración de Tor/Orbot + Conéctate a través de tu configuración de Orbot + ¿Desconectarse de tu Orbot/Tor? + Los datos se transferirán de inmediato en la red normal + + No + Lista de seguidos + Todos los seguidos + Global Lista de silenciados ## Conéctate a través de Tor con Orbot \n\n1. Instala [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android). @@ -313,6 +358,21 @@ para %1$s Notificar: Unirse a la conversación + ID de usuario o grupo + npub, nevent o hexadecimal + Crear + Unirse + Hoy + Advertencia de contenido + Esta publicación incluye contenido delicado que algunas personas pueden considerar ofensivo o perturbador. + Ocultar siempre el contenido delicado + Mostrar siempre el contenido delicado + Mostrar siempre advertencias sobre contenido + Recomendaciones: + Filtrar el spam de desconocidos + Avisar cuando las publicaciones tengan reportes de tus seguidos + Nuevo símbolo de reacción + No hay tipos de reacción preseleccionados para este usuario. Deja presionado el botón de corazón para cambiarlos. Zapraiser Añade una meta de sats para recaudar por esta publicación. Los clientes compatibles pueden mostrarla como una barra de progreso para incentivar las donaciones. Cantidad objetivo en sats @@ -328,6 +388,21 @@ Tasas de admisión URL de pagos Limitaciones + Países + Idiomas + Etiquetas + Política de publicación + Longitud del mensaje + Suscripciones + Filtros + Longitud del ID de la suscripción + Prefijo mínimo + Etiquetas de evento máximas + Longitud del contenido + PoW mínima + Auth + Pago + Token de Cashu Canjear Enviar a monedero de zaps Abrir en monedero de Cashu @@ -343,6 +418,21 @@ Al cerrar la sesión se borra toda tu información local. Asegúrate de tener una copia de seguridad de tus claves privadas para que no pierdas la cuenta. ¿Quieres continuar? Etiquetas seguidas Relés + Mercado + En vivo + Comunidad + Chats + Publicaciones aprobadas + Este grupo no tiene descripción ni reglas. Habla con el propietario para agregar una. + Esta comunidad no tiene descripción. Habla con el propietario para agregar una. + Contenido delicado + Agrega una advertencia de contenido sensible antes de mostrarlo + Configuración + Siempre + Solo Wi-Fi + Nunca + Sistema + Claro Oscuro Preferencias de la aplicación Idioma @@ -354,4 +444,208 @@ Ocultar barras de navegación al desplazarse Cargar imagen Spammers + Silenciado. Hacer clic para reactivar el sonido. + Sonido activado. Hacer clic para silenciar. + Buscar grabaciones locales y remotas + La dirección de Nostr se verificó + No se pudo verificar la dirección de Nostr + Verificando dirección de Nostr + Seleccionar o deseleccionar todo + Predeterminados + Seleccionar un relé para continuar + Reenviar zaps a: + Los clientes compatibles reenviarán los zaps a la dirección de Lightning o al perfil de usuario indicados a continuación en lugar de a los tuyos. + Revelar ubicación como + Agrega un Geohash de tu ubicación al mensaje. El público sabrá que te encuentras a menos de 5 km (3 mi) de la ubicación actual. + Agrega una advertencia de contenido delicado antes de mostrarlo. Esto es ideal para cualquier contenido NSFW o que a algunas personas les pueda resultar ofensivo o perturbador. + Función nueva + La activación de este modo requiere que Amethyst envíe un mensaje NIP-24 (mensajes directos sellados, de grupo y “GiftWrapped\"). NIP-24 es nuevo y la mayoría de los clientes aún no lo han implementado. Comprueba que el destinatario use un cliente compatible. + Activar + Público + Nuevo grupo público o privado + Privado + Para + Asunto + Tema de la conversación + "\@Usuario1, @Usuario2, @Usuario3" + Miembros de este grupo + Explicación para los miembros + Cambio de nombre para los nuevos objetivos. + Pegar desde el portapapeles + Para la interfaz de la aplicación + Tema oscuro, claro o del sistema + Cargar automáticamente imágenes y GIF + Reproducir automáticamente videos y GIF + Mostrar vistas previas de URL + Cuándo cargar imágenes + Copiar al portapapeles + Copiar npub al portapapeles + Copiar URL al portapapeles + Copiar ID de la nota al portapapeles + Creado + Reglas + Iniciar sesión con Amber + Actualiza tu estado + Error al analizar el mensaje de error + Los votos se calculan según la cantidad de zaps. Puedes establecer una cantidad mínima para evitar spammers y una máxima para que los zappers grandes no puedan dominar la encuesta. Usa la misma cantidad en ambos campos para asegurarte de que cada voto tenga el mismo valor. Déjalos vacíos para aceptar cualquier cantidad. + No se pudo enviar el zap + Mensaje para el usuario + OK + No se pudo llegar a %1$s: %2$s + No se pudo llegar a %1$s: %2$s + Error al analizar el resultado de %1$s: %2$s + %1$s falló con el código %2$s + Activo para: + Inicio + Mensajes directos + Chats + Global + Búsqueda + Dividir y reenviar zaps + Los clientes compatibles dividirán y reenviarán los zaps a los usuarios que se agreguen aquí en lugar de enviártelos a ti. + Buscar y agregar usuario + Nombre de usuario o para mostrar + Lightning no está configurado + El usuario %1$s no tiene una dirección de Lightning configurada para recibir sats. + Porcentaje + 25 + Dividir zaps con + Reenviar zaps a + No se encontraron billeteras de Lightning + Pagado + Monedero %1$s + Error al abrir la aplicación firmante + No se pudo encontrar la aplicación firmante. Comprueba si no se desinstaló la aplicación. + Solicitud de firma rechazada + Asegúrate de que la aplicación firmante haya autorizado esta transacción. + No se encontraron monederos para pagar una factura de Lightning (error: %1$s). Instala un monedero de Lightning para usar zaps. + No se encontraron monederos para pagar una factura de Lightning. Instala un monedero de Lightning para usar zaps. + Palabras ocultas + Ocultar nueva palabra o frase + Imagen de perfil + Mostrar imágenes de perfil + Seleccionar una opción + No se pudo pagar la factura + No se pudo retirar + No se pudo configurar Wallet Connect + Error al analizar la cadena de conexión NIP-47. Comprueba si esto es correcto con el proveedor del monedero: %1$s. Error: %2$s + Error al analizar la cadena de conexión NIP-47. Comprueba si esto es correcto con el proveedor del monedero: %1$s. + No se pudo canjear Cashu + La Mint proporcionó el siguiente mensaje de error: %1$s + Los tokens de Cashu ya se gastaron. + Cashu recibido + Se enviaron %1$s sats a tu monedero (comisión: %2$s sats). + No se encontró ningún monedero de Cashu compatible en el sistema + No se puede obtener la factura de los servidores del destinatario + El proveedor de conexión de monedero devolvió el siguiente error: %1$s + No se pudo conectar a Tor + No está disponible la descarga de documentos del relé + No se pudo ensamblar la LNUrl desde la dirección de Lightning \"%1$s\". Comprueba la configuración del usuario. + El servicio de Lightning del destinatario en %1$s no está disponible. Se calculó a partir de la dirección de Lightning \"%2$s\". Error: %3$s. Comprueba si el servidor está activo y si la dirección de Lightning es correcta. + No se pudo resolver %1$s. Comprueba si tienes conexión, si el servidor está activo y si la dirección de Lightning \"%2$s\" es correcta. + No se pudo resolver %1$s. Comprueba si tienes conexión, si el servidor está activo y si la dirección de Lightning \"%2$s\" es correcta..\n\nLa excepción fue: %3$s + No se pudo obtener la factura de %1$s + Error al analizar JSON desde la dirección de Lightning. Comprueba la configuración de Lightning del usuario. + No se encontró la URL de devolución de llamada en la configuración del servidor de la dirección de Lightning del usuario. + Error al analizar JSON desde la recuperación de factura de la dirección de Lightning. Comprueba la configuración de Lightning del usuario. + Cantidad de factura incorrecta (%1$s sats) de %2$s. Debería haber sido %3$s. + No se pudo crear una factura de Lightning antes de enviar el zap. El monedero de Lightning del destinatario envió el siguiente error: %1$s. + No se pudo crear una factura de Lightning antes de enviar el zap. No se encontró el elemento pr en el JSON resultante. + Usuario de solo lectura + No hay reacciones configuradas + Seleccionar una aplicación UnifiedPush + Notificación push + De aplicaciones UnifiedPush instaladas + + Desactiva las notificaciones push + Usa la app %1$s + Configuración de notificaciones push + Para recibir notificaciones push, instala cualquier aplicación compatible con [Unified Push](https://unifiedpush.org/), como [Nfty](https://ntfy.sh/). + Tras la instalación, selecciona la aplicación que deseas usar en la configuración. + + Mensaje de %1$s + Conversación + Envía un mensaje al vendedor + Hola, %1$s, ¿esto todavía está disponible? + Hola, ¿esto todavía está disponible? + Vender un artículo + Título + iPhone 13 + Estado + Categoría + Precio (en sats) + 1000 + Ubicación + Ciudad, estado, país. + Nuevo + Producto totalmente nuevo, en la caja original. + Como nuevo + Usado, pero sin señales de uso. + Bueno + Tiene algunas marcas de uso superficiales. + Aceptable + Todavía está en forma aceptable y funcional. + Ropa + Accesorios + Artículos electrónicos + Muebles + Objetos de colección + Libros + Mascotas + Deportes + Acondicionamiento físico + Arte + Artesanías + Hogar + Oficina + Comida + Varios + Otros + Error al subir contenido multimedia + No se pudo abrir el archivo comprimido + Error al comprimir el contenido multimedia: %1$s + Error de carga: %1$s + El servidor no proporcionó una URL después de la carga + No se pudo descargar el contenido cargado desde el servidor + No se pudo preparar el archivo local para cargar: %1$s + Iniciar sesión con código QR + Ruta + Inicio + Buscar + Descubrir + Mensajes + Notificaciones + Global + Cortos + Filtros de seguridad + Nueva publicación + Nuevos cortos: imágenes o vídeos + Nueva nota comunitaria + Abrir todas las reacciones a esta publicación + Cerrar todas las reacciones a esta publicación + Responder + Impulsar o citar + Me gusta + Zap + Imagen de perfil de %1$s + Relé %1$s + Ampliar lista de relés + Opciones de notas + Selector de lista de relés + Encuesta + Desactivar encuesta + Factura de Bitcoin + Cancelar factura de Bitcoin + Cancelar la venta de un artículo + Zapraiser + Cancelar zapraiser + Ubicación + Eliminar ubicación + Divisiones de zaps + Cancelar división de zap + Añadir advertencia de contenido + Eliminar advertencia de contenido + Mostrar npub como código QR + Dirección inválida + Amethyst recibió un URI para abrir pero era inválido: %1$s diff --git a/app/src/main/res/values-es-rMX/strings.xml b/app/src/main/res/values-es-rMX/strings.xml index de402ba42..9e284eff0 100644 --- a/app/src/main/res/values-es-rMX/strings.xml +++ b/app/src/main/res/values-es-rMX/strings.xml @@ -3,6 +3,7 @@ Apunta al código QR Mostrar QR Imagen de perfil + Tu imagen de perfil Escanear código QR Mostrar de todos modos La publicación fue reportada por @@ -42,7 +43,7 @@ Ver recuento Impulsar Impulsada - Citar + Cita Nueva cantidad en sats Agregar "respondiendo a " @@ -82,7 +83,7 @@ Publicaciones Bytes Errores - Feed de inicio + Tus noticias Feed de mensajes privados Feed del chat público Feed global @@ -138,12 +139,15 @@ Borrar Logo de la app nsec o npub + contraseña para abrir la clave Mostrar contraseña Ocultar contraseña Clave inválida - "Acepto las " + Clave inválida: %1$s + "Acepto los " condiciones de uso Es obligatorio aceptar las condiciones + Se requiere una contraseña Se requiere la clave Se requiere un nombre Iniciar sesión @@ -194,8 +198,19 @@ Marcar todos los nuevos como leídos Marcar todos como leídos Respaldar claves + ## Consejos sobre la protección y la copia de seguridad de las claves + \n\nTu cuenta está protegida por una clave secreta. La clave es una larga secuencia de caracteres que empieza por **nsec1**. Cualquiera que tenga acceso a esta clave secreta puede publicar y cambiar tu identidad. + \n\n- **No** guardes la clave secreta en ningún sitio web o software en los que no confíes. + \n- Los desarrolladores de Amethyst **nunca** te pedirán la clave secreta. + \n- **Sí** puedes realizar una copia de seguridad de la clave secreta para recuperar la cuenta. Para ello, te recomendamos usar un gestor de contraseñas. + + Para mayor seguridad, puedes cifrar la clave con una contraseña. Esta clave empieza por **ncryptsec1** y no puede utilizarse sin tu contraseña. + \n\nSi pierdes la contraseña, no podrás recuperar tu clave. + + Error al cifrar la clave privada Clave secreta (nsec) copiada al portapapeles Copiar mi clave secreta + Cifrar y copiar mi clave secreta Error de autenticación El sistema biométrico no pudo autenticar al propietario de este teléfono El sistema biométrico no pudo autenticar al propietario de este teléfono. Error: %1$s @@ -447,6 +462,7 @@ La activación de este modo requiere que Amethyst envíe un mensaje NIP-24 (mensajes directos sellados, de grupo y “GiftWrapped\"). NIP-24 es nuevo y la mayoría de los clientes aún no lo han implementado. Comprueba que el destinatario use un cliente compatible. Activar Público + Nuevo grupo público o privado Privado Para Asunto @@ -463,6 +479,7 @@ Mostrar vistas previas de URL Cuándo cargar imágenes Copiar al portapapeles + Copiar npub al portapapeles Copiar URL al portapapeles Copiar ID de la nota al portapapeles Creado @@ -518,7 +535,7 @@ Los tokens de Cashu ya se gastaron. Cashu recibido Se enviaron %1$s sats a tu billetera (comisión: %2$s sats). - No se encontró ninguna billetera Cashu compatible en el sistema + No se encontró ninguna billetera de Cashu compatible en el sistema No se puede obtener la factura de los servidores del destinatario El proveedor de conexión de billetera devolvió el siguiente error: %1$s No se pudo conectar a Tor @@ -591,4 +608,44 @@ El servidor no proporcionó una URL después de la subida No se pudo descargar el contenido subido desde el servidor No se pudo preparar el archivo local para subir: %1$s + Iniciar sesión con código QR + Ruta + Inicio + Buscar + Descubrir + Mensajes + Notificaciones + Global + Cortos + Filtros de seguridad + Nueva publicación + Nuevos cortos: imágenes o videos + Nueva nota comunitaria + Abrir todas las reacciones a esta publicación + Cerrar todas las reacciones a esta publicación + Responder + Impulsar o citar + Me gusta + Zap + Imagen de perfil de %1$s + Relé %1$s + Ampliar lista de relés + Opciones de notas + Selector de lista de relés + Encuesta + Desactivar encuesta + Factura de Bitcoin + Cancelar factura de Bitcoin + Cancelar la venta de un artículo + Zapraiser + Cancelar zapraiser + Ubicación + Eliminar ubicación + Divisiones de zaps + Cancelar división de zap + Agregar advertencia de contenido + Eliminar advertencia de contenido + Mostrar npub como código QR + Dirección inválida + Amethyst recibió un URI para abrir pero era inválido: %1$s diff --git a/app/src/main/res/values-es-rUS/strings.xml b/app/src/main/res/values-es-rUS/strings.xml index 173e79d01..b0c447063 100644 --- a/app/src/main/res/values-es-rUS/strings.xml +++ b/app/src/main/res/values-es-rUS/strings.xml @@ -3,6 +3,7 @@ Apunta al código QR Mostrar QR Imagen de perfil + Tu imagen de perfil Escanear código QR Mostrar de todos modos La publicación fue reportada por @@ -42,7 +43,7 @@ Ver recuento Impulsar Impulsada - Citar + Cita Nueva cantidad en sats Agregar "respondiendo a " @@ -82,7 +83,7 @@ Publicaciones Bytes Errores - Feed de inicio + Tus noticias Feed de mensajes privados Feed del chat público Feed global @@ -138,12 +139,15 @@ Borrar Logo de la app nsec o npub + contraseña para abrir la clave Mostrar contraseña Ocultar contraseña Clave inválida - "Acepto las " + Clave inválida: %1$s + "Acepto los " condiciones de uso Es obligatorio aceptar las condiciones + Se requiere una contraseña Se requiere la clave Se requiere un nombre Iniciar sesión @@ -194,8 +198,19 @@ Marcar todos los nuevos como leídos Marcar todos como leídos Respaldar claves + ## Consejos sobre la protección y la copia de seguridad de las claves + \n\nTu cuenta está protegida por una clave secreta. La clave es una larga secuencia de caracteres que empieza por **nsec1**. Cualquiera que tenga acceso a esta clave secreta puede publicar y cambiar tu identidad. + \n\n- **No** guardes la clave secreta en ningún sitio web o software en los que no confíes. + \n- Los desarrolladores de Amethyst **nunca** te pedirán la clave secreta. + \n- **Sí** puedes realizar una copia de seguridad de la clave secreta para recuperar la cuenta. Para ello, te recomendamos usar un gestor de contraseñas. + + Para mayor seguridad, puedes cifrar la clave con una contraseña. Esta clave empieza por **ncryptsec1** y no puede utilizarse sin tu contraseña. + \n\nSi pierdes la contraseña, no podrás recuperar tu clave. + + Error al cifrar la clave privada Clave secreta (nsec) copiada al portapapeles Copiar mi clave secreta + Cifrar y copiar mi clave secreta Error de autenticación El sistema biométrico no pudo autenticar al propietario de este teléfono El sistema biométrico no pudo autenticar al propietario de este teléfono. Error: %1$s @@ -447,6 +462,7 @@ La activación de este modo requiere que Amethyst envíe un mensaje NIP-24 (mensajes directos sellados, de grupo y “GiftWrapped\"). NIP-24 es nuevo y la mayoría de los clientes aún no lo han implementado. Comprueba que el destinatario use un cliente compatible. Activar Público + Nuevo grupo público o privado Privado Para Asunto @@ -463,6 +479,7 @@ Mostrar vistas previas de URL Cuándo cargar imágenes Copiar al portapapeles + Copiar npub al portapapeles Copiar URL al portapapeles Copiar ID de la nota al portapapeles Creado @@ -518,7 +535,7 @@ Los tokens de Cashu ya se gastaron. Cashu recibido Se enviaron %1$s sats a tu billetera (comisión: %2$s sats). - No se encontró ninguna billetera Cashu compatible en el sistema + No se encontró ninguna billetera de Cashu compatible en el sistema No se puede obtener la factura de los servidores del destinatario El proveedor de conexión de billetera devolvió el siguiente error: %1$s No se pudo conectar a Tor @@ -591,4 +608,44 @@ El servidor no proporcionó una URL después de la subida No se pudo descargar el contenido subido desde el servidor No se pudo preparar el archivo local para subir: %1$s + Iniciar sesión con código QR + Ruta + Inicio + Buscar + Descubrir + Mensajes + Notificaciones + Global + Cortos + Filtros de seguridad + Nueva publicación + Nuevos cortos: imágenes o videos + Nueva nota comunitaria + Abrir todas las reacciones a esta publicación + Cerrar todas las reacciones a esta publicación + Responder + Impulsar o citar + Me gusta + Zap + Imagen de perfil de %1$s + Relé %1$s + Ampliar lista de relés + Opciones de notas + Selector de lista de relés + Encuesta + Desactivar encuesta + Factura de Bitcoin + Cancelar factura de Bitcoin + Cancelar la venta de un artículo + Zapraiser + Cancelar zapraiser + Ubicación + Eliminar ubicación + Divisiones de zaps + Cancelar división de zap + Agregar advertencia de contenido + Eliminar advertencia de contenido + Mostrar npub como código QR + Dirección inválida + Amethyst recibió un URI para abrir pero era inválido: %1$s diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 6d18c0e8e..d4715f21a 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -139,12 +139,15 @@ Törlés App Logó nsec / npub / hex privát kulcs + jelszót a kulcs kinyitásához Jelszó megjelenítése Jelszó elrejtése Érvénytelen kulcs + Érvénytelen kulcs: %1$s "Elfogadom a " használati feltételeket A feltételek elfogadása szükséges + Jelszó megadása kötelező Kulcs szükséges Név megadása kötelező Bejelentkezés @@ -195,8 +198,18 @@ Az összes új megjelölése olvasottként Összes megjelölése olvasottként Biztonsági Kulcsok + ## Kulcs- és biztonsági mentési tippek + \n\nFiókodat titkos kulcs védi. A kulcs egy hosszú véletlenszerű karakterlánc, amely **nsec1**-el kezdődik. Bárki, aki a Te titkos kulcsodhoz hozzáfér, az a személyazonosságod használatával bármilyen tartalmat közzétehet. + \n\n- **Ne** helyezd el titkos kulcsodat olyan webhelyen vagy szoftverben, amelyben nem bízol. + \n- Az Amethyst fejlesztők a titkos kulcsodat **soha** nem fogják elkérni. + \n- A fiók-helyreállításhoz, titkos kulcsodról **mindig** készíts biztonságos biztonsági másolatot. Javasoljuk a jelszókezelő használatát. + + A nagyobb biztonság érdekében a kulcsot jelszóval titkosíthatod. Ez a kulcs **ncryptsec1**-val kezdődik, és a jelszavad nélkül nem használható. + \n\nHa elveszíted a jelszavát, nem tudod a kulcsodat visszaállítani. + Nem sikerült a privát kulcsot titkosítani A titkos kulcs (nsec) a vágólapra másolva A titkos kulcsom másolása + Titkosítsd és másold a titkos kulcsomat Hitelesítés nem sikerült A telefon tulajdonosát a biometrikus adatokkal nem sikerült hitelesíteni A telefon tulajdonosát a biometrikus adatokkal nem sikerült hitelesíteni. Hiba: %1$s From a97ade41fa45b3cc613464762eda9f6dd5f7bb92 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 19 Feb 2024 18:55:29 -0500 Subject: [PATCH 110/176] adds a release drafter to test --- .github/workflows/create-release.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 18105c04d..65829028f 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -227,4 +227,9 @@ jobs: upload_url: ${{ steps.create_release.outputs.upload_url }} asset_path: app/build/outputs/bundle/fdroidRelease/app-fdroid-release.aab asset_name: amethyst-fdroid-${{ github.ref_name }}.aab - asset_content_type: application/zip \ No newline at end of file + asset_content_type: application/zip + + - name: Drafts a description for the release + uses: release-drafter/release-drafter@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From 1b93fc57cb824f78cbeec93bcebb4dd6e06d1fef Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 19 Feb 2024 18:56:08 -0500 Subject: [PATCH 111/176] Creates a way to ask for donations in the Notification page. --- .../amethyst/LocalPreferences.kt | 5 + .../vitorpamplona/amethyst/model/Account.kt | 17 +- .../amethyst/ui/elements/DisplayZapSplits.kt | 19 +- .../amethyst/ui/note/NoteCompose.kt | 2 +- .../amethyst/ui/note/ReactionsRow.kt | 8 +- .../amethyst/ui/note/ZapTheDevsCard.kt | 459 ++++++++++++++++++ .../amethyst/ui/screen/CardFeedView.kt | 29 ++ .../amethyst/ui/screen/ThreadFeedView.kt | 2 +- .../ui/screen/loggedIn/AccountViewModel.kt | 6 + app/src/main/res/values/strings.xml | 8 + .../vitorpamplona/quartz/crypto/KeyPair.kt | 8 +- 11 files changed, 552 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapTheDevsCard.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index ab2498580..c62bdc4b0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -112,6 +112,7 @@ private object PrefKeys { const val LOGIN_WITH_EXTERNAL_SIGNER = "login_with_external_signer" const val AUTOMATICALLY_SHOW_PROFILE_PICTURE = "automatically_show_profile_picture" const val SIGNER_PACKAGE_NAME = "signer_package_name" + const val HAS_DONATED_IN_VERSION = "has_donated_in_version" const val ALL_ACCOUNT_INFO = "all_saved_accounts_info" const val SHARED_SETTINGS = "shared_settings" @@ -331,6 +332,7 @@ object LocalPreferences { PrefKeys.LAST_READ_PER_ROUTE, Event.mapper.writeValueAsString(account.lastReadPerRoute), ) + putStringSet(PrefKeys.HAS_DONATED_IN_VERSION, account.hasDonatedInVersion) if (account.showSensitiveContent == null) { remove(PrefKeys.SHOW_SENSITIVE_CONTENT) @@ -617,6 +619,8 @@ object LocalPreferences { NostrSignerInternal(keyPair) } + val hasDonatedInVersion = getStringSet(PrefKeys.HAS_DONATED_IN_VERSION, null) ?: setOf() + val account = Account( keyPair = keyPair, @@ -644,6 +648,7 @@ object LocalPreferences { warnAboutPostsWithReports = warnAboutReports, filterSpamFromStrangers = filterSpam, lastReadPerRoute = lastReadPerRoute, + hasDonatedInVersion = hasDonatedInVersion, ) // Loads from DB diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index bc830d6ce..ab1effaf1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -31,6 +31,7 @@ import androidx.lifecycle.asLiveData import androidx.lifecycle.liveData import androidx.lifecycle.switchMap import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.service.FileHeader import com.vitorpamplona.amethyst.service.Nip96MediaServers import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource @@ -96,6 +97,7 @@ import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toPersistentSet +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -181,10 +183,9 @@ class Account( var warnAboutPostsWithReports: Boolean = true, var filterSpamFromStrangers: Boolean = true, var lastReadPerRoute: Map = mapOf(), + var hasDonatedInVersion: Set = setOf(), + val scope: CoroutineScope = Amethyst.instance.applicationIOScope, ) { - // Uses a single scope for the entire application. - val scope = Amethyst.instance.applicationIOScope - var transientHiddenUsers: ImmutableSet = persistentSetOf() data class PaymentRequest( @@ -2354,6 +2355,16 @@ class Account( return lastReadPerRoute[route] ?: 0 } + fun hasDonatedInThisVersion(): Boolean { + return hasDonatedInVersion.contains(BuildConfig.VERSION_NAME) + } + + fun markDonatedInThisVersion() { + hasDonatedInVersion = hasDonatedInVersion + BuildConfig.VERSION_NAME + saveable.invalidateData() + live.invalidateData() + } + suspend fun registerObservers() = withContext(Dispatchers.Main) { // saves contact list for the next time. diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayZapSplits.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayZapSplits.kt index 6fab24206..ecdf5ad57 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayZapSplits.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayZapSplits.kt @@ -49,15 +49,32 @@ import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.Size25dp import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.quartz.events.EventInterface +import com.vitorpamplona.quartz.events.ZapSplitSetup @OptIn(ExperimentalLayoutApi::class) @Composable fun DisplayZapSplits( noteEvent: EventInterface, + useAuthorIfEmpty: Boolean = false, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - val list = remember(noteEvent) { noteEvent.zapSplitSetup() } + val list = + remember(noteEvent) { + val list = noteEvent.zapSplitSetup() + if (list.isEmpty() && useAuthorIfEmpty) { + listOf( + ZapSplitSetup( + lnAddressOrPubKeyHex = noteEvent.pubKey(), + relay = null, + weight = 1.0, + isLnAddress = false, + ), + ) + } else { + list + } + } if (list.isEmpty()) return Row(verticalAlignment = Alignment.CenterVertically) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 297789c98..08f07f767 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -1123,7 +1123,7 @@ private fun NoteBody( val zapSplits = remember(noteEvent) { noteEvent?.hasZapSplitSetup() ?: false } if (zapSplits && noteEvent != null) { Spacer(modifier = HalfDoubleVertSpacer) - DisplayZapSplits(noteEvent, accountViewModel, nav) + DisplayZapSplits(noteEvent, false, accountViewModel, nav) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 6750bf8e6..4cc196fd0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -625,7 +625,7 @@ fun TextCount( } @Composable -private fun SlidingAnimationAmount( +fun SlidingAnimationAmount( amount: MutableState, textColor: Color, ) { @@ -1028,7 +1028,7 @@ fun ZapReaction( } } -private fun zapClick( +fun zapClick( baseNote: Note, accountViewModel: AccountViewModel, context: Context, @@ -1065,7 +1065,7 @@ private fun zapClick( } @Composable -private fun ObserveZapIcon( +fun ObserveZapIcon( baseNote: Note, accountViewModel: AccountViewModel, inner: @Composable (MutableState) -> Unit, @@ -1088,7 +1088,7 @@ private fun ObserveZapIcon( } @Composable -private fun ObserveZapAmountText( +fun ObserveZapAmountText( baseNote: Note, accountViewModel: AccountViewModel, inner: @Composable (MutableState) -> Unit, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapTheDevsCard.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapTheDevsCard.kt new file mode 100644 index 000000000..593f6bc3a --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapTheDevsCard.kt @@ -0,0 +1,459 @@ +/** + * Copyright (c) 2024 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.ui.note + +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProgressIndicatorDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.BuildConfig +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.ThemeType +import com.vitorpamplona.amethyst.service.ZapPaymentHandler +import com.vitorpamplona.amethyst.ui.components.ClickableText +import com.vitorpamplona.amethyst.ui.components.LoadNote +import com.vitorpamplona.amethyst.ui.elements.DisplayZapSplits +import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.routeToMessage +import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.ModifierWidth3dp +import com.vitorpamplona.amethyst.ui.theme.Size10dp +import com.vitorpamplona.amethyst.ui.theme.Size20Modifier +import com.vitorpamplona.amethyst.ui.theme.Size20dp +import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn +import com.vitorpamplona.amethyst.ui.theme.imageModifier +import com.vitorpamplona.quartz.crypto.KeyPair +import com.vitorpamplona.quartz.events.Event +import fr.acinq.secp256k1.Hex +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking + +@Preview +@Composable +fun ZapTheDevsCardPreview() { + val sharedPreferencesViewModel: SharedPreferencesViewModel = viewModel() + val myCoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + sharedPreferencesViewModel.init() + sharedPreferencesViewModel.updateTheme(ThemeType.DARK) + + runBlocking(Dispatchers.IO) { + val releaseNotes = + """ + { + "id": "0465b20da0adf45dd612024d124e1ed384f7ecd2cd7358e77998828e7bf35fa2", + "pubkey": "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", + "created_at": 1708014675, + "kind": 1, + "tags": [ + [ + "p", + "ca89cb11f1c75d5b6622268ff43d2288ea8b2cb5b9aa996ff9ff704fc904b78b", + "", + "mention" + ], + [ + "p", + "7eb29c126b3628077e2e3d863b917a56b74293aa9d8a9abc26a40ba3f2866baf", + "", + "mention" + ], + [ + "t", + "Amethyst" + ], + [ + "t", + "amethyst" + ], + [ + "zap", + "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", + "wss://vitor.nostr1.com", + "0.6499999761581421" + ], + [ + "zap", + "ca89cb11f1c75d5b6622268ff43d2288ea8b2cb5b9aa996ff9ff704fc904b78b", + "wss://nos.lol", + "0.25" + ], + [ + "zap", + "7eb29c126b3628077e2e3d863b917a56b74293aa9d8a9abc26a40ba3f2866baf", + "wss://vitor.nostr1.com", + "0.10000000149011612" + ], + [ + "r", + "https://github.com/vitorpamplona/amethyst/releases/download/v0.84.2/amethyst-googleplay-universal-v0.84.2.apk" + ], + [ + "r", + "https://github.com/vitorpamplona/amethyst/releases/download/v0.84.2/amethyst-fdroid-universal-v0.84.2.apk" + ] + ], + "content": "#Amethyst v0.84.2: Text alignment fix\n\nBugfixes:\n- Fixes link misalignment in posts\n\nUpdated translations: \n- Czech, German, Swedish, and Portuguese by nostr:npub1e2yuky03caw4ke3zy68lg0fz3r4gkt94hx4fjmlelacyljgyk79svn3eef\n- French by nostr:npub106efcyntxc5qwl3w8krrhyt626m59ya2nk9f40px5s968u5xdwhsjsr8fz\n\nDownload:\n- [Play Edition](https://github.com/vitorpamplona/amethyst/releases/download/v0.84.2/amethyst-googleplay-universal-v0.84.2.apk )\n- [FOSS Edition - No translations](https://github.com/vitorpamplona/amethyst/releases/download/v0.84.2/amethyst-fdroid-universal-v0.84.2.apk )", + "sig": "e036ecce534e22efd47634c56328af62576ab3a36c565f7c8c5fbea67f48cd46d4041ecfc0ca01dafa0ebe8a0b119d125527a28f88aa30356b80c26dd0953aed" + } + """.trimIndent() + + LocalCache.justConsume(Event.fromJson(releaseNotes), null) + } + + val accountViewModel = + AccountViewModel( + Account( + // blank keys + keyPair = + KeyPair( + privKey = Hex.decode("0f761f8a5a481e26f06605a1d9b3e9eba7a107d351f43c43a57469b788274499"), + pubKey = Hex.decode("989c3734c46abac7ce3ce229971581a5a6ee39cdd6aa7261a55823fa7f8c4799"), + forcePubKeyCheck = false, + ), + scope = myCoroutineScope, + ), + sharedPreferencesViewModel.sharedPrefs, + ) + + LoadNote( + baseNoteHex = "0465b20da0adf45dd612024d124e1ed384f7ecd2cd7358e77998828e7bf35fa2", + accountViewModel, + ) { releaseNote -> + if (releaseNote != null) { + ThemeComparisonColumn( + onDark = { + ZapTheDevsCard( + releaseNote, + accountViewModel, + nav = {}, + ) + }, + onLight = { + ZapTheDevsCard( + releaseNote, + accountViewModel, + nav = {}, + ) + }, + ) + } + } +} + +@Composable +fun ZapTheDevsCard( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val releaseNoteState by baseNote.live().metadata.observeAsState() + val releaseNote = releaseNoteState?.note ?: return + + Row(modifier = Modifier.padding(horizontal = Size10dp)) { + Card( + modifier = MaterialTheme.colorScheme.imageModifier, + ) { + Column( + modifier = Modifier.padding(16.dp), + ) { + // Title + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(id = R.string.zap_the_devs_title), + style = + TextStyle( + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + ), + ) + + IconButton( + modifier = Size20Modifier, + onClick = { accountViewModel.markDonatedInThisVersion() }, + ) { + CloseIcon() + } + } + + Spacer(modifier = StdVertSpacer) + + ClickableText( + text = + buildAnnotatedString { + append(stringResource(id = R.string.zap_the_devs_description, BuildConfig.VERSION_NAME)) + append(" ") + withStyle(SpanStyle(color = MaterialTheme.colorScheme.primary)) { + append("#value4value") + } + }, + onClick = { nav("Hashtag/value4value") }, + ) + + Spacer(modifier = StdVertSpacer) + + val noteEvent = releaseNote.event + if (noteEvent != null) { + val route = + remember(releaseNote) { + routeFor(releaseNote, accountViewModel.userProfile()) + } + + if (route != null) { + ClickableText( + text = + buildAnnotatedString { + withStyle(SpanStyle(color = MaterialTheme.colorScheme.primary)) { + append(stringResource(id = R.string.version_name, BuildConfig.VERSION_NAME.substringBefore("-"))) + } + append(" " + stringResource(id = R.string.brought_to_you_by)) + }, + onClick = { nav(route) }, + ) + } else { + Text(stringResource(id = R.string.this_version_brought_to_you_by)) + } + + Spacer(modifier = StdVertSpacer) + + DisplayZapSplits( + noteEvent = noteEvent, + useAuthorIfEmpty = true, + accountViewModel = accountViewModel, + nav = nav, + ) + + Spacer(modifier = StdVertSpacer) + } + + ZapDonationButton( + baseNote = releaseNote, + grayTint = MaterialTheme.colorScheme.onPrimary, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } + } +} + +@Composable +fun ZapDonationButton( + baseNote: Note, + grayTint: Color, + accountViewModel: AccountViewModel, + iconSize: Dp = Size20dp, + iconSizeModifier: Modifier = Size20Modifier, + animationSize: Dp = 14.dp, + nav: (String) -> Unit, +) { + var wantsToZap by remember { mutableStateOf(false) } + var showErrorMessageDialog by remember { mutableStateOf(null) } + var wantsToPay by + remember(baseNote) { + mutableStateOf>( + persistentListOf(), + ) + } + + val context = LocalContext.current + val scope = rememberCoroutineScope() + + var zappingProgress by remember { mutableFloatStateOf(0f) } + var hasZapped by remember { mutableStateOf(false) } + + Button( + onClick = { + zapClick( + baseNote, + accountViewModel, + context, + onZappingProgress = { progress: Float -> + scope.launch { zappingProgress = progress } + }, + onMultipleChoices = { wantsToZap = true }, + onError = { _, message -> + scope.launch { + zappingProgress = 0f + showErrorMessageDialog = message + } + }, + onPayViaIntent = { wantsToPay = it }, + ) + }, + modifier = Modifier.fillMaxWidth(), + ) { + if (wantsToZap) { + ZapAmountChoicePopup( + baseNote = baseNote, + iconSize = iconSize, + accountViewModel = accountViewModel, + onDismiss = { + wantsToZap = false + zappingProgress = 0f + }, + onChangeAmount = { + wantsToZap = false + }, + onError = { _, message -> + scope.launch { + zappingProgress = 0f + showErrorMessageDialog = message + } + }, + onProgress = { + scope.launch(Dispatchers.Main) { zappingProgress = it } + }, + onPayViaIntent = { wantsToPay = it }, + ) + } + + if (showErrorMessageDialog != null) { + ErrorMessageDialog( + title = stringResource(id = R.string.error_dialog_zap_error), + textContent = showErrorMessageDialog ?: "", + onClickStartMessage = { + baseNote.author?.let { + scope.launch(Dispatchers.IO) { + val route = routeToMessage(it, showErrorMessageDialog, accountViewModel) + nav(route) + } + } + }, + onDismiss = { showErrorMessageDialog = null }, + ) + } + + if (wantsToPay.isNotEmpty()) { + PayViaIntentDialog( + payingInvoices = wantsToPay, + accountViewModel = accountViewModel, + onClose = { wantsToPay = persistentListOf() }, + onError = { + wantsToPay = persistentListOf() + scope.launch { + zappingProgress = 0f + showErrorMessageDialog = it + } + }, + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = iconSizeModifier, + ) { + if (zappingProgress > 0.00001 && zappingProgress < 0.99999) { + Spacer(ModifierWidth3dp) + + CircularProgressIndicator( + progress = + animateFloatAsState( + targetValue = zappingProgress, + animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec, + label = "ZapIconIndicator", + ) + .value, + modifier = remember { Modifier.size(animationSize) }, + strokeWidth = 2.dp, + color = grayTint, + ) + } else { + ObserveZapIcon( + baseNote, + accountViewModel, + ) { wasZappedByLoggedInUser -> + LaunchedEffect(wasZappedByLoggedInUser.value) { + hasZapped = wasZappedByLoggedInUser.value + if (wasZappedByLoggedInUser.value && !accountViewModel.account.hasDonatedInThisVersion()) { + delay(1000) + accountViewModel.markDonatedInThisVersion() + } + } + + Crossfade(targetState = wasZappedByLoggedInUser.value, label = "ZapIcon") { + if (it) { + ZappedIcon(iconSizeModifier) + } else { + ZapIcon(iconSizeModifier, grayTint) + } + } + } + } + } + + if (hasZapped) { + Text(text = stringResource(id = R.string.thank_you)) + } else { + Text(text = stringResource(id = R.string.donate_now)) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt index a68e3a719..ca24534e9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt @@ -38,6 +38,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -45,10 +46,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.note.BadgeCompose import com.vitorpamplona.amethyst.ui.note.MessageSetCompose import com.vitorpamplona.amethyst.ui.note.MultiSetCompose import com.vitorpamplona.amethyst.ui.note.NoteCompose +import com.vitorpamplona.amethyst.ui.note.ZapTheDevsCard import com.vitorpamplona.amethyst.ui.note.ZapUserSetCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.FeedPadding @@ -176,6 +179,10 @@ private fun FeedLoaded( contentPadding = FeedPadding, state = listState, ) { + item { + ShowDonationCard(accountViewModel, nav) + } + itemsIndexed(state.feed.value, key = { _, item -> item.id() }) { _, item -> val defaultModifier = remember { Modifier.fillMaxWidth().animateItemPlacement() } @@ -192,6 +199,28 @@ private fun FeedLoaded( } } +@Composable +private fun ShowDonationCard( + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val account by accountViewModel.account.live.observeAsState() + if (account?.account?.hasDonatedInThisVersion() == false) { + LoadNote( + "4d5a05aec61d8798f30f76b2efab81b98d75a03f935fb82823a1080bd56473cd", + accountViewModel, + ) { loadedNoteId -> + if (loadedNoteId != null) { + ZapTheDevsCard( + loadedNoteId, + accountViewModel, + nav, + ) + } + } + } +} + @Composable private fun RenderCardItem( item: Card, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index ae1de817b..36b6f75fb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -564,7 +564,7 @@ fun NoteMaster( val zapSplits = remember(noteEvent) { noteEvent?.hasZapSplitSetup() ?: false } if (zapSplits && noteEvent != null) { Spacer(modifier = DoubleVertSpacer) - DisplayZapSplits(noteEvent, accountViewModel, nav) + DisplayZapSplits(noteEvent, false, accountViewModel, nav) } ReactionsRow(note, true, accountViewModel, nav) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 58b6fd3fb..aca014f52 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -647,6 +647,12 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View account.updateShowSensitiveContent(null) } + fun markDonatedInThisVersion() { + viewModelScope.launch { + account.markDonatedInThisVersion() + } + } + fun defaultZapType(): LnZapEvent.ZapType { return account.defaultZapType } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a84536ad2..24ff0b7f0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -766,4 +766,12 @@ Invalid address Amethyst received a URI to open but that uri was invalid: %1$s + + Zap the Devs! + Your donation helps us make a difference. Every sat counts! + Donate Now + was brought to you by: + This version was brought to you by: + Version %1$s + Thank you! diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/KeyPair.kt b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/KeyPair.kt index d8afcfc3f..e06f3c3dc 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/KeyPair.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/KeyPair.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey class KeyPair( privKey: ByteArray? = null, pubKey: ByteArray? = null, + forcePubKeyCheck: Boolean = true, ) { val privKey: ByteArray? val pubKey: ByteArray @@ -44,7 +45,12 @@ class KeyPair( } else { // as private key is provided, ignore the public key and set keys according to private key this.privKey = privKey - this.pubKey = CryptoUtils.pubkeyCreate(privKey) + + if (pubKey == null || forcePubKeyCheck) { + this.pubKey = CryptoUtils.pubkeyCreate(privKey) + } else { + this.pubKey = pubKey + } } } From a148da4b7ba99d3bc607bd4b6c626ef38b8a73ea Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Tue, 20 Feb 2024 11:40:16 +0000 Subject: [PATCH 112/176] Updated translations. DE, SV, PT, CS --- app/src/main/res/values-cs/strings.xml | 8 ++++++++ app/src/main/res/values-de/strings.xml | 8 ++++++++ app/src/main/res/values-pt-rBR/strings.xml | 8 ++++++++ app/src/main/res/values-sv-rSE/strings.xml | 9 +++++++++ 4 files changed, 33 insertions(+) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index bcf27f042..d71d45496 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -646,4 +646,12 @@ Zobrazit npub jako QR kód Neplatná adresa Amethyst obdržel URI k otevření, ale tento URI byl neplatný: %1$s + + Zapni vývojáře! + Váš příspěvek nám pomáhá dělat rozdíl. Každý sat se počítá! + Přispět nyní + představeno pro vás: + Tato verze byla představena pro vás: + Verze %1$s + Děkujeme! diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index a0270effb..acf38190d 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -651,4 +651,12 @@ anz der Bedingungen ist erforderlich Npub als QR-Code anzeigen Ungültige Adresse Amethyst hat eine URI zum Öffnen erhalten, aber diese URI war ungültig: %1$s + + Zap die Entwickler! + Deine Spende hilft uns, einen Unterschied zu machen. Jeder Sat zählt! + Jetzt spenden + wurde Ihnen präsentiert von: + Diese Version wurde Ihnen präsentiert von: + Version %1$s + Vielen Dank! diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index aeddd23cc..62d0e2322 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -646,4 +646,12 @@ Mostrar npub como um código QR Endereço inválido O Amethyst recebeu um URI para abrir, mas esse URI era inválido: %1$s + + Zap os desenvolvedores! + Sua doação nos ajuda a fazer a diferença. Cada sat conta! + Doar agora + foi trazido a você por: + Esta versão foi trazida a você por: + Versão %1$s + Obrigado! diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index b44e0b4d5..f00cd039c 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -645,4 +645,13 @@ Visa npub som en QR-kod Ogiltig adress Amethyst fick en URI att öppna men den URI var ogiltig: %1$s + + Zappa utvecklarna! + Din donation hjälper oss att göra skillnad. Varje sat räknas! + Donera nu + tillhandahölls av: + Denna version tillhandahölls av: + Version %1$s + Tack! + From c8d11fdcacbe90af2cf315c43b3c6a01fcfa36ba Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Tue, 20 Feb 2024 15:59:43 +0000 Subject: [PATCH 113/176] New Crowdin translations by GitHub Action --- app/src/main/res/values-cs/strings.xml | 1 - app/src/main/res/values-de/strings.xml | 2 -- app/src/main/res/values-fr/strings.xml | 7 +++++++ app/src/main/res/values-pt-rBR/strings.xml | 1 - app/src/main/res/values-sv-rSE/strings.xml | 3 --- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index d71d45496..011afc4e8 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -646,7 +646,6 @@ Zobrazit npub jako QR kód Neplatná adresa Amethyst obdržel URI k otevření, ale tento URI byl neplatný: %1$s - Zapni vývojáře! Váš příspěvek nám pomáhá dělat rozdíl. Každý sat se počítá! Přispět nyní diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index acf38190d..cbea02ebb 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -651,12 +651,10 @@ anz der Bedingungen ist erforderlich Npub als QR-Code anzeigen Ungültige Adresse Amethyst hat eine URI zum Öffnen erhalten, aber diese URI war ungültig: %1$s - Zap die Entwickler! Deine Spende hilft uns, einen Unterschied zu machen. Jeder Sat zählt! Jetzt spenden wurde Ihnen präsentiert von: Diese Version wurde Ihnen präsentiert von: - Version %1$s Vielen Dank! diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index c248787f0..b4ddeb9ea 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -648,4 +648,11 @@ Afficher npub en tant que QR code Adresse invalide Amethyst a reçu une URI à ouvrir mais cette URI était invalide : %1$s + Zap les Devs ! + Votre don nous aide à faire la différence. Chaque sat compte ! + Faire un don maintenant + vous a été apporté par : + Cette version vous a été apportée par : + Version %1$s + Merci ! diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 62d0e2322..5e07db2e5 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -646,7 +646,6 @@ Mostrar npub como um código QR Endereço inválido O Amethyst recebeu um URI para abrir, mas esse URI era inválido: %1$s - Zap os desenvolvedores! Sua doação nos ajuda a fazer a diferença. Cada sat conta! Doar agora diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index f00cd039c..7c6774fa7 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -645,13 +645,10 @@ Visa npub som en QR-kod Ogiltig adress Amethyst fick en URI att öppna men den URI var ogiltig: %1$s - Zappa utvecklarna! Din donation hjälper oss att göra skillnad. Varje sat räknas! Donera nu tillhandahölls av: Denna version tillhandahölls av: - Version %1$s Tack! - From a03f9d2b9842ceccce16b32004cbb52649829e52 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 20 Feb 2024 11:30:11 -0500 Subject: [PATCH 114/176] Fixes position of the notification dot --- .../com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt index 532a5bb60..102a143eb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt @@ -26,6 +26,7 @@ import android.view.ViewTreeObserver import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.size import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -53,7 +54,7 @@ import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Font12SP import com.vitorpamplona.amethyst.ui.theme.Size0dp -import com.vitorpamplona.amethyst.ui.theme.Size10Modifier +import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.bottomIconModifier import kotlinx.collections.immutable.persistentListOf @@ -200,7 +201,7 @@ fun AddNotifIconIfNeeded( @Composable private fun NotificationDotIcon(modifier: Modifier) { - Box(Size10Modifier) { + Box(modifier.size(Size10dp)) { Box( modifier = MaterialTheme.colorScheme.bottomIconModifier, contentAlignment = Alignment.TopEnd, From 0d44c44c6e5015a781bde69831a4e034d22567d5 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 20 Feb 2024 11:30:27 -0500 Subject: [PATCH 115/176] Adds release note ID to gradle. --- app/build.gradle | 1 + .../java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index d9932b68d..7f6f5be40 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,6 +14,7 @@ android { targetSdk 34 versionCode 358 versionName "0.84.3" + buildConfigField "String", "RELEASE_NOTES_ID", "\"4d5a05aec61d8798f30f76b2efab81b98d75a03f935fb82823a1080bd56473cd\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt index ca24534e9..2d24b634c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt @@ -46,6 +46,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.note.BadgeCompose import com.vitorpamplona.amethyst.ui.note.MessageSetCompose @@ -207,7 +208,7 @@ private fun ShowDonationCard( val account by accountViewModel.account.live.observeAsState() if (account?.account?.hasDonatedInThisVersion() == false) { LoadNote( - "4d5a05aec61d8798f30f76b2efab81b98d75a03f935fb82823a1080bd56473cd", + BuildConfig.RELEASE_NOTES_ID, accountViewModel, ) { loadedNoteId -> if (loadedNoteId != null) { From ae291d278fd41e73b3967829b0d1d78e4b4e36e9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 20 Feb 2024 12:28:46 -0500 Subject: [PATCH 116/176] Fixes text's vertical alignment when emoji's are present. --- .../amethyst/ui/components/ClickableRoute.kt | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt index 93fc8ee20..1bf1cfe03 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.components import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.appendInlineContent import androidx.compose.material3.LocalContentColor @@ -566,24 +565,29 @@ fun CreateClickableTextWithEmoji( if (emojiLists == null) { CreateClickableText(clickablePart, suffix, maxLines, overrideColor, fontWeight, route, nav) } else { + val clickablePartStyle = + SpanStyle( + color = overrideColor ?: MaterialTheme.colorScheme.primary, + fontWeight = fontWeight, + ) + + val nonClickablePartStyle = + SpanStyle( + color = overrideColor ?: MaterialTheme.colorScheme.onBackground, + fontWeight = fontWeight, + ) + ClickableInLineIconRenderer( emojiLists!!.part1, maxLines, - LocalTextStyle.current - .copy(color = overrideColor ?: MaterialTheme.colorScheme.primary, fontWeight = fontWeight) - .toSpanStyle(), + clickablePartStyle, ) { nav(route) } InLineIconRenderer( emojiLists!!.part2, - LocalTextStyle.current - .copy( - color = overrideColor ?: MaterialTheme.colorScheme.onBackground, - fontWeight = fontWeight, - ) - .toSpanStyle(), + nonClickablePartStyle, maxLines = maxLines, ) } @@ -677,7 +681,7 @@ fun ClickableInLineIconRenderer( } } - BasicText( + Text( text = annotatedText, modifier = pressIndicator, inlineContent = inlineContent, From 444857ad959ca249e2fffdcabe55b7f6290ebbe6 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 20 Feb 2024 15:52:25 -0500 Subject: [PATCH 117/176] Updating Vico --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f3c3da5aa..ea84488d3 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { room_version = "2.4.3" accompanist_version = '0.34.0' coil_version = '2.5.0' - vico_version = '1.13.1' + vico_version = '1.14.0' media3_version = '1.2.1' core_ktx_version = '1.12.0' material3_version = '1.2.0' From f4cee92702611dddf945ed58b13909272ee6685c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 20 Feb 2024 16:51:18 -0500 Subject: [PATCH 118/176] Fixes the crash on images not being present in the image pager. --- .../amethyst/ui/components/LoadUrlPreview.kt | 11 ++--- .../ui/components/ZoomableContentView.kt | 45 ++++++++++--------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt index 82441101c..9710607e2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt @@ -32,7 +32,6 @@ import androidx.compose.runtime.setValue import com.vitorpamplona.amethyst.model.UrlCachedPreviewer import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding -import kotlinx.collections.immutable.persistentListOf @Composable fun LoadUrlPreview( @@ -68,19 +67,17 @@ fun LoadUrlPreview( if (state.previewInfo.mimeType.type == "image") { Box(modifier = HalfVertPadding) { ZoomableContentView( - ZoomableUrlImage(url), - persistentListOf(), + content = ZoomableUrlImage(url), roundedCorner = true, - accountViewModel, + accountViewModel = accountViewModel, ) } } else if (state.previewInfo.mimeType.type == "video") { Box(modifier = HalfVertPadding) { ZoomableContentView( - ZoomableUrlVideo(url), - persistentListOf(), + content = ZoomableUrlVideo(url), roundedCorner = true, - accountViewModel, + accountViewModel = accountViewModel, ) } } else { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index c2c01f147..50a308be7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -229,12 +229,12 @@ fun figureOutMimeType(fullUrl: String): ZoomableContent { @OptIn(ExperimentalFoundationApi::class) fun ZoomableContentView( content: ZoomableContent, - images: ImmutableList = listOf(content).toImmutableList(), + images: ImmutableList = remember(content) { listOf(content).toImmutableList() }, roundedCorner: Boolean, accountViewModel: AccountViewModel, ) { // store the dialog open or close state - var dialogOpen by remember { mutableStateOf(false) } + var dialogOpen by remember(content) { mutableStateOf(false) } // store the dialog open or close state val shareOpen = remember { mutableStateOf(false) } @@ -847,14 +847,16 @@ private fun DialogContent( SlidingCarousel( pagerState = pagerState, ) { index -> - RenderImageOrVideo( - content = allImages[index], - roundedCorner = false, - topPaddingForControllers = Size55dp, - onControllerVisibilityChanged = { controllerVisible.value = it }, - onToggleControllerVisibility = { controllerVisible.value = !controllerVisible.value }, - accountViewModel = accountViewModel, - ) + allImages.getOrNull(index)?.let { + RenderImageOrVideo( + content = it, + roundedCorner = false, + topPaddingForControllers = Size55dp, + onControllerVisibilityChanged = { controllerVisible.value = it }, + onToggleControllerVisibility = { controllerVisible.value = !controllerVisible.value }, + accountViewModel = accountViewModel, + ) + } } } else { RenderImageOrVideo( @@ -879,18 +881,19 @@ private fun DialogContent( ) { CloseButton(onPress = onDismiss) - val myContent = allImages[pagerState.currentPage] - if (myContent is ZoomableUrlContent) { - Row { - CopyToClipboard(content = myContent) - Spacer(modifier = StdHorzSpacer) - SaveToGallery(url = myContent.url) + allImages.getOrNull(pagerState.currentPage)?.let { myContent -> + if (myContent is ZoomableUrlContent) { + Row { + CopyToClipboard(content = myContent) + Spacer(modifier = StdHorzSpacer) + SaveToGallery(url = myContent.url) + } + } else if (myContent is ZoomableLocalImage && myContent.localFile != null) { + SaveToGallery( + localFile = myContent.localFile, + mimeType = myContent.mimeType, + ) } - } else if (myContent is ZoomableLocalImage && myContent.localFile != null) { - SaveToGallery( - localFile = myContent.localFile, - mimeType = myContent.mimeType, - ) } } } From dbcf59779e803a0ee3f09079cb0c1f01e223db70 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 21 Feb 2024 09:59:39 -0500 Subject: [PATCH 119/176] Fix benchmark test --- .../quartz/benchmark/EventBenchmark.kt | 126 ++++++++++-------- 1 file changed, 72 insertions(+), 54 deletions(-) diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventBenchmark.kt index 60ad22c9c..b20d501f4 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventBenchmark.kt @@ -40,68 +40,86 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class EventBenchmark { val payload1 = - """[ - "EVENT", - "40b9", - { - "id":"48a72b485d38338627ec9d427583551f9af4f016c739b8ec0d6313540a8b12cf", - "kind":1, - "pubkey":"3d842afecd5e293f28b6627933704a3fb8ce153aa91d790ab11f6a752d44a42d", - "created_at":1677940007, - "content":"I got asked about follower count again today. Why does my follower - count go down when I delete public relays (in our list) and replace them with - filter.nostr.wine? \\n\\nI’ll give you one final explanation to rule them all. - First, let’s go over how clients calculate your follower count.\\n\\n1. Your - client sends a request to all your connected relays asking for accounts who - follow you\\n2. Relays answer back with the events requested\\n3. The client - aggregates the event total and displays it\\n\\nEach relay has a set limit on - how many stored events it will return per request. For some relays it’s 500, - others 1000, some as high as 5000. Let’s say for simplicity that all your - public relays use 500 as their limit. If you ask 10 relays for your followers - the max possible answer you can get is 5000. That won’t change if you have - 20,000 followers or 100,000. You may get back a “different” 5000 each time, - but you’ll still cap out at 5000 because that is the most events your client - will receive.\u2028\u2028Our limit on filter.nostr.wine is 2000 events. If you - replace 10 public relays with only filter.nostr.wine, the MOST followers you - will ever get back from our filter relay is 2000. That doesn’t mean you only - have 2000 followers or that your reach is reduced in any way.\\n\\nAs long as - you are writing to and reading from the same public relays, neither your reach - nor any content was lost. That concludes my TED talk. I hope you all have a - fantastic day and weekend.", - "tags":[ - - ], - "sig":"dcaf8ab98bb9179017b35bd814092850d1062b26c263dff89fb1ae8c019a324139d1729012d9d05ff0a517f76b1117d869b2cc7d36bea8aa5f4b94c5e2548aa8" - } -]""" + "[\"EVENT\",\"40b9\",{\"id\":\"48a72b485d38338627ec9d427583551f9af4f016c739b8ec0d6313540a8b12cf\"," + + "\"kind\":1,\"pubkey\":\"3d842afecd5e293f28b6627933704a3fb8ce153aa91d790ab11f6a752d44a42d\"," + + "\"created_at\":1677940007,\"content\":" + + "\"I got asked about follower count again today. Why does my follower count go down when " + + "I delete public relays (in our list) and replace them with filter.nostr.wine? \\n\\nI’ll " + + "give you one final explanation to rule them all. First, let’s go over how clients calculate " + + "your follower count.\\n\\n1. Your client sends a request to all your connected relays asking " + + "for accounts who follow you\\n2. Relays answer back with the events requested\\n3. The client " + + "aggregates the event total and displays it\\n\\nEach relay has a set limit on how many stored " + + "events it will return per request. For some relays it’s 500, others 1000, some as high as 5000. " + + "Let’s say for simplicity that all your public relays use 500 as their limit. If you ask 10 " + + "relays for your followers the max possible answer you can get is 5000. That won’t change if " + + "you have 20,000 followers or 100,000. You may get back a “different” 5000 each time, but you’ll " + + "still cap out at 5000 because that is the most events your client will receive.\u2028\u2028Our " + + "limit on filter.nostr.wine is 2000 events. If you replace 10 public relays with only " + + "filter.nostr.wine, the MOST followers you will ever get back from our filter relay is 2000. " + + "That doesn’t mean you only have 2000 followers or that your reach is reduced in any way.\\n\\nAs " + + "long as you are writing to and reading from the same public relays, neither your reach nor any " + + "content was lost. That concludes my TED talk. I hope you all have a fantastic day and weekend.\"," + + "\"tags\":[],\"sig\":\"dcaf8ab98bb9179017b35bd814092850d1062b26c263dff89fb1ae8c019a324139d1729012d" + + "9d05ff0a517f76b1117d869b2cc7d36bea8aa5f4b94c5e2548aa8\"}]" val payload2 = - """{ - "content": "Astral:\n\nhttps://void.cat/d/A5Fba5B1bcxwEmeyoD9nBs.webp\n\n - Iris:\n\nhttps://void.cat/d/44hTcVvhRps6xYYs99QsqA.webp\n\n - Snort:\n\nhttps://void.cat/d/4nJD5TRePuQChM5tzteYbU.webp\n\n - Amethyst agrees with Astral which I suspect are both wrong. - nostr:npub13sx6fp3pxq5rl70x0kyfmunyzaa9pzt5utltjm0p8xqyafndv95q3saapa - nostr:npub1v0lxxxxutpvrelsksy8cdhgfux9l6a42hsj2qzquu2zk7vc9qnkszrqj49 - nostr:npub1g53mukxnjkcmr94fhryzkqutdz2ukq4ks0gvy5af25rgmwsl4ngq43drvk - nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z ", + """ + { + "content": "Astral:\n\nhttps://void.cat/d/A5Fba5B1bcxwEmeyoD9nBs.webp\n\nIris:\n\nhttps://void.cat/d/44hTcVvhRps6xYYs99QsqA.webp\n\nSnort:\n\nhttps://void.cat/d/4nJD5TRePuQChM5tzteYbU.webp\n\nAmethyst agrees with Astral which I suspect are both wrong. nostr:npub13sx6fp3pxq5rl70x0kyfmunyzaa9pzt5utltjm0p8xqyafndv95q3saapa nostr:npub1v0lxxxxutpvrelsksy8cdhgfux9l6a42hsj2qzquu2zk7vc9qnkszrqj49 nostr:npub1g53mukxnjkcmr94fhryzkqutdz2ukq4ks0gvy5af25rgmwsl4ngq43drvk nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z ", "created_at": 1683596206, "id": "98b574c3527f0ffb30b7271084e3f07480733c7289f8de424d29eae82e36c758", "kind": 1, "pubkey": "46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d", "sig": "4aa5264965018fa12a326686ad3d3bd8beae3218dcc83689b19ca1e6baeb791531943c15363aa6707c7c0c8b2d601deca1f20c32078b2872d356cdca03b04cce", "tags": [ - ["e","27ac621d7dc4a932e1a79f984308e7d20656dd6fddb2ce9cdfcb6a67b9a7bcc3","","root"], - ["e","be7245af96210a0dd048cab4ad38e52dbd6c09a53ea21a7edb6be8898e5727cc","","reply"], - ["p","22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954"], - ["p","22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954"], - ["p","3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24"], - ["p","ec4d241c334311b3a304433ee3442be29d0e88e7ec19b85edf2bba29b93565e2"], - ["p","0fe0b18b4dbf0e0aa40fcd47209b2a49b3431fc453b460efcf45ca0bd16bd6ac"], - ["p","8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168"], - ["p","63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed"], - ["p","4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0"], - ["p","460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"] + [ + "e", + "27ac621d7dc4a932e1a79f984308e7d20656dd6fddb2ce9cdfcb6a67b9a7bcc3", + "", + "root" + ], + [ + "e", + "be7245af96210a0dd048cab4ad38e52dbd6c09a53ea21a7edb6be8898e5727cc", + "", + "reply" + ], + [ + "p", + "22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954" + ], + [ + "p", + "22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954" + ], + [ + "p", + "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24" + ], + [ + "p", + "ec4d241c334311b3a304433ee3442be29d0e88e7ec19b85edf2bba29b93565e2" + ], + [ + "p", + "0fe0b18b4dbf0e0aa40fcd47209b2a49b3431fc453b460efcf45ca0bd16bd6ac" + ], + [ + "p", + "8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168" + ], + [ + "p", + "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed" + ], + [ + "p", + "4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0" + ], + [ + "p", + "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c" + ] ], "seenOn": [ "wss://nostr.wine/" From 74b1cad578c6e72048665825408a0bbe20a9201a Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Wed, 21 Feb 2024 15:02:57 +0000 Subject: [PATCH 120/176] New Crowdin translations by GitHub Action --- app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values-sv-rSE/strings.xml | 1 + 2 files changed, 2 insertions(+) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index cbea02ebb..cacb0ce9a 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -656,5 +656,6 @@ anz der Bedingungen ist erforderlich Jetzt spenden wurde Ihnen präsentiert von: Diese Version wurde Ihnen präsentiert von: + Version %1$s Vielen Dank! diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 7c6774fa7..7ced34d33 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -650,5 +650,6 @@ Donera nu tillhandahölls av: Denna version tillhandahölls av: + Version %1$s Tack! From f10dd38126d90bf5ed7077af3510c3b14cad955e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 21 Feb 2024 10:04:40 -0500 Subject: [PATCH 121/176] refactoring on method names to make them clearer --- .../java/com/vitorpamplona/amethyst/service/relays/Client.kt | 4 ++-- .../java/com/vitorpamplona/amethyst/service/relays/Relay.kt | 2 +- .../com/vitorpamplona/amethyst/service/relays/RelayPool.kt | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt index 3f0283fc7..4bf85b83a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt @@ -108,7 +108,7 @@ object Client : RelayPool.Listener { checkNotInMainThread() subscriptions = subscriptions + Pair(subscriptionId, filters) - RelayPool.sendFilterOnlyIfDisconnected(subscriptionId) + RelayPool.connectAndSendFiltersIfDisconnected() } fun send( @@ -292,7 +292,7 @@ object Client : RelayPool.Listener { open fun onRelayStateChange( type: Relay.StateType, relay: Relay, - channel: String?, + subscriptionId: String?, ) = Unit /** When an relay saves or rejects a new event. */ diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt index cdb428eae..04a6aaaa2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt @@ -409,7 +409,7 @@ class Relay( return buffer.toString() } - fun sendFilterOnlyIfDisconnected(subscriptionId: String) { + fun connectAndSendFiltersIfDisconnected() { checkNotInMainThread() if (socket == null) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt index 89fe955be..08135e9bb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt @@ -81,8 +81,8 @@ object RelayPool : Relay.Listener { relays.forEach { it.sendFilter(subscriptionId) } } - fun sendFilterOnlyIfDisconnected(subscriptionId: String) { - relays.forEach { it.sendFilterOnlyIfDisconnected(subscriptionId) } + fun connectAndSendFiltersIfDisconnected() { + relays.forEach { it.connectAndSendFiltersIfDisconnected() } } fun sendToSelectedRelays( From d982f029e454ece59729a69f9513c71d5f65d636 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 21 Feb 2024 10:22:20 -0500 Subject: [PATCH 122/176] Fixes Ukrainian translation --- app/src/main/res/values-uk/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 45f7def09..c966009bc 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -195,7 +195,7 @@ Скопіювати мій приватний ключ Помилка входу Не вдалося виконати біометрику для автентифікації власника цього телефону - Не вдалося виконати біометрику для автентифікації власника цього телефону + Не вдалося виконати біометрику для автентифікації власника цього телефону. Помилка: %1$s Помилка "Створено %1$s" "Картинка значка-нагороди %1$s" From 5cd1c5ab90fc825d9ed27906123f516fe06bd523 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 21 Feb 2024 11:23:14 -0500 Subject: [PATCH 123/176] Faster hex validation --- .../quartz/benchmark/HexBenchmark.kt | 6 +++++ .../vitorpamplona/quartz/encoders/HexUtils.kt | 26 +++---------------- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt index 7a8e0331c..8a4744cde 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.benchmark import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.encoders.HexValidator import junit.framework.TestCase.assertEquals import org.junit.Rule import org.junit.Test @@ -65,4 +66,9 @@ class HexBenchmark { benchmarkRule.measureRepeated { assertEquals(testHex, fr.acinq.secp256k1.Hex.encode(bytes)) } } + + @Test + fun isHex() { + benchmarkRule.measureRepeated { HexValidator.isHex(testHex) } + } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/HexUtils.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/HexUtils.kt index b995bd6ba..ec7025ad5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/HexUtils.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/HexUtils.kt @@ -34,29 +34,9 @@ fun HexKey.hexToByteArray(): ByteArray { object HexValidator { private fun isHexChar(c: Char): Boolean { return when (c) { - '0', - '1', - '2', - '3', - '4', - '5', - '6', - '7', - '8', - '9', - 'a', - 'b', - 'c', - 'd', - 'e', - 'f', - 'A', - 'B', - 'C', - 'D', - 'E', - 'F', - -> true + in '0'..'9' -> true + in 'a'..'f' -> true + in 'A'..'F' -> true else -> false } } From e3f3350c85c625a42a3bee3171cc3786c91b9be5 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 21 Feb 2024 11:24:51 -0500 Subject: [PATCH 124/176] Fixes Robohash test --- .../quartz/benchmark/RobohashBenchmark.kt | 270 ++++++++++-------- 1 file changed, 147 insertions(+), 123 deletions(-) diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/RobohashBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/RobohashBenchmark.kt index 79fe0f293..2f0b781d0 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/RobohashBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/RobohashBenchmark.kt @@ -42,129 +42,153 @@ class RobohashBenchmark { val warmHex = "f4f016c739b8ec0d6313540a8b12cf48a72b485d38338627ec9d427583551f9a" val testHex = "48a72b485d38338627ec9d427583551f9af4f016c739b8ec0d6313540a8b12cf" val resultingSVG = - """ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - """ - .trimIndent() + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" @Test fun createSVG() { From 0429c4dc3aa75d7426f4f50d48f3f39a3adab9f7 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 21 Feb 2024 11:25:08 -0500 Subject: [PATCH 125/176] Faster comparisons on GiftWrap Benchmarks --- .../vitorpamplona/quartz/benchmark/GiftWrapBenchmark.kt | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapBenchmark.kt index 53848c4b5..9bab1384e 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapBenchmark.kt @@ -76,12 +76,7 @@ class GiftWrapBenchmark { expectedLength, events!! .wraps - .map { - println("TEST ${it.toJson()}") - it.toJson() - } - .joinToString("") - .length, + .sumOf { it.toJson().length }, ) // Simulate Receiver From 4b9a55e178a730196d4a614c2eef84f3dd06eec0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 21 Feb 2024 13:51:27 -0500 Subject: [PATCH 126/176] Increasing the speed of the Robohash SVG to buffer function --- .../amethyst/ui/components/Robohash.kt | 13 ++-- .../benchmark/RobohashBenchmark.kt | 31 ++++++++-- commons/build.gradle | 2 + .../amethyst/commons}/Robohash.kt | 61 +++++++++---------- 4 files changed, 64 insertions(+), 43 deletions(-) rename benchmark/src/androidTest/java/com/vitorpamplona/{quartz => amethyst}/benchmark/RobohashBenchmark.kt (95%) rename {quartz/src/main/java/com/vitorpamplona/quartz/utils => commons/src/main/java/com/vitorpamplona/amethyst/commons}/Robohash.kt (98%) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/Robohash.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/Robohash.kt index ca8680946..7262646a3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/Robohash.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/Robohash.kt @@ -30,9 +30,10 @@ import coil.fetch.Fetcher import coil.fetch.SourceResult import coil.request.ImageRequest import coil.request.Options +import com.vitorpamplona.amethyst.commons.Robohash import com.vitorpamplona.amethyst.service.checkNotInMainThread -import com.vitorpamplona.quartz.utils.Robohash -import okio.Buffer +import okio.buffer +import okio.source import java.nio.charset.Charset @Stable @@ -43,14 +44,10 @@ class HashImageFetcher( ) : Fetcher { override suspend fun fetch(): SourceResult { checkNotInMainThread() + val source = try { - val buffer = Buffer() - buffer.writeString( - Robohash.assemble(data.toString(), isLightTheme), - Charset.defaultCharset(), - ) - buffer + Robohash.assemble(data.toString(), isLightTheme).byteInputStream(Charset.defaultCharset()).source().buffer() } finally { } diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/RobohashBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/RobohashBenchmark.kt similarity index 95% rename from benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/RobohashBenchmark.kt rename to benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/RobohashBenchmark.kt index 2f0b781d0..fe35c01d1 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/RobohashBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/RobohashBenchmark.kt @@ -18,16 +18,20 @@ * 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.benchmark +package com.vitorpamplona.amethyst.benchmark import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.vitorpamplona.quartz.utils.Robohash +import com.vitorpamplona.amethyst.commons.Robohash import junit.framework.TestCase.assertEquals +import okio.Buffer +import okio.buffer +import okio.source import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import java.nio.charset.Charset /** * Benchmark, which will execute on an Android device. @@ -41,7 +45,7 @@ class RobohashBenchmark { val warmHex = "f4f016c739b8ec0d6313540a8b12cf48a72b485d38338627ec9d427583551f9a" val testHex = "48a72b485d38338627ec9d427583551f9af4f016c739b8ec0d6313540a8b12cf" - val resultingSVG = + val expectedTestSVG = "" + "" + "Get it on Obtaininum | tr -d '\n' | tee some_signing_key.j 7. Add your CHANGE LOG to the description of the new release 8. Download the `aab` file and upload it to the PlayStore. +## Using the Quartz library + +Setup [JitPack.io](https://jitpack.io/#vitorpamplona/amethyst/v0.84.3) to your build file + +Add `maven { url 'https://jitpack.io' }` to settings.gradle at the end of repositories: + +```gradle +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + mavenCentral() + maven { url 'https://jitpack.io' } + } +} +``` + +Add the dependency + +```gradle +implementation('com.github.vitorpamplona.amethyst:quartz:v0.84.3') { + exclude group: 'net.java.dev.jna' +} +``` + ## Contributing [Issues](https://github.com/vitorpamplona/amethyst/issues) and [pull requests](https://github.com/vitorpamplona/amethyst/pulls) here are very welcome. Translations can be provided via [Crowdin](https://crowdin.com/project/amethyst-social) From 3eac579638962f674101f7f6cbb09cb6517dc67f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 23 Feb 2024 14:29:14 -0500 Subject: [PATCH 144/176] Fixing Format --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9fa7fdf4e..bc446f4c6 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ # Amethyst -### Nostr Client for Android +## Nostr Client for Android Join the social network you control. @@ -18,7 +18,7 @@ Join the social network you control. [![CI](https://img.shields.io/github/actions/workflow/status/vitorpamplona/amethyst/build.yml?labelColor=27303D)](https://github.com/vitorpamplona/amethyst/actions/workflows/build.yml) [![License: Apache-2.0](https://img.shields.io/github/license/vitorpamplona/amethyst?labelColor=27303D&color=0877d2)](/LICENSE) -## Install +## Download and Install [Get it on Obtaininum Date: Fri, 23 Feb 2024 16:59:47 -0500 Subject: [PATCH 145/176] Updates Coil and Google Service dependencies --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index ea84488d3..e72a837d7 100644 --- a/build.gradle +++ b/build.gradle @@ -8,14 +8,14 @@ buildscript { nav_version = '2.7.7' room_version = "2.4.3" accompanist_version = '0.34.0' - coil_version = '2.5.0' + coil_version = '2.6.0' vico_version = '1.14.0' media3_version = '1.2.1' core_ktx_version = '1.12.0' material3_version = '1.2.0' } dependencies { - classpath 'com.google.gms:google-services:4.4.0' + classpath 'com.google.gms:google-services:4.4.1' } } From 173e82d236603608fd72c1b7ca429070d887e470 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 23 Feb 2024 17:55:39 -0500 Subject: [PATCH 146/176] Adds Wikipage Kind --- .../amethyst/model/LocalCache.kt | 37 +++++ .../amethyst/ui/note/NoteCompose.kt | 101 +++++++++++- .../amethyst/ui/screen/ThreadFeedView.kt | 148 ++++++++++++++++++ app/src/main/res/values/strings.xml | 1 + .../quartz/events/BaseTextNoteEvent.kt | 7 +- .../quartz/events/EventFactory.kt | 1 + .../quartz/events/WikiNoteEvent.kt | 87 ++++++++++ 7 files changed, 378 insertions(+), 4 deletions(-) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/WikiNoteEvent.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 5b88fb9f4..3214a11b2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -96,6 +96,7 @@ import com.vitorpamplona.quartz.events.StatusEvent import com.vitorpamplona.quartz.events.TextNoteEvent import com.vitorpamplona.quartz.events.VideoHorizontalEvent import com.vitorpamplona.quartz.events.VideoVerticalEvent +import com.vitorpamplona.quartz.events.WikiNoteEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toImmutableList @@ -395,6 +396,41 @@ object LocalCache { } } + fun consume( + event: WikiNoteEvent, + relay: Relay?, + ) { + val version = getOrCreateNote(event.id) + val note = getOrCreateAddressableNote(event.address()) + val author = getOrCreateUser(event.pubKey) + + if (version.event == null) { + version.loadEvent(event, author, emptyList()) + version.moveAllReferencesTo(note) + } + + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event?.id() == event.id()) return + + if (antiSpam.isSpam(event, relay)) { + relay?.let { it.spamCounter++ } + return + } + + val replyTo = event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } + + if (event.createdAt > (note.createdAt() ?: 0)) { + note.loadEvent(event, author, replyTo) + + refreshObservers(note) + } + } + fun consume( event: PollNoteEvent, relay: Relay? = null, @@ -1848,6 +1884,7 @@ object LocalCache { is TextNoteEvent -> consume(event, relay) is VideoHorizontalEvent -> consume(event, relay) is VideoVerticalEvent -> consume(event, relay) + is WikiNoteEvent -> consume(event, relay) else -> { Log.w("Event Not Supported", event.toJson()) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index db4e2a79d..e25618332 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -225,6 +225,7 @@ import com.vitorpamplona.quartz.events.UserMetadata import com.vitorpamplona.quartz.events.VideoEvent import com.vitorpamplona.quartz.events.VideoHorizontalEvent import com.vitorpamplona.quartz.events.VideoVerticalEvent +import com.vitorpamplona.quartz.events.WikiNoteEvent import com.vitorpamplona.quartz.events.toImmutableListOfLists import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -1160,6 +1161,9 @@ private fun RenderNoteRow( is LongTextNoteEvent -> { RenderLongFormContent(baseNote, accountViewModel, nav) } + is WikiNoteEvent -> { + RenderWikiContent(baseNote, accountViewModel, nav) + } is BadgeAwardEvent -> { RenderBadgeAward(baseNote, backgroundColor, accountViewModel, nav) } @@ -2916,12 +2920,19 @@ private fun LoadAndDisplayUrl(url: String) { } @Composable -private fun LoadAndDisplayUser( +fun LoadAndDisplayUser( userBase: User, nav: (String) -> Unit, ) { - val route = remember { "User/${userBase.pubkeyHex}" } + LoadAndDisplayUser(userBase, "User/${userBase.pubkeyHex}", nav) +} +@Composable +fun LoadAndDisplayUser( + userBase: User, + route: String, + nav: (String) -> Unit, +) { val userState by userBase.live().metadata.observeAsState() val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() } val userTags = @@ -3660,6 +3671,92 @@ private fun LongFormHeader( } summary?.let { + Spacer(modifier = StdVertSpacer) + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + modifier = + Modifier + .fillMaxWidth() + .padding(start = 10.dp, end = 10.dp, bottom = 10.dp), + color = Color.Gray, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + } + } + } +} + +@Composable +private fun RenderWikiContent( + note: Note, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteEvent = note.event as? WikiNoteEvent ?: return + + WikiNoteHeader(noteEvent, note, accountViewModel, nav) +} + +@Composable +private fun WikiNoteHeader( + noteEvent: WikiNoteEvent, + note: Note, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val title = remember(noteEvent) { noteEvent.title() } + val forkedAddress = remember(noteEvent) { noteEvent.forkFromAddress() } + val summary = + remember(noteEvent) { + noteEvent.summary()?.ifBlank { null } ?: noteEvent.content.take(200).ifBlank { null } + } + val image = remember(noteEvent) { noteEvent.image() } + + Row( + modifier = + Modifier + .padding(top = Size5dp) + .clip(shape = QuoteBorder) + .border( + 1.dp, + MaterialTheme.colorScheme.subtleBorder, + QuoteBorder, + ), + ) { + Column { + val automaticallyShowUrlPreview = remember { accountViewModel.settings.showUrlPreview.value } + + if (automaticallyShowUrlPreview) { + image?.let { + AsyncImage( + model = it, + contentDescription = + stringResource( + R.string.preview_card_image_for, + it, + ), + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth(), + ) + } + ?: CreateImageHeader(note, accountViewModel) + } + + title?.let { + Text( + text = it, + style = MaterialTheme.typography.bodyLarge, + modifier = + Modifier + .fillMaxWidth() + .padding(start = 10.dp, end = 10.dp, top = 10.dp), + ) + } + + summary?.let { + Spacer(modifier = StdVertSpacer) Text( text = it, style = MaterialTheme.typography.bodySmall, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index 36b6f75fb..a2e7ff250 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -76,21 +76,27 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.InlineCarrousel +import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status +import com.vitorpamplona.amethyst.ui.components.mockAccountViewModel import com.vitorpamplona.amethyst.ui.elements.DisplayFollowingCommunityInPost import com.vitorpamplona.amethyst.ui.elements.DisplayFollowingHashtagsInPost import com.vitorpamplona.amethyst.ui.elements.DisplayPoW import com.vitorpamplona.amethyst.ui.elements.DisplayReward import com.vitorpamplona.amethyst.ui.elements.DisplayZapSplits import com.vitorpamplona.amethyst.ui.elements.Reward +import com.vitorpamplona.amethyst.ui.navigation.routeFor import com.vitorpamplona.amethyst.ui.navigation.routeToMessage import com.vitorpamplona.amethyst.ui.note.AudioHeader import com.vitorpamplona.amethyst.ui.note.AudioTrackHeader @@ -104,6 +110,8 @@ import com.vitorpamplona.amethyst.ui.note.DisplayRelaySet import com.vitorpamplona.amethyst.ui.note.FileHeaderDisplay import com.vitorpamplona.amethyst.ui.note.FileStorageHeaderDisplay import com.vitorpamplona.amethyst.ui.note.HiddenNote +import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote +import com.vitorpamplona.amethyst.ui.note.LoadAndDisplayUser import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.NoteDropDownMenu @@ -131,6 +139,7 @@ import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.Size15Modifier import com.vitorpamplona.amethyst.ui.theme.Size24Modifier import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.lessImportantLink import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.selectedNote @@ -144,6 +153,7 @@ import com.vitorpamplona.quartz.events.ClassifiedsEvent import com.vitorpamplona.quartz.events.CommunityDefinitionEvent import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent import com.vitorpamplona.quartz.events.EmojiPackEvent +import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent import com.vitorpamplona.quartz.events.GenericRepostEvent @@ -155,11 +165,13 @@ import com.vitorpamplona.quartz.events.PollNoteEvent import com.vitorpamplona.quartz.events.RelaySetEvent import com.vitorpamplona.quartz.events.RepostEvent import com.vitorpamplona.quartz.events.VideoEvent +import com.vitorpamplona.quartz.events.WikiNoteEvent import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext @Composable @@ -444,6 +456,8 @@ fun NoteMaster( BadgeDisplay(baseNote = note) } else if (noteEvent is LongTextNoteEvent) { RenderLongFormHeaderForThread(noteEvent) + } else if (noteEvent is WikiNoteEvent) { + RenderWikiHeaderForThread(noteEvent, accountViewModel, nav) } else if (noteEvent is ClassifiedsEvent) { RenderClassifiedsReaderForThread(noteEvent, note, accountViewModel, nav) } @@ -797,3 +811,137 @@ private fun RenderLongFormHeaderForThread(noteEvent: LongTextNoteEvent) { } } } + +@Preview +@Composable +private fun RenderWikiHeaderForThreadPreview() { + val event = Event.fromJson("{\"id\":\"277f982a4cd3f67cc47ad9282176acabee1713848f547d6021e0c155572078e1\",\"pubkey\":\"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c\",\"created_at\":1708695717,\"kind\":30818,\"tags\":[[\"d\",\"amethyst\"],[\"a\",\"30818:f03e7c5262648e0b7823dfb49f8f17309cfec9cb14711413dcabdf3d7fc6369a:amethyst\",\"wss://relay.nostr.band\",\"fork\"],[\"e\",\"ceabc60c8022c472c727aa25ae7691885964366386ce265c47e5a78be6cb00be\",\"wss://relay.nostr.band\",\"fork\"],[\"title\",\"Amethyst\"],[\"published_at\",\"1708707133\"]],\"content\":\"An Android-only app written in Kotlin with support for over 90 event kinds. \\n\\n![](https://play-lh.googleusercontent.com/lvZlAm9dBrpHeOo7sIPKCsiKOLYLhR2b0FiOT4tyiwWO2dvsR2gDS0xk9tOOr9U-6uM=w240-h480-rw)\\n\",\"sig\":\"6748126a909a20dbdb67947a09d64e41d7140a79335a4ad675c6173d7dd5dbcab9c360dec617bd67bbbc20dfad416b15056eda2e20716cd6c425a84301a125a0\"}") as WikiNoteEvent + val accountViewModel = mockAccountViewModel() + val nav: (String) -> Unit = {} + + runBlocking { + withContext(Dispatchers.IO) { + LocalCache.justConsume(event, null) + } + } + + LoadNote(baseNoteHex = "277f982a4cd3f67cc47ad9282176acabee1713848f547d6021e0c155572078e1", accountViewModel = accountViewModel) { baseNote -> + ThemeComparisonColumn( + onDark = { + val bg = MaterialTheme.colorScheme.background + val backgroundColor = + remember { + mutableStateOf(bg) + } + + Column { + RenderWikiHeaderForThread(noteEvent = event, accountViewModel = accountViewModel, nav) + RenderTextEvent( + baseNote!!, + false, + true, + backgroundColor, + accountViewModel, + nav, + ) + } + }, + onLight = { + val bg = MaterialTheme.colorScheme.background + val backgroundColor = + remember { + mutableStateOf(bg) + } + + Column { + RenderWikiHeaderForThread(noteEvent = event, accountViewModel = accountViewModel, nav) + RenderTextEvent( + baseNote!!, + false, + true, + backgroundColor, + accountViewModel, + nav, + ) + } + }, + ) + } +} + +@Composable +private fun RenderWikiHeaderForThread( + noteEvent: WikiNoteEvent, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val forkedAddress = remember(noteEvent) { noteEvent.forkFromAddress() } + + Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp)) { + Column { + noteEvent.image()?.let { + AsyncImage( + model = it, + contentDescription = + stringResource( + R.string.preview_card_image_for, + it, + ), + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth(), + ) + } + + noteEvent.title()?.let { + Spacer(modifier = DoubleVertSpacer) + Text( + text = it, + fontSize = 28.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.fillMaxWidth(), + ) + } + + forkedAddress?.let { + LoadAddressableNote(aTag = it, accountViewModel = accountViewModel) { originalVersion -> + if (originalVersion != null) { + ShowForkInformation(originalVersion, Modifier.fillMaxWidth(), accountViewModel, nav) + } + } + } + + noteEvent + .summary() + ?.ifBlank { null } + ?.let { + Spacer(modifier = DoubleVertSpacer) + Text( + text = it, + modifier = Modifier.fillMaxWidth(), + color = Color.Gray, + ) + } + } + } +} + +@Composable +fun ShowForkInformation( + originalVersion: AddressableNote, + modifier: Modifier = Modifier, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteState by originalVersion.live().metadata.observeAsState() + val note = noteState?.note ?: return + val author = note.author ?: return + val route = remember(note) { routeFor(note, accountViewModel.userProfile()) } + + if (route != null) { + Row(modifier) { + Text(stringResource(id = R.string.forked_from)) + Spacer(modifier = StdHorzSpacer) + LoadAndDisplayUser(author, route, nav) + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5b0e87fa9..c48dd418b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -776,4 +776,5 @@ Thank you! Max Limit Restricted Writes + Forked from diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt index ef6f61a20..39ba084ed 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt @@ -43,7 +43,7 @@ open class BaseTextNoteEvent( fun mentions() = taggedUsers() open fun replyTos(): List { - val oldStylePositional = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] } + val oldStylePositional = tags.filter { it.size > 1 && it.size <= 3 && it[0] == "e" }.map { it[1] } val newStyleReply = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "reply" }?.get(1) val newStyleRoot = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "root" }?.get(1) @@ -153,7 +153,10 @@ open class BaseTextNoteEvent( fun tagsWithoutCitations(): List { val repliesTo = replyTos() val tagAddresses = - taggedAddresses().filter { it.kind != CommunityDefinitionEvent.KIND }.map { it.toTag() } + taggedAddresses().filter { + it.kind != CommunityDefinitionEvent.KIND && + it.kind != WikiNoteEvent.KIND + }.map { it.toTag() } if (repliesTo.isEmpty() && tagAddresses.isEmpty()) return emptyList() val citations = findCitations() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt index b2fd06e2e..0c7956752 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt @@ -123,6 +123,7 @@ class EventFactory { VideoHorizontalEvent.KIND -> VideoHorizontalEvent(id, pubKey, createdAt, tags, content, sig) VideoVerticalEvent.KIND -> VideoVerticalEvent(id, pubKey, createdAt, tags, content, sig) VideoViewEvent.KIND -> VideoViewEvent(id, pubKey, createdAt, tags, content, sig) + WikiNoteEvent.KIND -> WikiNoteEvent(id, pubKey, createdAt, tags, content, sig) else -> Event(id, pubKey, createdAt, kind, tags, content, sig) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/WikiNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/WikiNoteEvent.kt new file mode 100644 index 000000000..25bb51454 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/WikiNoteEvent.kt @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2024 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.events + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.encoders.ATag +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class WikiNoteEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseTextNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig), AddressableEvent { + override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: "" + + override fun address() = ATag(kind, pubKey, dTag(), null) + + fun topics() = hashtags() + + fun forkFromAddress() = + tags.firstOrNull { it.size > 3 && it[0] == "a" && it[3] == "fork" }?.let { + val aTagValue = it[1] + val relay = it.getOrNull(2) + + ATag.parse(aTagValue, relay) + } + + fun forkFromVersion() = tags.firstOrNull { it.size > 3 && it[0] == "e" && it[3] == "fork" }?.get(1) + + fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1) + + fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1) + + fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1) + + fun publishedAt() = + try { + tags.firstOrNull { it.size > 1 && it[0] == "published_at" }?.get(1)?.toLongOrNull() + } catch (_: Exception) { + null + } + + companion object { + const val KIND = 30818 + + fun create( + msg: String, + title: String?, + replyTos: List?, + mentions: List?, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (WikiNoteEvent) -> Unit, + ) { + val tags = mutableListOf>() + replyTos?.forEach { tags.add(arrayOf("e", it)) } + mentions?.forEach { tags.add(arrayOf("p", it)) } + title?.let { tags.add(arrayOf("title", it)) } + tags.add(arrayOf("alt", "Wiki Post: $title")) + signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady) + } + } +} From 452911bf276829f2ddff35717d30328f69bf948c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 23 Feb 2024 18:08:30 -0500 Subject: [PATCH 147/176] Removes the need to specify nulls for new TextNote creations --- .../quartz/events/TextNoteEvent.kt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt index 91e371922..083fc4523 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt @@ -45,16 +45,16 @@ class TextNoteEvent( fun create( msg: String, - replyTos: List?, - mentions: List?, - addresses: List?, - extraTags: List?, + replyTos: List? = null, + mentions: List? = null, + addresses: List? = null, + extraTags: List? = null, zapReceiver: List? = null, - markAsSensitive: Boolean, - zapRaiserAmount: Long?, - replyingTo: String?, - root: String?, - directMentions: Set, + markAsSensitive: Boolean = false, + zapRaiserAmount: Long? = null, + replyingTo: String? = null, + root: String? = null, + directMentions: Set = emptySet(), geohash: String? = null, nip94attachments: List? = null, signer: NostrSigner, From b9c3123f1860bf87b9d52571c878ecebe17b1147 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 23 Feb 2024 18:08:55 -0500 Subject: [PATCH 148/176] Faster Longform tag filters --- .../vitorpamplona/quartz/events/LongTextNoteEvent.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/LongTextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/LongTextNoteEvent.kt index a64b56fc4..fdf5cfd1a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/LongTextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/LongTextNoteEvent.kt @@ -35,17 +35,17 @@ class LongTextNoteEvent( content: String, sig: HexKey, ) : BaseTextNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig), AddressableEvent { - override fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: "" + override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: "" override fun address() = ATag(kind, pubKey, dTag(), null) - fun topics() = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) } + fun topics() = hashtags() - fun title() = tags.filter { it.firstOrNull() == "title" }.mapNotNull { it.getOrNull(1) }.firstOrNull() + fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1) - fun image() = tags.filter { it.firstOrNull() == "image" }.mapNotNull { it.getOrNull(1) }.firstOrNull() + fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1) - fun summary() = tags.filter { it.firstOrNull() == "summary" }.mapNotNull { it.getOrNull(1) }.firstOrNull() + fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1) fun publishedAt() = try { From 49c2d4f4869ecc32f4b6b6c09bfff19e60043248 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 23 Feb 2024 22:09:06 -0500 Subject: [PATCH 149/176] Adding Basic Git Rendering support. --- .../amethyst/model/LocalCache.kt | 96 ++++++++ .../amethyst/ui/note/NoteCompose.kt | 221 ++++++++++++++++++ .../amethyst/ui/screen/ThreadFeedView.kt | 8 + app/src/main/res/values/strings.xml | 3 + .../quartz/events/EventFactory.kt | 4 + .../quartz/events/GitIssueEvent.kt | 79 +++++++ .../quartz/events/GitPatchEvent.kt | 95 ++++++++ .../quartz/events/GitReplyEvent.kt | 79 +++++++ .../quartz/events/GitRepositoryEvent.kt | 59 +++++ 9 files changed, 644 insertions(+) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/GitIssueEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/GitPatchEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/GitReplyEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/GitRepositoryEvent.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 3214a11b2..e23032590 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -71,6 +71,10 @@ import com.vitorpamplona.quartz.events.FileStorageEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent import com.vitorpamplona.quartz.events.GenericRepostEvent import com.vitorpamplona.quartz.events.GiftWrapEvent +import com.vitorpamplona.quartz.events.GitIssueEvent +import com.vitorpamplona.quartz.events.GitPatchEvent +import com.vitorpamplona.quartz.events.GitReplyEvent +import com.vitorpamplona.quartz.events.GitRepositoryEvent import com.vitorpamplona.quartz.events.HighlightEvent import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.events.LiveActivitiesEvent @@ -361,6 +365,87 @@ object LocalCache { refreshObservers(note) } + fun consume( + event: GitPatchEvent, + relay: Relay? = null, + ) { + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) + + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event != null) return + + if (antiSpam.isSpam(event, relay)) { + relay?.let { it.spamCounter++ } + return + } + + note.loadEvent(event, author, emptyList()) + + refreshObservers(note) + } + + fun consume( + event: GitIssueEvent, + relay: Relay? = null, + ) { + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) + + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event != null) return + + if (antiSpam.isSpam(event, relay)) { + relay?.let { it.spamCounter++ } + return + } + + note.loadEvent(event, author, emptyList()) + + refreshObservers(note) + } + + fun consume( + event: GitReplyEvent, + relay: Relay? = null, + ) { + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) + + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event != null) return + + if (antiSpam.isSpam(event, relay)) { + relay?.let { it.spamCounter++ } + return + } + + val replyTo = + event + .tagsWithoutCitations() + .filter { it != event.repository()?.toTag() } + .mapNotNull { checkGetOrCreateNote(it) } + + note.loadEvent(event, author, replyTo) + + refreshObservers(note) + } + fun consume( event: LongTextNoteEvent, relay: Relay?, @@ -508,6 +593,13 @@ object LocalCache { consumeBaseReplaceable(event, relay) } + fun consume( + event: GitRepositoryEvent, + relay: Relay?, + ) { + consumeBaseReplaceable(event, relay) + } + fun consume( event: ChannelListEvent, relay: Relay?, @@ -1847,6 +1939,10 @@ object LocalCache { is FileStorageEvent -> consume(event, relay) is FileStorageHeaderEvent -> consume(event, relay) is GiftWrapEvent -> consume(event, relay) + is GitIssueEvent -> consume(event, relay) + is GitReplyEvent -> consume(event, relay) + is GitPatchEvent -> consume(event, relay) + is GitRepositoryEvent -> consume(event, relay) is HighlightEvent -> consume(event, relay) is LiveActivitiesEvent -> consume(event, relay) is LiveActivitiesChatMessageEvent -> consume(event, relay) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index e25618332..a895af644 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -48,6 +48,7 @@ import androidx.compose.foundation.shape.CutCornerShape import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.IconButton import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme @@ -204,6 +205,8 @@ import com.vitorpamplona.quartz.events.EmptyTagList import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent import com.vitorpamplona.quartz.events.GenericRepostEvent +import com.vitorpamplona.quartz.events.GitPatchEvent +import com.vitorpamplona.quartz.events.GitRepositoryEvent import com.vitorpamplona.quartz.events.HighlightEvent import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.events.LiveActivitiesEvent @@ -1182,6 +1185,19 @@ private fun RenderNoteRow( is LiveActivitiesEvent -> { RenderLiveActivityEvent(baseNote, accountViewModel, nav) } + is GitRepositoryEvent -> { + RenderGitRepositoryEvent(baseNote, accountViewModel, nav) + } + is GitPatchEvent -> { + RenderGitPatchEvent( + baseNote, + makeItShort, + canPreview, + backgroundColor, + accountViewModel, + nav, + ) + } is PrivateDmEvent -> { RenderPrivateMessage( baseNote, @@ -3464,6 +3480,211 @@ fun AudioHeader( } } +@Composable +fun RenderGitPatchEvent( + baseNote: Note, + makeItShort: Boolean, + canPreview: Boolean, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val event = baseNote.event as? GitPatchEvent ?: return + + RenderGitPatchEvent(event, baseNote, makeItShort, canPreview, backgroundColor, accountViewModel, nav) +} + +@Composable +private fun RenderShortRepositoryHeader( + baseNote: AddressableNote, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteState = baseNote.live().metadata.observeAsState() + val note = remember(noteState) { noteState.value?.note } ?: return + val noteEvent = note.event as? GitRepositoryEvent ?: return + + Column( + modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp), + ) { + val title = remember(noteEvent) { noteEvent.name() ?: noteEvent.dTag() } + Text( + text = stringResource(id = R.string.git_repository, title), + style = MaterialTheme.typography.titleLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth(), + ) + + noteEvent.description()?.let { + Spacer(modifier = DoubleVertSpacer) + Text( + text = it, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +@Composable +private fun RenderGitPatchEvent( + noteEvent: GitPatchEvent, + note: Note, + makeItShort: Boolean, + canPreview: Boolean, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val repository = remember(noteEvent) { noteEvent.repository() } + + if (repository != null) { + LoadAddressableNote(aTag = repository, accountViewModel = accountViewModel) { + if (it != null) { + RenderShortRepositoryHeader(it, accountViewModel, nav) + Spacer(modifier = DoubleVertSpacer) + } + } + } + + LoadDecryptedContent(note, accountViewModel) { body -> + val eventContent by + remember(note.event) { + derivedStateOf { + val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null } + + if (!subject.isNullOrBlank() && !body.split("\n")[0].contains(subject)) { + "### $subject\n$body" + } else { + body + } + } + } + + val isAuthorTheLoggedUser = remember(note.event) { accountViewModel.isLoggedUser(note.author) } + + if (makeItShort && isAuthorTheLoggedUser) { + Text( + text = eventContent, + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } else { + SensitivityWarning( + note = note, + accountViewModel = accountViewModel, + ) { + val modifier = remember(note) { Modifier.fillMaxWidth() } + val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList } + + TranslatableRichTextViewer( + content = eventContent, + canPreview = canPreview && !makeItShort, + modifier = modifier, + tags = tags, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + if (note.event?.hasHashtags() == true) { + val hashtags = + remember(note.event) { note.event?.hashtags()?.toImmutableList() ?: persistentListOf() } + DisplayUncitedHashtags(hashtags, eventContent, nav) + } + } + } +} + +@Composable +fun RenderGitRepositoryEvent( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val event = baseNote.event as? GitRepositoryEvent ?: return + + RenderGitRepositoryEvent(event, baseNote, accountViewModel, nav) +} + +@Composable +private fun RenderGitRepositoryEvent( + noteEvent: GitRepositoryEvent, + note: Note, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val title = remember(noteEvent) { noteEvent.name() ?: noteEvent.dTag() } + val summary = remember(noteEvent) { noteEvent.description() } + val web = remember(noteEvent) { noteEvent.web() } + val clone = remember(noteEvent) { noteEvent.clone() } + + Row( + modifier = + Modifier + .clip(shape = QuoteBorder) + .border( + 1.dp, + MaterialTheme.colorScheme.subtleBorder, + QuoteBorder, + ).padding(Size10dp), + ) { + Column { + Text( + text = stringResource(id = R.string.git_repository, title), + style = MaterialTheme.typography.titleMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth(), + ) + + summary?.let { + Text( + text = it, + modifier = Modifier.fillMaxWidth().padding(vertical = Size5dp), + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + } + + HorizontalDivider(thickness = DividerThickness) + + web?.let { + Row(Modifier.fillMaxWidth().padding(top = Size5dp)) { + Text( + text = stringResource(id = R.string.git_web_address), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Spacer(modifier = StdHorzSpacer) + ClickableUrl( + url = it, + urlText = it.removePrefix("https://").removePrefix("http://"), + ) + } + } + + clone?.let { + Row(Modifier.fillMaxWidth().padding(top = Size5dp)) { + Text( + text = stringResource(id = R.string.git_clone_address), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Spacer(modifier = StdHorzSpacer) + ClickableUrl( + url = it, + urlText = it.removePrefix("https://").removePrefix("http://"), + ) + } + } + } + } +} + @Composable fun RenderLiveActivityEvent( baseNote: Note, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index a2e7ff250..abb9572d6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -120,6 +120,8 @@ import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay import com.vitorpamplona.amethyst.ui.note.ReactionsRow import com.vitorpamplona.amethyst.ui.note.RenderAppDefinition import com.vitorpamplona.amethyst.ui.note.RenderEmojiPack +import com.vitorpamplona.amethyst.ui.note.RenderGitPatchEvent +import com.vitorpamplona.amethyst.ui.note.RenderGitRepositoryEvent import com.vitorpamplona.amethyst.ui.note.RenderPinListEvent import com.vitorpamplona.amethyst.ui.note.RenderPoll import com.vitorpamplona.amethyst.ui.note.RenderPostApproval @@ -157,6 +159,8 @@ import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent import com.vitorpamplona.quartz.events.GenericRepostEvent +import com.vitorpamplona.quartz.events.GitPatchEvent +import com.vitorpamplona.quartz.events.GitRepositoryEvent import com.vitorpamplona.quartz.events.HighlightEvent import com.vitorpamplona.quartz.events.LongTextNoteEvent import com.vitorpamplona.quartz.events.PeopleListEvent @@ -526,6 +530,10 @@ fun NoteMaster( accountViewModel, nav, ) + } else if (noteEvent is GitRepositoryEvent) { + RenderGitRepositoryEvent(baseNote, accountViewModel, nav) + } else if (noteEvent is GitPatchEvent) { + RenderGitPatchEvent(baseNote, false, true, backgroundColor, accountViewModel, nav) } else if (noteEvent is AppDefinitionEvent) { RenderAppDefinition(baseNote, accountViewModel, nav) } else if (noteEvent is HighlightEvent) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c48dd418b..d68c4f42e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -777,4 +777,7 @@ Max Limit Restricted Writes Forked from + Git Repository: %1$s + Web: + Clone: diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt index 0c7956752..05edf3688 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt @@ -89,6 +89,10 @@ class EventFactory { FileStorageHeaderEvent(id, pubKey, createdAt, tags, content, sig) GenericRepostEvent.KIND -> GenericRepostEvent(id, pubKey, createdAt, tags, content, sig) GiftWrapEvent.KIND -> GiftWrapEvent(id, pubKey, createdAt, tags, content, sig) + GitIssueEvent.KIND -> GitIssueEvent(id, pubKey, createdAt, tags, content, sig) + GitReplyEvent.KIND -> GitReplyEvent(id, pubKey, createdAt, tags, content, sig) + GitPatchEvent.KIND -> GitPatchEvent(id, pubKey, createdAt, tags, content, sig) + GitRepositoryEvent.KIND -> GitRepositoryEvent(id, pubKey, createdAt, tags, content, sig) GoalEvent.KIND -> GoalEvent(id, pubKey, createdAt, tags, content, sig) HighlightEvent.KIND -> HighlightEvent(id, pubKey, createdAt, tags, content, sig) HTTPAuthorizationEvent.KIND -> diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GitIssueEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GitIssueEvent.kt new file mode 100644 index 000000000..ed11d2015 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GitIssueEvent.kt @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2024 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.events + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.encoders.ATag +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class GitIssueEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseTextNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + private fun innerRepository() = + tags.firstOrNull { it.size > 3 && it[0] == "a" && it[3] == "root" } + ?: tags.firstOrNull { it.size > 1 && it[0] == "a" } + + private fun repositoryHex() = innerRepository()?.getOrNull(1) + + fun rootIssueOrPath() = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "root" }?.get(1) + + fun repository() = + innerRepository()?.let { + if (it.size > 1) { + val aTagValue = it[1] + val relay = it.getOrNull(2) + + ATag.parse(aTagValue, relay) + } else { + null + } + } + + companion object { + const val KIND = 1621 + const val ALT = "A Git Issue" + + fun create( + patch: String, + createdAt: Long = TimeUtils.now(), + signer: NostrSigner, + onReady: (GitIssueEvent) -> Unit, + ) { + val content = patch + val tags = + mutableListOf( + arrayOf(), + ) + + tags.add(arrayOf("alt", ALT)) + + signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GitPatchEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GitPatchEvent.kt new file mode 100644 index 000000000..d9a6670e1 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GitPatchEvent.kt @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2024 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.events + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.encoders.ATag +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class GitPatchEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { + private fun innerRepository() = + tags.firstOrNull { it.size > 3 && it[0] == "a" && it[3] == "root" } + ?: tags.firstOrNull { it.size > 1 && it[0] == "a" } + + private fun repositoryHex() = innerRepository()?.getOrNull(1) + + fun repository() = + innerRepository()?.let { + if (it.size > 1) { + val aTagValue = it[1] + val relay = it.getOrNull(2) + + ATag.parse(aTagValue, relay) + } else { + null + } + } + + fun commit() = tags.firstOrNull { it.size > 1 && it[0] == "commit" }?.get(1) + + fun parentCommit() = tags.firstOrNull { it.size > 1 && it[0] == "parent-commit" }?.get(1) + + fun commitPGPSig() = tags.firstOrNull { it.size > 1 && it[0] == "commit-pgp-sig" }?.get(1) + + fun committer() = + tags.filter { it.size > 1 && it[0] == "committer" }?.mapNotNull { + Committer(it.getOrNull(1), it.getOrNull(2), it.getOrNull(3), it.getOrNull(4)) + } + + data class Committer( + val name: String?, + val email: String?, + val timestamp: String?, + val timezoneInMinutes: String?, + ) + + companion object { + const val KIND = 1617 + const val ALT = "A Git Patch" + + fun create( + patch: String, + createdAt: Long = TimeUtils.now(), + signer: NostrSigner, + onReady: (GitPatchEvent) -> Unit, + ) { + val content = patch + val tags = + mutableListOf( + arrayOf(), + ) + + tags.add(arrayOf("alt", ALT)) + + signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GitReplyEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GitReplyEvent.kt new file mode 100644 index 000000000..a6b1a5658 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GitReplyEvent.kt @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2024 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.events + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.encoders.ATag +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class GitReplyEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseTextNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + private fun innerRepository() = + tags.firstOrNull { it.size > 3 && it[0] == "a" && it[3] == "root" } + ?: tags.firstOrNull { it.size > 1 && it[0] == "a" } + + private fun repositoryHex() = innerRepository()?.getOrNull(1) + + fun repository() = + innerRepository()?.let { + if (it.size > 1) { + val aTagValue = it[1] + val relay = it.getOrNull(2) + + ATag.parse(aTagValue, relay) + } else { + null + } + } + + fun rootIssueOrPath() = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "root" }?.get(1) + + companion object { + const val KIND = 1622 + const val ALT = "A Git Issue" + + fun create( + patch: String, + createdAt: Long = TimeUtils.now(), + signer: NostrSigner, + onReady: (GitReplyEvent) -> Unit, + ) { + val content = patch + val tags = + mutableListOf( + arrayOf(), + ) + + tags.add(arrayOf("alt", ALT)) + + signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GitRepositoryEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GitRepositoryEvent.kt new file mode 100644 index 000000000..f610dc7e2 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GitRepositoryEvent.kt @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2024 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.events + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class GitRepositoryEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun name() = tags.firstOrNull { it.size > 1 && it[0] == "name" }?.get(1) + + fun description() = tags.firstOrNull { it.size > 1 && it[0] == "description" }?.get(1) + + fun web() = tags.firstOrNull { it.size > 1 && it[0] == "web" }?.get(1) + + fun clone() = tags.firstOrNull { it.size > 1 && it[0] == "clone" }?.get(1) + + companion object { + const val KIND = 30617 + const val ALT = "Git Repository" + + fun create( + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (GitRepositoryEvent) -> Unit, + ) { + val tags = mutableListOf>() + tags.add(arrayOf("alt", ALT)) + signer.sign(createdAt, KIND, tags.toTypedArray(), "", onReady) + } + } +} From ca4cc93b31fe49b35bbcc5f47500f8a054a33c71 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 24 Feb 2024 09:15:41 -0500 Subject: [PATCH 150/176] Fix Quartz use --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index bc446f4c6..51a530e1d 100644 --- a/README.md +++ b/README.md @@ -236,9 +236,7 @@ dependencyResolutionManagement { Add the dependency ```gradle -implementation('com.github.vitorpamplona.amethyst:quartz:v0.84.3') { - exclude group: 'net.java.dev.jna' -} +implementation('com.github.vitorpamplona.amethyst:quartz:v0.84.3') ``` ## Contributing From 3f33871c9c1c0f20632d2020aabe39a9553871d8 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Sat, 24 Feb 2024 14:16:45 +0000 Subject: [PATCH 151/176] New Crowdin translations by GitHub Action --- app/src/main/res/values-fr/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 2e9eb7407..ea3522c88 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -657,4 +657,8 @@ Merci ! Limite maximale Écritures limitées + Forké depuis + Dépôt Git : %1$s + Web : + Clone : From 6afd9a01914afcce8861ca4412af8c78d9b093ae Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 25 Feb 2024 11:39:48 -0500 Subject: [PATCH 152/176] Adds note on why the libscrypt was removed. --- quartz/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/quartz/build.gradle b/quartz/build.gradle index 834d8b4fe..c5ff3ec13 100644 --- a/quartz/build.gradle +++ b/quartz/build.gradle @@ -36,6 +36,7 @@ android { } packagingOptions { + // delete native scrypt lib because we cannot use dylibs in android exclude '**/libscrypt.dylib' } } From 5ad86e5604546bfac7d5d482f3f3f6a2a345f5c7 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 25 Feb 2024 11:40:39 -0500 Subject: [PATCH 153/176] Adds support for the `q ` tag together with the support for the `mention` marker. --- .../amethyst/service/NostrSingleEventDataSource.kt | 2 +- .../java/com/vitorpamplona/quartz/events/TextNoteEvent.kt | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt index 08f8001f4..3bfaa538f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt @@ -134,7 +134,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") { LnZapEvent.KIND, PollNoteEvent.KIND, ), - tags = mapOf("e" to it.map { it.idHex }), + tags = mapOf("e" to it.map { it.idHex }, "q" to it.map { it.idHex }), since = findMinimumEOSEs(it), // Max amount of "replies" to download on a specific event. limit = 1000, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt index 083fc4523..bd0efc504 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt @@ -79,6 +79,11 @@ class TextNoteEvent( tags.add(arrayOf("p", it)) } } + replyTos?.forEach { + if (it in directMentions) { + tags.add(arrayOf("q", it)) + } + } addresses ?.map { it.toTag() } ?.let { From ad03d355b0ea655e2e28c48b350dbcd389b0f4de Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 25 Feb 2024 11:45:59 -0500 Subject: [PATCH 154/176] Adds internet for testing of OTS attestations --- quartz/src/androidTest/AndroidManifest.xml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 quartz/src/androidTest/AndroidManifest.xml diff --git a/quartz/src/androidTest/AndroidManifest.xml b/quartz/src/androidTest/AndroidManifest.xml new file mode 100644 index 000000000..ecebd576e --- /dev/null +++ b/quartz/src/androidTest/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file From c4d6b1a5d1e4fa5b76f6d2c1bd781056bcb0b70f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 25 Feb 2024 11:49:51 -0500 Subject: [PATCH 155/176] Adds support for OpenTimestamps in Quartz --- .../vitorpamplona/quartz/NIP19EmbedTests.kt | 190 ++++++ .../java/com/vitorpamplona/quartz/OtsTest.kt | 85 +++ .../quartz/events/EventFactory.kt | 1 + .../vitorpamplona/quartz/events/OtsEvent.kt | 105 +++ .../vitorpamplona/quartz/ots/BlockHeader.java | 73 +++ .../vitorpamplona/quartz/ots/Calendar.java | 120 ++++ .../quartz/ots/CalendarAsyncSubmit.java | 59 ++ .../quartz/ots/DetachedTimestampFile.java | 227 +++++++ .../com/vitorpamplona/quartz/ots/Esplora.java | 66 ++ .../com/vitorpamplona/quartz/ots/Hash.java | 198 ++++++ .../com/vitorpamplona/quartz/ots/Merkle.java | 95 +++ .../quartz/ots/OpenTimestamps.java | 475 ++++++++++++++ .../com/vitorpamplona/quartz/ots/README.md | 17 + .../ots/StreamDeserializationContext.java | 107 +++ .../ots/StreamSerializationContext.java | 82 +++ .../vitorpamplona/quartz/ots/Timestamp.java | 617 ++++++++++++++++++ .../com/vitorpamplona/quartz/ots/Utils.java | 204 ++++++ .../quartz/ots/VerifyResult.java | 56 ++ .../BitcoinBlockHeaderAttestation.java | 118 ++++ .../EthereumBlockHeaderAttestation.java | 79 +++ .../LitecoinBlockHeaderAttestation.java | 99 +++ .../ots/attestation/PendingAttestation.java | 134 ++++ .../ots/attestation/TimeAttestation.java | 83 +++ .../ots/attestation/UnknownAttestation.java | 76 +++ .../quartz/ots/crypto/Digest.java | 52 ++ .../quartz/ots/crypto/ExtendedDigest.java | 11 + .../quartz/ots/crypto/GeneralDigest.java | 138 ++++ .../quartz/ots/crypto/KeccakDigest.java | 473 ++++++++++++++ .../quartz/ots/crypto/Memoable.java | 28 + .../vitorpamplona/quartz/ots/crypto/Pack.java | 242 +++++++ .../quartz/ots/crypto/RIPEMD160Digest.java | 442 +++++++++++++ .../CommitmentNotFoundException.java | 7 + .../exceptions/DeserializationException.java | 7 + .../ots/exceptions/ExceededSizeException.java | 7 + .../quartz/ots/exceptions/UrlException.java | 7 + .../ots/exceptions/VerificationException.java | 7 + .../quartz/ots/http/Request.java | 119 ++++ .../quartz/ots/http/Response.java | 78 +++ .../com/vitorpamplona/quartz/ots/op/Op.java | 139 ++++ .../vitorpamplona/quartz/ots/op/OpAppend.java | 63 ++ .../vitorpamplona/quartz/ots/op/OpBinary.java | 75 +++ .../vitorpamplona/quartz/ots/op/OpCrypto.java | 97 +++ .../quartz/ots/op/OpKECCAK256.java | 62 ++ .../quartz/ots/op/OpPrepend.java | 64 ++ .../quartz/ots/op/OpRIPEMD160.java | 67 ++ .../vitorpamplona/quartz/ots/op/OpSHA1.java | 68 ++ .../vitorpamplona/quartz/ots/op/OpSHA256.java | 62 ++ .../vitorpamplona/quartz/ots/op/OpUnary.java | 46 ++ 48 files changed, 5727 insertions(+) create mode 100644 quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP19EmbedTests.kt create mode 100644 quartz/src/androidTest/java/com/vitorpamplona/quartz/OtsTest.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/OtsEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/BlockHeader.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/Calendar.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/CalendarAsyncSubmit.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/DetachedTimestampFile.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/Esplora.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/Hash.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/Merkle.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/OpenTimestamps.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/README.md create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/StreamDeserializationContext.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/StreamSerializationContext.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/Timestamp.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/Utils.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/VerifyResult.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/attestation/BitcoinBlockHeaderAttestation.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/attestation/EthereumBlockHeaderAttestation.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/attestation/LitecoinBlockHeaderAttestation.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/attestation/PendingAttestation.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/attestation/TimeAttestation.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/attestation/UnknownAttestation.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/crypto/Digest.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/crypto/ExtendedDigest.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/crypto/GeneralDigest.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/crypto/KeccakDigest.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/crypto/Memoable.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/crypto/Pack.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/crypto/RIPEMD160Digest.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/exceptions/CommitmentNotFoundException.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/exceptions/DeserializationException.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/exceptions/ExceededSizeException.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/exceptions/UrlException.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/exceptions/VerificationException.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/http/Request.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/http/Response.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/op/Op.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpAppend.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpBinary.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpCrypto.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpKECCAK256.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpPrepend.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpRIPEMD160.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpSHA1.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpSHA256.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpUnary.java diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP19EmbedTests.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP19EmbedTests.kt new file mode 100644 index 000000000..973b2fe5e --- /dev/null +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP19EmbedTests.kt @@ -0,0 +1,190 @@ +/** + * Copyright (c) 2024 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 + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.crypto.KeyPair +import com.vitorpamplona.quartz.encoders.Bech32 +import com.vitorpamplona.quartz.encoders.Hex +import com.vitorpamplona.quartz.encoders.bechToBytes +import com.vitorpamplona.quartz.events.Event +import com.vitorpamplona.quartz.events.FhirResourceEvent +import com.vitorpamplona.quartz.events.TextNoteEvent +import com.vitorpamplona.quartz.signers.NostrSignerInternal +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertTrue +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith +import java.io.ByteArrayOutputStream +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.zip.GZIPInputStream +import java.util.zip.GZIPOutputStream +import kotlin.time.measureTimedValue + +@RunWith(AndroidJUnit4::class) +class NIP19EmbedTests { + @Test + fun testEmbedEvent() { + val signer = + NostrSignerInternal( + KeyPair(Hex.decode("e8e7197ccc53c9ed4cf9b1c8dce085475fa1ffdd71f2c14e44fe23d0bdf77598")), + ) + + var textNote: Event? = null + + val countDownLatch = CountDownLatch(1) + + TextNoteEvent.create("I like this. It could solve the ninvite problem in #1062, and it seems like it could be applied very broadly to limit the spread of events that shouldn't stand on their own or need to be private. The one question I have is how long are these embeds? If it's 50 lines of text, that breaks the human readable (or at least parseable) requirement of kind 1s. Also, encoding json in a tlv is silly, we should at least use the tlv to reduce the payload size.", signer = signer) { + textNote = it + countDownLatch.countDown() + } + + Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) + + assertNotNull(textNote) + + val bech32 = Bech32.encodeBytes(hrp = "n", textNote!!.toJson().toByteArray(Charsets.UTF_8), Bech32.Encoding.Bech32) + + println(bech32) + + val decoded = String(bech32.bechToBytes()) + val decodedNote = Event.fromJson(decoded) + + assertTrue(decodedNote.hasValidSignature()) + + assertEquals(textNote!!.toJson(), decodedNote.toJson()) + } + + @Test + fun testVisionPrescriptionEmbedEvent() { + val signer = + NostrSignerInternal( + KeyPair(Hex.decode("e8e7197ccc53c9ed4cf9b1c8dce085475fa1ffdd71f2c14e44fe23d0bdf77598")), + ) + + var eyeglassesPrescriptionEvent: Event? = null + + val countDownLatch = CountDownLatch(1) + + FhirResourceEvent.create(fhirPayload = visionPrescriptionFhir, signer = signer) { + eyeglassesPrescriptionEvent = it + countDownLatch.countDown() + } + + Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) + + assertNotNull(eyeglassesPrescriptionEvent) + + val bech32 = Bech32.encodeBytes(hrp = "nembed", eyeglassesPrescriptionEvent!!.toJson().toByteArray(Charsets.UTF_8), Bech32.Encoding.Bech32) + + println(eyeglassesPrescriptionEvent!!.toJson()) + println(bech32) + + val decoded = String(bech32.bechToBytes()) + val decodedNote = Event.fromJson(decoded) + + assertTrue(decodedNote.hasValidSignature()) + + assertEquals(eyeglassesPrescriptionEvent!!.toJson(), decodedNote.toJson()) + } + + @Test + fun testVisionPrescriptionEmbedEventCompression() { + val signer = + NostrSignerInternal( + KeyPair(Hex.decode("e8e7197ccc53c9ed4cf9b1c8dce085475fa1ffdd71f2c14e44fe23d0bdf77598")), + ) + + var eyeglassesPrescriptionEvent: Event? = null + + val countDownLatch = CountDownLatch(1) + + FhirResourceEvent.create(fhirPayload = visionPrescriptionFhir, signer = signer) { + eyeglassesPrescriptionEvent = it + countDownLatch.countDown() + } + + Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) + + assertNotNull(eyeglassesPrescriptionEvent) + + val bech32 = Bech32.encodeBytes(hrp = "nembed", gzip(eyeglassesPrescriptionEvent!!.toJson()), Bech32.Encoding.Bech32) + + println(eyeglassesPrescriptionEvent!!.toJson()) + println(bech32) + + val decoded = ungzip(bech32.bechToBytes()) + val decodedNote = Event.fromJson(decoded) + + assertTrue(decodedNote.hasValidSignature()) + + assertEquals(eyeglassesPrescriptionEvent!!.toJson(), decodedNote.toJson()) + } + + @Test + fun testCompressionSizes() { + val signer = + NostrSignerInternal( + KeyPair(Hex.decode("e8e7197ccc53c9ed4cf9b1c8dce085475fa1ffdd71f2c14e44fe23d0bdf77598")), + ) + + var eyeglassesPrescriptionEvent: Event? = null + + val countDownLatch = CountDownLatch(1) + + FhirResourceEvent.create(fhirPayload = visionPrescriptionFhir, signer = signer) { + eyeglassesPrescriptionEvent = it + countDownLatch.countDown() + } + + Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) + + assertNotNull(eyeglassesPrescriptionEvent) + + val json = eyeglassesPrescriptionEvent!!.toJson() + + Bech32.encodeBytes(hrp = "nembed", json.toByteArray(), Bech32.Encoding.Bech32) + Bech32.encodeBytes(hrp = "nembed", gzip(json), Bech32.Encoding.Bech32) + + val (bech32json, elapsedjson) = measureTimedValue { Bech32.encodeBytes(hrp = "nembed", json.toByteArray(), Bech32.Encoding.Bech32) } + val (bech32gzip, elapsedgzip) = measureTimedValue { Bech32.encodeBytes(hrp = "nembed", gzip(json), Bech32.Encoding.Bech32) } + + println("Raw JSON ${json.length} chars") + println("Bech32 JSON ${bech32json.length} chars in $elapsedjson") + println("Bech32 GZIP ${bech32gzip.length} chars in $elapsedgzip") + + assertTrue(true) + } + + val visionPrescriptionFhir = "{\"resourceType\":\"VisionPrescription\",\"status\":\"active\",\"created\":\"2014-06-15\",\"patient\":{\"reference\":\"Patient/example\"},\"dateWritten\":\"2014-06-15\",\"prescriber\":{\"reference\":\"Practitioner/example\"},\"lensSpecification\":[{\"eye\":\"right\",\"sphere\":-2,\"prism\":[{\"amount\":0.5,\"base\":\"down\"}],\"add\":2},{\"eye\":\"left\",\"sphere\":-1,\"cylinder\":-0.5,\"axis\":180,\"prism\":[{\"amount\":0.5,\"base\":\"up\"}],\"add\":2}]}" +} + +fun gzip(content: String): ByteArray { + val bos = ByteArrayOutputStream() + GZIPOutputStream(bos).bufferedWriter(Charsets.UTF_8).use { it.write(content) } + val array = bos.toByteArray() + return array +} + +fun ungzip(content: ByteArray): String = GZIPInputStream(content.inputStream()).bufferedReader(Charsets.UTF_8).use { it.readText() } diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/OtsTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/OtsTest.kt new file mode 100644 index 000000000..3a57bb2c0 --- /dev/null +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/OtsTest.kt @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2024 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 + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.crypto.KeyPair +import com.vitorpamplona.quartz.events.Event +import com.vitorpamplona.quartz.events.OtsEvent +import com.vitorpamplona.quartz.signers.NostrSignerInternal +import junit.framework.TestCase.assertEquals +import org.junit.Assert +import org.junit.Ignore +import org.junit.Test +import org.junit.runner.RunWith +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +@RunWith(AndroidJUnit4::class) +class OtsTest { + val otsEvent = "{\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqCiiW0FlsU9lqK5f1A+cL6CGJ1Ah4V/A1yNJY/stUE3wECJz6ng/QxU5Z6xwaMx97qkI//AQqJv8bEMrGTplGWRv5qm4DgjxIHkcQqzpL0Fjr9VBAAijDe0IsQYpOhw1SIjZIgQa6i16CPEEZck7CvAIxR0AloJzCZoAg9/jDS75DI4uLWh0dHBzOi8vYWxpY2UuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wEOwPtjIkKI1hmtv9t1kuxZcI8QRlyTsK8Ahl0wrCSggZzgCD3+MNLvkMjiwraHR0cHM6Ly9ib2IuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wEE1dVGa8JCuf2ek0c5ybDKII8SCBoVz8Sal45Kd1O8STWIGJTcl5JPtAZBZitqk3BE9MqAjxBGXJOwrwCHoGVgAZi9q9AIPf4w0u+QyOKShodHRwczovL2Zpbm5leS5jYWxlbmRhci5ldGVybml0eXdhbGwuY29t8BAFZFXFYg7DJJ0OzjmJ0FKWCPEEZck7CvAI0M49IcBR5bf/AIPf4w0u+QyOIyJodHRwczovL2J0Yy5jYWxlbmRhci5jYXRhbGxheHkuY29tCPEgoE3IfYTmxxo4W/x/QYp/NGX6Wu93gSQkwbpjpOhZORcI8SDWLLurVQaXHdUuwivCfTfuxYCaq+AzypSGqLDAVocrEgjwIBfgjta16y13Gp4etQOCa9YiKEcM+/9AieG/vZolr3IDCPAgMR2zFCb384CEi8tVuI2fHgLT3I9zpe7oqJTzCcEqxWEI8SAJSdgeeosr7IxdOt8r7f0ipWc8FI6GAhgep8zSRgWikAjxIGxYmtCsC79Tx4z4YsT1WuMo+ycMkwhGQsQltF597cchCPAgCLrBf9vR1Aex6yY+vSkXAvLjMKdMqM/a1g8zNPwLeJcI8SBDCbTk4CczTuiIyZeUyYRVh31BZdjaSd2nU/pBQxu+6QjwIOwqE9/WqGC6CHH0i+tr7edvYX5PstSDf08KmnMqsqCoCPEgQIdEBg358vfek3Qjfyrgl51iCU6WUWmThsGLPDTcB0QI8SAX4dp64iI8pBx+zBqAQwUN6XgZ1cEfT8+2vha/9I1vzwjxWQEAAAAB8YJxvxJJth8OnxIV6UOXveIZAcJPTHcAkWgnucpuYqYAAAAAAP3///8CRhoMAAAAAAAWABTUUZK82uAvbU3vVyaaPIOddZBicwAAAAAAAAAAImog8ARCqgwACAjwIB9TcMDLhzgeS1Uw647lNvCfWECkkUvfrrOe6nay0sGdCAjwICYfs90sbPggoMICyOHGYbmOzop2L9mlnh4xqiBLY7yPCAjxINDiVWOBHnRmGJleQdB9myvJAJbNJ9kciZlTOkgJy89mCAjwIHfxqDLdwycj1Vtyth2CaSDdLQwiey9oV6Xov4stLpWNCAjxIGurJYpKJnKp9+y7MAdC+gXgHOiAu5P3RRUFW9l5hGaCCAjxICXqs9hdY0QMP/MNeqlt6s7xaIYtEXZ1CLvou5gaZNEICAjxIHdYxVeI76NXbT2zHcv6lw+v819Ooib7KWxc1GAsiX2fCAjwICPWdi3uBXOlIdmYi+V9C7wAqLyGE4DMoHD+GtvLizqiCAjwIOF2vENtWN5okEMMS+JSf1SGTY9yYP9j0JjXLbC1s+N1CAjxINWsgCtsPxhRNbe372k8/20WDbiL9e8934hGF256DvRECAjwII3mi+Li06j10ORxg0dYMkcsyGb115Jiqq1YEV3K/u+aCAgABYiWDXPXGQEDw9Qy\",\"created_at\":1707690688,\"id\":\"759f9da5846e936fab06766a524b36ba71c03bbc69ad0944fb8ee4bb1f3dd705\",\"kind\":1040,\"pubkey\":\"82fbb08c8ef45c4d71c88368d0ae805bc62fb92f166ab04a0b7a0c83d8cbc29a\",\"sig\":\"07c7896c8cbb97b5d7483097590c9d31b73f35c1ad9e752002bb5c1776cbd852e1d32704333d6930c9bc3e40f8b899a1f2e9f91cc3bf797d86acdecba7792576\",\"tags\":[[\"e\",\"a828a25b4165b14f65a8ae5fd40f9c2fa086275021e15fc0d7234963fb2d504d\"],[\"p\",\"595ca8eaace5899cb6ab7e2542bfc972136376f2eabc09287f1857eb8f167e53\"],[\"alt\",\"NIP-03 time stamp\"]]}" + val otsEvent2 = "{\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqGNPU2jhd4no+zg2ytDkuf5PIoivr8KHI8BL68aKGNbwENyCNtiEN98IzIZgEu3cl6YI//AQ6TkSRd3BTGhDHCK1KkJc+AjxIAHaizG++NNL3Vm13BJrIhT7Br6tEYpb0TVRGaadgiUMCPAgOSDREH9v1Y50UHu79LfC4Lcd9WklQJzRQpw+Unb/pyII8QRltDD58AgqrxfAVrLw7QCD3+MNLvkMji4taHR0cHM6Ly9hbGljZS5idGMuY2FsZW5kYXIub3BlbnRpbWVzdGFtcHMub3Jn//AQQMq/CLpGwY60nmddPS7OVgjxIDKxqd9nl+Mej41vP52Wd7gv7004r3n1rFGDObS8icRvCPAgH9TB/kwvXJEEw+h9Ce6fLaI3MORjtTEge0GbAefT6W4I8QRltDD58AhRcoU3gAo/swCD3+MNLvkMjiwraHR0cHM6Ly9ib2IuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wECWtWsKo0uvSr8BYonjs3DEI8CBlsh2ng1Spl0K4oStYElGuMJsjd2uo5nXB+apo5A7ipwjxIM8oxynBwNA+QS/X7Ebtl1kyhFgfoOQioASNfCBzZ4gaCPEEZbQw+fAId6Yd5cw5gioAg9/jDS75DI4pKGh0dHBzOi8vZmlubmV5LmNhbGVuZGFyLmV0ZXJuaXR5d2FsbC5jb23wEJmPzXQbxv0AFTIyjTWjMskI8CAurbkrfrBtlinZXSDxj+m/oIkze57hGjTSxu1Xs87XYQjwIPk/LMD0zIgKoEE2dfeoYrrdHuO6dwmghTwUFajH2QzkCPEEZbQw+fAIE+Pq1/Wmdpj/AIPf4w0u+QyOIyJodHRwczovL2J0Yy5jYWxlbmRhci5jYXRhbGxheHkuY29tCPAgB2CbqkV7VpjRKIl3Ea6cBmB/EHcSN/YCgcc1E+mc07QI8CAfpkZ2Hh4Rukz3x4il3tZqQtlDlbna+I6so2t2YSEmMQjwIJOv32jbsMa2HJwpleRCKLEhgYOoHCSfpv1ZO0YNNNFsCPAgLMM7eFfCjokQfU4gdU5WpG/wBLkO9lDRF0GktL6ujt8I8SCRxJ0bC1PQ8qFmI/1jh8AS5d1/6VRJNMt1Hz41QmNr3QjwIBmgrKBF+OZ3y+XOMv2E7IZ4WwLr2u2H+ehsBfy7cPlICPEgm4ZMCSXzZVWu40d+zk2edaur6KOauo8X7V2KaFBR1VoI8SBKVVOiyq6IFqGn/15kLwk7L8upMAIZ0znjhYxYqSTQCQjwIMBD1twPZ33GxbwTiuOCeJPkoP++6R2wYpCii8UBTdgwCPAg84VkgMXwrt2xxRoeC1/6CtsFctki+w3m8Rs5/6g/IhEI8CCnzZQDhJyicX7bS7U8PMUObuC9Y4TXe+4THoXBMMXkxwjxIMZ0oAvshpcwowR3qPEDbwKZ6B4NPSU4Hz/+4PnD74gnCPAgIfLZEKqAvkMNXfakXoNq1UVqGSzL4Z86z5GzUfbvw8UI8SC8KoIeLvjd4vJ/xhNVphakPRd80YKeNkeYEuVH8k2EtAjwIPRtinLLxzt8iuw0XZtpTDzEstZOTNYVm+Bi3fEzdeIuCPFZAQAAAAE8vsasINN5DKon0KakX2HNdCB126ZLKXrw4PfvyEfqbwAAAAAA/f///wLPGw8AAAAAABYAFCvgxlh6msa4ZOtgvlc5KiZCx7IvAAAAAAAAAAAiaiDwBKygDAAICPEgB/2DJ3s6gMky/PceGZocTFRXjZUiCCAhHGYwQk/8yrUICPEgDuSd6+PJHUMuEHyLcKFxw7xfvRHRfInjkV3/Zy3BxqAICPEgZDgQ+4VXzlOIkGoO8EVxDgs2cWaeh4EEiaqa/y50gKAICPAgFI+zIuYcMF69GmPQVsXa9oy8eng7MeRZdIxArQyeX3oICPEgX5HYIuImpiSTEapgEssEW4l+W+4aRfNCG3pZf7z0hCoICPEgPkAbOSjFdtS4NT7MXgMYVQoQhI1JZtdFxUu4J3NTt7IICPEguW3qyuyGjctu5d9rM9P9ZCs/ZK4vAc+z21b9ygklgWAICPEgu4e2645xtvGhI1Zzuiv23vRhwE8uC9vj1TAgNg/C8UcICPAgwoQX8X0nY4HoQLRsJ0z8JCWQDzRh2iL2QXEb8z3gbjIICPAgzTwlPRtStsLJWhz3Q/0l8tMnrPSHVuh+zCiGk95dW2MICPAgGcBEuYZyzFNapHOfnJ9Q515QzO2VbIRhlVI0vIhd4jwICPAgidRMoM2pA+KmVJenVrLcbollsbUg9lL9bmv1C1dSxswICPAgZinGakwhbHdanTaRJeBkEUlbhfNokvj8b5KneyG+wzIICAAFiJYNc9cZAQOtwTI=\",\"created_at\":1706324334,\"id\":\"2ad074ddb7724eb13b4244b49cf2321b1057f37fdf8ce102e6329b839cf763a9\",\"kind\":1040,\"pubkey\":\"82fbb08c8ef45c4d71c88368d0ae805bc62fb92f166ab04a0b7a0c83d8cbc29a\",\"sig\":\"ad7274bb32ba9e9cfdbd52f4887e8a2fda1047c75a7185b2ab7ff254ebac14ed48a2b60737494d655e24c9400eeeec7e29293a77bfcaafaecd94b350c9a2c22b\",\"tags\":[[\"e\",\"a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6\"],[\"p\",\"c31e22c3715c1bde5608b7e0d04904f22f5fc453ba1806d21c9f2382e1e58c6c\"],[\"alt\",\"NIP-03 time stamp\"]]}" + val otsPendingEvent = "{\"id\":\"12fa15ad4b4cf9dc5940389325b69b93c5c1f59c049c701ee669b275299fdaf1\",\"pubkey\":\"dcaa6c8a2f47b6fef4a34b20e8843c59dbe7c5f07a402338c09fd147dd01d22b\",\"created_at\":1708877521,\"kind\":1040,\"tags\":[[\"e\",\"a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6\"],[\"alt\",\"Opentimestamps Attestation\"]],\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqGNPU2jhd4no+zg2ytDkuf5PIoivr8KHI8BL68aKGNbwELidvzr0usf55CkpKf6OABQI//AQK3sWd2tq+7KO8YNJIARJugjxBGXbZtPwCL0H4/7GL5+SAIPf4w0u+QyOLCtodHRwczovL2JvYi5idGMuY2FsZW5kYXIub3BlbnRpbWVzdGFtcHMub3Jn//AQPDZsJgN1TnJXoUzlsgo93wjwIIfBc7LUqkCbC1BLZRZ+6LXztK50UdH5xe7fn40bupkrCPEEZdtm0/AI0CADXN5ZIncAg9/jDS75DI4uLWh0dHBzOi8vYWxpY2UuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ/AQcELcSrE04cuGKlZQf2LeVwjwILUDSf9vK2GaefKTpn/LV2oUsQaA5WbqaP3C+1ZxQfRNCPEEZdtm0/AIbCtb+yRXFqUAg9/jDS75DI4pKGh0dHBzOi8vZmlubmV5LmNhbGVuZGFyLmV0ZXJuaXR5d2FsbC5jb20=\",\"sig\":\"f6854c0228c15c08aeb70bbabe9ed87bbb7289fab31b13cabac15138bb71179553e06080b83f4a813fbdaf614f63293beea3fc73fe865da6551193fa4d38de04\"}" + + val otsEvent2Digest = "a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6" + + @Test + fun verifyNostrEvent() { + val ots = Event.Companion.fromJson(otsEvent) as OtsEvent + println(ots.info()) + assertEquals(1707688818L, ots.verify()) + } + + @Test + fun verifyNostrEvent2() { + val ots = Event.Companion.fromJson(otsEvent2) as OtsEvent + println(ots.info()) + assertEquals(1706322179L, ots.verify()) + } + + @Test + fun verifyNostrPendingEvent() { + val ots = Event.Companion.fromJson(otsPendingEvent) as OtsEvent + println(ots.info()) + assertEquals(null, ots.verify()) + } + + @Ignore("Need to figure out a way to wait for Bitcoin confirmations to test thiss") + @Test + fun createOTSEventAndVerify() { + val signer = NostrSignerInternal(KeyPair()) + var ots: OtsEvent? = null + + val countDownLatch = CountDownLatch(1) + + OtsEvent.create(otsEvent2Digest, signer) { + ots = it + countDownLatch.countDown() + } + + Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) + + println(ots!!.toJson()) + println(ots!!.info()) + + assertEquals(1706322179L, ots!!.verify()) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt index 05edf3688..ae0b8e962 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt @@ -111,6 +111,7 @@ class EventFactory { MetadataEvent.KIND -> MetadataEvent(id, pubKey, createdAt, tags, content, sig) MuteListEvent.KIND -> MuteListEvent(id, pubKey, createdAt, tags, content, sig) NNSEvent.KIND -> NNSEvent(id, pubKey, createdAt, tags, content, sig) + OtsEvent.KIND -> OtsEvent(id, pubKey, createdAt, tags, content, sig) PeopleListEvent.KIND -> PeopleListEvent(id, pubKey, createdAt, tags, content, sig) PinListEvent.KIND -> PinListEvent(id, pubKey, createdAt, tags, content, sig) PollNoteEvent.KIND -> PollNoteEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/OtsEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/OtsEvent.kt new file mode 100644 index 000000000..a88bc406d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/OtsEvent.kt @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2024 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.events + +import android.util.Log +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.encoders.hexToByteArray +import com.vitorpamplona.quartz.ots.DetachedTimestampFile +import com.vitorpamplona.quartz.ots.Hash +import com.vitorpamplona.quartz.ots.OpenTimestamps +import com.vitorpamplona.quartz.ots.VerifyResult +import com.vitorpamplona.quartz.ots.op.OpSHA256 +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlinx.coroutines.CancellationException +import java.util.Base64 + +@Immutable +class OtsEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { + fun digestEvent() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1) + + fun digest() = digestEvent()?.hexToByteArray() + + fun otsByteArray(): ByteArray { + return Base64.getDecoder().decode(content) + } + + fun verify(): Long? { + try { + val digest = digest() ?: return null + + val detachedOts = DetachedTimestampFile.deserialize(otsByteArray()) + + val result = OpenTimestamps.verify(detachedOts, digest) + if (result == null || result.isEmpty()) { + return null + } else { + return result.get(VerifyResult.Chains.BITCOIN)?.timestamp + } + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e("OpenTimeStamps", "Failed to verify", e) + return null + } + } + + fun info(): String { + val detachedOts = DetachedTimestampFile.deserialize(otsByteArray()) + return OpenTimestamps.info(detachedOts) + } + + companion object { + const val KIND = 1040 + const val ALT = "Opentimestamps Attestation" + + fun stamp(eventId: HexKey): ByteArray { + val hash = Hash(eventId.hexToByteArray(), OpSHA256._TAG) + val file = DetachedTimestampFile.from(hash) + val timestamp = OpenTimestamps.stamp(file) + val detachedToSerialize = DetachedTimestampFile(hash.getOp(), timestamp) + return detachedToSerialize.serialize() + } + + fun create( + eventId: HexKey, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (OtsEvent) -> Unit, + ) { + val otsFile = stamp(eventId) + val tags = + arrayOf( + arrayOf("e", eventId), + arrayOf("alt", ALT), + ) + signer.sign(createdAt, KIND, tags, Base64.getEncoder().encodeToString(otsFile), onReady) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/BlockHeader.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/BlockHeader.java new file mode 100644 index 000000000..3d0983d1f --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/BlockHeader.java @@ -0,0 +1,73 @@ +package com.vitorpamplona.quartz.ots; + +public class BlockHeader { + + private String merkleroot; + private String blockHash; + private String time; + + public void setTime(String time) { + this.time = time; + } + + public Long getTime() { + return Long.valueOf(time); + } + + public String getMerkleroot() { + return merkleroot; + } + + public void setMerkleroot(String merkleroot) { + this.merkleroot = merkleroot; + } + + public String getBlockHash() { + return blockHash; + } + + public void setBlockHash(String blockHash) { + this.blockHash = blockHash; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + BlockHeader that = (BlockHeader) o; + + if (merkleroot != null ? !merkleroot.equals(that.merkleroot) : that.merkleroot != null) { + return false; + } + + if (blockHash != null ? !blockHash.equals(that.blockHash) : that.blockHash != null) { + return false; + } + + return time != null ? time.equals(that.time) : that.time == null; + } + + @Override + public int hashCode() { + int result = merkleroot != null ? merkleroot.hashCode() : 0; + result = 31 * result + (blockHash != null ? blockHash.hashCode() : 0); + result = 31 * result + (time != null ? time.hashCode() : 0); + + return result; + } + + @Override + public String toString() { + return "BlockHeader{" + + "merkleroot='" + merkleroot + '\'' + + ", blockHash='" + blockHash + '\'' + + ", time='" + time + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/Calendar.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/Calendar.java new file mode 100644 index 000000000..8de9f03b8 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/Calendar.java @@ -0,0 +1,120 @@ +package com.vitorpamplona.quartz.ots; + +import com.vitorpamplona.quartz.ots.exceptions.CommitmentNotFoundException; +import com.vitorpamplona.quartz.ots.exceptions.DeserializationException; +import com.vitorpamplona.quartz.ots.exceptions.ExceededSizeException; +import com.vitorpamplona.quartz.ots.exceptions.UrlException; +import com.vitorpamplona.quartz.ots.http.Request; +import com.vitorpamplona.quartz.ots.http.Response; + +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +/** + * Class representing remote calendar server interface. + */ +public class Calendar { + + private String url; + + + /** + * Create a RemoteCalendar. + * + * @param url The server url. + */ + public Calendar(String url) { + this.url = url; + } + + /** + * Get calendar url. + * + * @return The calendar url. + */ + public String getUrl() { + return this.url; + } + + /** + * Submitting a digest to remote calendar. Returns a com.eternitywall.ots.Timestamp committing to that digest. + * + * @param digest The digest hash to send. + * @return the Timestamp received from the calendar. + * @throws ExceededSizeException if response is too big. + * @throws UrlException if url is not reachable. + * @throws DeserializationException if the data is corrupt + */ + public Timestamp submit(byte[] digest) + throws ExceededSizeException, UrlException, DeserializationException { + try { + Map headers = new HashMap<>(); + headers.put("Accept", "application/vnd.opentimestamps.v1"); + headers.put("User-Agent", "java-opentimestamps"); + headers.put("Content-Type", "application/x-www-form-urlencoded"); + + URL obj = new URL(url + "/digest"); + Request task = new Request(obj); + task.setData(digest); + task.setHeaders(headers); + Response response = task.call(); + byte[] body = response.getBytes(); + if (body.length > 10000) { + throw new ExceededSizeException("Calendar response exceeded size limit"); + } + + StreamDeserializationContext ctx = new StreamDeserializationContext(body); + return Timestamp.deserialize(ctx, digest); + } catch (ExceededSizeException | DeserializationException e) + { + throw e; + } + catch (Exception e) { + throw new UrlException(e.getMessage()); + } + } + + /** + * Get a timestamp for a given commitment. + * + * @param commitment The digest hash to send. + * @return the Timestamp from the calendar server (with blockchain information if already written). + * @throws ExceededSizeException if response is too big. + * @throws UrlException if url is not reachable. + * @throws CommitmentNotFoundException if commit is not found. + * @throws DeserializationException if the data is corrupt + */ + public Timestamp getTimestamp(byte[] commitment) throws DeserializationException, ExceededSizeException, CommitmentNotFoundException, UrlException { + try { + Map headers = new HashMap<>(); + headers.put("Accept", "application/vnd.opentimestamps.v1"); + headers.put("User-Agent", "java-opentimestamps"); + headers.put("Content-Type", "application/x-www-form-urlencoded"); + + URL obj = new URL(url + "/timestamp/" + Utils.bytesToHex(commitment).toLowerCase()); + Request task = new Request(obj); + task.setHeaders(headers); + Response response = task.call(); + byte[] body = response.getBytes(); + if (body.length > 10000) { + throw new ExceededSizeException("Calendar response exceeded size limit"); + } + + if (!response.isOk()) { + throw new CommitmentNotFoundException("com.eternitywall.ots.Calendar response a status code != 200 which is: " + response.getStatus()); + } + + StreamDeserializationContext ctx = new StreamDeserializationContext(body); + + return Timestamp.deserialize(ctx, commitment); + } + catch (DeserializationException | ExceededSizeException | CommitmentNotFoundException e) + { + throw e; + } + catch (Exception e) { + throw new UrlException(e.getMessage()); + } + } +} \ No newline at end of file diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/CalendarAsyncSubmit.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/CalendarAsyncSubmit.java new file mode 100644 index 000000000..9d772fe84 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/CalendarAsyncSubmit.java @@ -0,0 +1,59 @@ +package com.vitorpamplona.quartz.ots; + +import android.util.Log; + +import com.vitorpamplona.quartz.ots.http.Request; +import com.vitorpamplona.quartz.ots.http.Response; + +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Callable; + +/** + * For making async calls to a calendar server + */ +public class CalendarAsyncSubmit implements Callable> { + + private String url; + private byte[] digest; + private BlockingQueue> queue; + + public CalendarAsyncSubmit(String url, byte[] digest) { + this.url = url; + this.digest = digest; + } + + public void setQueue(BlockingQueue> queue) { + this.queue = queue; + } + + @Override + public Optional call() throws Exception { + Map headers = new HashMap<>(); + headers.put("Accept", "application/vnd.opentimestamps.v1"); + headers.put("User-Agent", "java-opentimestamps"); + headers.put("Content-Type", "application/x-www-form-urlencoded"); + + URL obj = new URL(url + "/digest"); + Request task = new Request(obj); + task.setData(digest); + task.setHeaders(headers); + Response response = task.call(); + + if (response.isOk()) { + byte[] body = response.getBytes(); + StreamDeserializationContext ctx = new StreamDeserializationContext(body); + Timestamp timestamp = Timestamp.deserialize(ctx, digest); + Optional of = Optional.of(timestamp); + queue.add(of); + return of; + } + + queue.add(Optional.empty()); + + return Optional.empty(); + } +} \ No newline at end of file diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/DetachedTimestampFile.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/DetachedTimestampFile.java new file mode 100644 index 000000000..1b212316d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/DetachedTimestampFile.java @@ -0,0 +1,227 @@ +package com.vitorpamplona.quartz.ots; + +import com.vitorpamplona.quartz.ots.exceptions.DeserializationException; +import com.vitorpamplona.quartz.ots.op.Op; +import com.vitorpamplona.quartz.ots.op.OpCrypto; +import com.vitorpamplona.quartz.ots.op.OpSHA256; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.security.NoSuchAlgorithmException; + +/** + * Class representing Detached com.vitorpamplona.quartz.ots.Timestamp File. + * A file containing a timestamp for another file. + * Contains a timestamp, along with a header and the digest of the file. + */ +public class DetachedTimestampFile { + + /** + * Header magic bytes Designed to be give the user some information in a hexdump, while being + * identified as 'data' by the file utility. + * + * @default \x00OpenTimestamps\x00\x00Proof\x00\xbf\x89\xe2\xe8\x84\xe8\x92\x94 + */ + static byte[] HEADER_MAGIC = {(byte) 0x00, (byte) 0x4f, (byte) 0x70, (byte) 0x65, (byte) 0x6e, + (byte) 0x54, (byte) 0x69, (byte) 0x6d, (byte) 0x65, (byte) 0x73, + (byte) 0x74, (byte) 0x61, (byte) 0x6d, (byte) 0x70, (byte) 0x73, (byte) 0x00, (byte) 0x00, + (byte) 0x50, (byte) 0x72, (byte) 0x6f, (byte) 0x6f, (byte) 0x66, (byte) 0x00, + (byte) 0xbf, (byte) 0x89, (byte) 0xe2, (byte) 0xe8, (byte) 0x84, (byte) 0xe8, (byte) 0x92, + (byte) 0x94}; + + /** + * While the git commit timestamps have a minor version, probably better to + * leave it out here: unlike Git commits round-tripping is an issue when + * timestamps are upgraded, and we could end up with bugs related to not + * saving/updating minor version numbers correctly. + * + * @default 1 + */ + static byte MAJOR_VERSION = 1; + + Op fileHashOp; + Timestamp timestamp; + + public DetachedTimestampFile(Op fileHashOp, Timestamp timestamp) { + this.fileHashOp = fileHashOp; + this.timestamp = timestamp; + } + + /** + * The digest of the file that was timestamped. + * + * @return The message inside the timestamp. + */ + public byte[] fileDigest() { + return this.timestamp.msg; + } + + /** + * Retrieve the internal timestamp. + * + * @return the timestamp. + */ + public Timestamp getTimestamp() { + return this.timestamp; + } + + /** + * Serialize a com.vitorpamplona.quartz.ots.Timestamp File. + * + * @param ctx The stream serialization context. + */ + public void serialize(StreamSerializationContext ctx) { + ctx.writeBytes(HEADER_MAGIC); + ctx.writeVaruint(MAJOR_VERSION); + this.fileHashOp.serialize(ctx); + ctx.writeBytes(this.timestamp.msg); + this.timestamp.serialize(ctx); + } + + /** + * Serialize a com.vitorpamplona.quartz.ots.Timestamp File. + * + * @return The byte array of serialized data. + */ + public byte[] serialize() { + StreamSerializationContext ctx = new StreamSerializationContext(); + this.serialize(ctx); + + return ctx.getOutput(); + } + + /** + * Deserialize a com.vitorpamplona.quartz.ots.Timestamp File. + * + * @param ctx The stream deserialization context. + * @return The generated com.vitorpamplona.quartz.ots.DetachedTimestampFile object. + */ + public static DetachedTimestampFile deserialize(StreamDeserializationContext ctx) throws DeserializationException { + ctx.assertMagic(HEADER_MAGIC); + ctx.readVaruint(); + + OpCrypto fileHashOp = (OpCrypto) OpCrypto.deserialize(ctx); + byte[] fileHash = ctx.readBytes(fileHashOp._DIGEST_LENGTH()); + Timestamp timestamp = Timestamp.deserialize(ctx, fileHash); + + ctx.assertEof(); + + return new DetachedTimestampFile(fileHashOp, timestamp); + } + + /** + * Deserialize a com.vitorpamplona.quartz.ots.Timestamp File. + * + * @param ots The byte array of deserialization DetachedFileTimestamped. + * @return The generated com.vitorpamplona.quartz.ots.DetachedTimestampFile object. + */ + public static DetachedTimestampFile deserialize(byte[] ots) throws DeserializationException { + StreamDeserializationContext ctx = new StreamDeserializationContext(ots); + + return DetachedTimestampFile.deserialize(ctx); + } + + /** + * Read the Detached com.vitorpamplona.quartz.ots.Timestamp File from bytes. + * + * @param fileHashOp The file hash operation. + * @param ctx The stream deserialization context. + * @return The generated com.vitorpamplona.quartz.ots.DetachedTimestampFile object. + * @throws NoSuchAlgorithmException desc + */ + public static DetachedTimestampFile from(OpCrypto fileHashOp, StreamDeserializationContext ctx) throws NoSuchAlgorithmException { + byte[] fdHash = fileHashOp.hashFd(ctx); + + return new DetachedTimestampFile(fileHashOp, new Timestamp(fdHash)); + } + + /** + * Read the Detached com.vitorpamplona.quartz.ots.Timestamp File from bytes. + * + * @param fileHashOp The file hash operation. + * @param bytes The byte array of data to hash + * @return The generated com.vitorpamplona.quartz.ots.DetachedTimestampFile object. + * @throws NoSuchAlgorithmException desc + */ + public static DetachedTimestampFile from(OpCrypto fileHashOp, byte[] bytes) throws Exception { + byte[] fdHash = fileHashOp.hashFd(bytes); + + return new DetachedTimestampFile(fileHashOp, new Timestamp(fdHash)); + } + + /** + * Read the Detached com.vitorpamplona.quartz.ots.Timestamp File from hash. + * + * @param inputStream The InputStream of the file to hash + * @return The generated com.vitorpamplona.quartz.ots.DetachedTimestampFile object. + * @throws Exception if the input stream is null + */ + public static DetachedTimestampFile from(InputStream inputStream) throws Exception { + if (inputStream == null) { + throw new Exception(); // TODO: Add exception string later on + } + + try { + DetachedTimestampFile fileTimestamp = DetachedTimestampFile.from(new OpSHA256(), inputStream); // Read from file reader stream + return fileTimestamp; + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + throw new Exception(); + } + } + + /** + * Read the Detached com.vitorpamplona.quartz.ots.Timestamp File from InputStream. + * + * @param fileHashOp The file hash operation. + * @param inputStream The input stream file. + * @return The generated com.vitorpamplona.quartz.ots.DetachedTimestampFile object. + * @throws IOException desc + * @throws NoSuchAlgorithmException desc + */ + public static DetachedTimestampFile from(OpCrypto fileHashOp, InputStream inputStream) throws IOException, NoSuchAlgorithmException { + byte[] fdHash = fileHashOp.hashFd(inputStream); + + return new DetachedTimestampFile(fileHashOp, new Timestamp(fdHash)); + } + + /** + * Read the Detached com.vitorpamplona.quartz.ots.Timestamp File from hash. + * + * @param hash The hash of the file. + * @return The generated com.vitorpamplona.quartz.ots.DetachedTimestampFile object. + */ + public static DetachedTimestampFile from(Hash hash) { + return new DetachedTimestampFile(hash.getOp(), new Timestamp(hash.getValue())); + } + + /** + * Read the Detached com.vitorpamplona.quartz.ots.Timestamp File from File. + * + * @param fileHashOp The file hash operation. + * @param file The hash file. + * @return The generated com.vitorpamplona.quartz.ots.DetachedTimestampFile object. + * @throws IOException desc + * @throws NoSuchAlgorithmException desc + */ + public static DetachedTimestampFile from(OpCrypto fileHashOp, File file) throws IOException, NoSuchAlgorithmException { + byte[] fdHash = fileHashOp.hashFd(file); + + return new DetachedTimestampFile(fileHashOp, new Timestamp(fdHash)); + } + + /** + * Print the object. + * + * @return The output. + */ + @Override + public String toString() { + String output = "com.vitorpamplona.quartz.ots.DetachedTimestampFile\n"; + output += "fileHashOp: " + this.fileHashOp.toString() + '\n'; + output += "timestamp: " + this.timestamp.toString() + '\n'; + + return output; + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/Esplora.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/Esplora.java new file mode 100644 index 000000000..f2ca8e6e6 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/Esplora.java @@ -0,0 +1,66 @@ +package com.vitorpamplona.quartz.ots; + +import com.vitorpamplona.quartz.ots.http.Request; +import com.vitorpamplona.quartz.ots.http.Response; + +import org.json.JSONObject; + +import java.net.URL; +import java.util.concurrent.*; +import java.util.logging.Logger; + +public class Esplora { + + private static final Logger log = Utils.getLogger(Esplora.class.getName()); + private static final String esploraUrl = "https://blockstream.info/api"; + + /** + * Retrieve the block information from the block hash. + * + * @param hash Hash of the block. + * @return the blockheader of the hash + * @throws Exception desc + */ + public static BlockHeader block(final String hash) throws Exception { + final URL url = new URL(esploraUrl + "/block/" + hash); + final Request task = new Request(url); + final ExecutorService executor = Executors.newSingleThreadExecutor(); + final Future future = executor.submit(task); + final Response take = future.get(); + executor.shutdown(); + if (!take.isOk()) + throw new Exception(); + + final JSONObject jsonObject = take.getJson(); + final String merkleroot = jsonObject.getString("merkle_root"); + final String time = String.valueOf(jsonObject.getInt("timestamp")); + final BlockHeader blockHeader = new BlockHeader(); + blockHeader.setMerkleroot(merkleroot); + blockHeader.setTime(time); + blockHeader.setBlockHash(hash); + log.info(take.getFromUrl() + " " + blockHeader); + return blockHeader; + //log.warning("Cannot parse merkleroot from body: " + jsonObject + ": " + e.getMessage()); + } + + /** + * Retrieve the block hash from the block height. + * + * @param height Height of the block. + * @return the hash of the block at height height + * @throws Exception desc + */ + public static String blockHash(final Integer height) throws Exception { + final URL url = new URL(esploraUrl + "/block-height/" + height); + final Request task = new Request(url); + final ExecutorService executor = Executors.newSingleThreadExecutor(); + final Future future = executor.submit(task); + final Response take = future.get(); + executor.shutdown(); + if (!take.isOk()) + throw new Exception(); + final String blockHash = take.getString(); + log.info(take.getFromUrl() + " " + blockHash); + return blockHash; + } +} \ No newline at end of file diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/Hash.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/Hash.java new file mode 100644 index 000000000..4d1375f5f --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/Hash.java @@ -0,0 +1,198 @@ +package com.vitorpamplona.quartz.ots; + +import com.vitorpamplona.quartz.ots.op.OpCrypto; +import com.vitorpamplona.quartz.ots.op.OpKECCAK256; +import com.vitorpamplona.quartz.ots.op.OpRIPEMD160; +import com.vitorpamplona.quartz.ots.op.OpSHA1; +import com.vitorpamplona.quartz.ots.op.OpSHA256; +import com.vitorpamplona.quartz.encoders.Hex; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.security.NoSuchAlgorithmException; + +public class Hash { + + private byte[] value; + private byte algorithm; + + /** + * Create a Hash object. + * + * @param value - The byte array of the hash + * @param algorithm - The hashlib tag of crypto operation + */ + public Hash(byte[] value, byte algorithm) { + this.value = value; + this.algorithm = algorithm; + } + + /** + * Create a Hash object. + * + * @param value - The byte array of the hash + * @param label - The hashlib name of crypto operation + */ + public Hash(byte[] value, String label) { + this.value = value; + this.algorithm = getOp(label)._TAG(); + } + + /** + * Get Value. + * + * @return value - The hash in byte array. + */ + public byte[] getValue() { + return value; + } + + /** + * Set Value tag. + * + * @param value - The hash in byte array. + */ + public void setValue(byte[] value) { + this.value = value; + } + + /** + * Get Algorithm tag. + * + * @return algorithm - The algorithm tag of crypto operation. + */ + public byte getAlgorithm() { + return algorithm; + } + + /** + * Set Algorithm tag. + * + * @param algorithm - The algorithm tag of crypto operation. + */ + public void setAlgorithm(byte algorithm) { + this.algorithm = algorithm; + } + + /** + * Get the current Crypto operation. + * + * @return The generated com.vitorpamplona.quartz.ots.OpCrypto object. + */ + public OpCrypto getOp() { + if (this.algorithm == OpSHA1._TAG) { + return new OpSHA1(); + } else if (this.algorithm == OpSHA256._TAG) { + return new OpSHA256(); + } else if (this.algorithm == OpRIPEMD160._TAG) { + return new OpRIPEMD160(); + } else if (this.algorithm == OpKECCAK256._TAG) { + return new OpKECCAK256(); + } + + return new OpSHA256(); + } + + /** + * Get Crypto operation from hashlib tag. + * + * @param algorithm The hashlib tag. + * @return The generated com.vitorpamplona.quartz.ots.OpCrypto object. + */ + public static OpCrypto getOp(byte algorithm) { + if (algorithm == OpSHA1._TAG) { + return new OpSHA1(); + } else if (algorithm == OpSHA256._TAG) { + return new OpSHA256(); + } else if (algorithm == OpRIPEMD160._TAG) { + return new OpRIPEMD160(); + } else if (algorithm == OpKECCAK256._TAG) { + return new OpKECCAK256(); + } + + return new OpSHA256(); + } + + /** + * Get Crypto operation from hashlib name. + * + * @param label The hashlib name. + * @return The generated com.vitorpamplona.quartz.ots.OpCrypto object. + */ + public static OpCrypto getOp(String label) { + if (label.toLowerCase().equals(new OpSHA1()._TAG_NAME())) { + return new OpSHA1(); + } else if (label.toLowerCase().equals(new OpSHA256()._TAG_NAME())) { + return new OpSHA256(); + } else if (label.toLowerCase().equals(new OpRIPEMD160()._TAG_NAME())) { + return new OpRIPEMD160(); + } else if (label.toLowerCase().equals(new OpKECCAK256()._TAG_NAME())) { + return new OpKECCAK256(); + } + + return new OpSHA256(); + } + + /** + * Build hash from data. + * + * @param bytes The byte array of data to hash. + * @param algorithm The hash file. + * @return The generated com.vitorpamplona.quartz.ots.Hash object. + * @throws IOException desc + * @throws NoSuchAlgorithmException desc + */ + public static Hash from(byte[] bytes, byte algorithm) throws IOException, NoSuchAlgorithmException { + OpCrypto opCrypto = getOp(algorithm); + byte[] value = opCrypto.hashFd(bytes); + + return new Hash(value, algorithm); + } + + /** + * Build hash from File. + * + * @param file The File of data to hash. + * @param algorithm The hash file. + * @return The generated com.vitorpamplona.quartz.ots.Hash object. + * @throws IOException desc + * @throws NoSuchAlgorithmException desc + */ + public static Hash from(File file, byte algorithm) throws IOException, NoSuchAlgorithmException { + OpCrypto opCrypto = getOp(algorithm); + byte[] value = opCrypto.hashFd(file); + + return new Hash(value, algorithm); + } + + /** + * Build hash from InputStream. + * + * @param inputStream The InputStream of data to hash. + * @param algorithm The hash file. + * @return The generated com.vitorpamplona.quartz.ots.Hash object. + * @throws IOException desc + * @throws NoSuchAlgorithmException desc + */ + public static Hash from(InputStream inputStream, byte algorithm) throws IOException, NoSuchAlgorithmException { + OpCrypto opCrypto = getOp(algorithm); + byte[] value = opCrypto.hashFd(inputStream); + + return new Hash(value, algorithm); + } + + /** + * Print the object. + * + * @return The output. + */ + @Override + public String toString() { + String output = "com.vitorpamplona.quartz.ots.Hash\n"; + output += "algorithm: " + this.getOp()._HASHLIB_NAME() + '\n'; + output += "value: " + Hex.encode(this.value) + '\n'; + + return output; + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/Merkle.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/Merkle.java new file mode 100644 index 000000000..59222c5e4 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/Merkle.java @@ -0,0 +1,95 @@ +package com.vitorpamplona.quartz.ots; + +import com.vitorpamplona.quartz.ots.op.OpAppend; +import com.vitorpamplona.quartz.ots.op.OpPrepend; +import com.vitorpamplona.quartz.ots.op.OpSHA256; + +import java.util.ArrayList; +import java.util.List; + +/** + * Utility functions for merkle trees + */ +public class Merkle { + + /** + * Concatenate left and right, then perform a unary operation on them left and right can be either timestamps or bytes. + * Appropriate intermediary append/prepend operations will be created as needed for left and right. + * + * @param left the left timestamp parameter + * @param right the right timestamp parameter + * @return the concatenation of left and right + */ + public static Timestamp catThenUnaryOp(Timestamp left, Timestamp right) { + // rightPrependStamp = right.ops.add(OpPrepend(left.msg)) + Timestamp rightPrependStamp = right.add(new OpPrepend(left.msg)); + + // Left and right should produce the same thing, so we can set the timestamp of the left to the right. + // left.ops[OpAppend(right.msg)] = right_prepend_stamp + // leftAppendStamp = left.ops.add(OpAppend(right.msg)) + //Timestamp leftPrependStamp = left.add(new OpAppend(right.msg)); + left.ops.put(new OpAppend(right.msg), rightPrependStamp); + + // return rightPrependStamp.ops.add(unaryOpCls()) + Timestamp res = rightPrependStamp.add(new OpSHA256()); + return res; + } + + public static Timestamp catSha256(Timestamp left, Timestamp right) { + return Merkle.catThenUnaryOp(left, right); + } + + public static Timestamp catSha256d(Timestamp left, Timestamp right) { + Timestamp sha256Timestamp = Merkle.catSha256(left, right); + // res = sha256Timestamp.ops.add(OpSHA256()); + Timestamp res = sha256Timestamp.add(new OpSHA256()); + return res; + } + + /** + * Merkelize a set of timestamps. + * A merkle tree of all the timestamps is built in-place using binop() to + * timestamp each pair of timestamps. The exact algorithm used is structurally + * identical to a merkle-mountain-range, although leaf sums aren't committed. + * As this function is under the consensus-critical core, it's guaranteed that + * the algorithm will not be changed in the future. + * + * @param timestamps a list of timestamps + * @return the timestamp for the tip of the tree. + */ + public static Timestamp makeMerkleTree(List timestamps) { + List stamps = timestamps; + Timestamp prevStamp = null; + boolean exit = false; + + while (!exit) { + if (stamps.size() > 0) { + prevStamp = stamps.get(0); + } + + List subStamps = stamps.subList(1, stamps.size()); + List nextStamps = new ArrayList<>(); + + for (Timestamp stamp : subStamps) { + if (prevStamp == null) { + prevStamp = stamp; + } else { + nextStamps.add(Merkle.catSha256(prevStamp, stamp)); + prevStamp = null; + } + } + + if (nextStamps.size() == 0) { + exit = true; + } else { + if (prevStamp != null) { + nextStamps.add(prevStamp); + } + + stamps = nextStamps; + } + } + + return prevStamp; + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/OpenTimestamps.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/OpenTimestamps.java new file mode 100644 index 000000000..f2ba09282 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/OpenTimestamps.java @@ -0,0 +1,475 @@ +package com.vitorpamplona.quartz.ots; + +import android.util.Log; + +import com.vitorpamplona.quartz.ots.attestation.BitcoinBlockHeaderAttestation; +import com.vitorpamplona.quartz.ots.attestation.EthereumBlockHeaderAttestation; +import com.vitorpamplona.quartz.ots.attestation.LitecoinBlockHeaderAttestation; +import com.vitorpamplona.quartz.ots.attestation.PendingAttestation; +import com.vitorpamplona.quartz.ots.attestation.TimeAttestation; +import com.vitorpamplona.quartz.encoders.Hex; +import com.vitorpamplona.quartz.ots.exceptions.VerificationException; +import com.vitorpamplona.quartz.ots.op.OpAppend; +import com.vitorpamplona.quartz.ots.op.OpCrypto; +import com.vitorpamplona.quartz.ots.op.OpSHA256; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * The main class for timestamp operations. + */ +public class OpenTimestamps { + + /** + * Show information on a detached timestamp. + * + * @param detachedTimestampFile The DetachedTimestampFile ots. + * @return the string representation of the timestamp. + */ + public static String info(DetachedTimestampFile detachedTimestampFile) { + return info(detachedTimestampFile, false); + } + + /** + * Show information on a detached timestamp with verbose option. + * + * @param detachedTimestampFile The DetachedTimestampFile ots. + * @param verbose Show verbose output. + * @return the string representation of the timestamp. + */ + public static String info(DetachedTimestampFile detachedTimestampFile, boolean verbose) { + if (detachedTimestampFile == null) { + return "No ots file"; + } + + String fileHash = Utils.bytesToHex(detachedTimestampFile.timestamp.msg).toLowerCase(); + String hashOp = ((OpCrypto) detachedTimestampFile.fileHashOp)._TAG_NAME(); + + String firstLine = "File " + hashOp + " hash: " + fileHash + '\n'; + + return firstLine + "Timestamp:\n" + detachedTimestampFile.timestamp.strTree(0, verbose); + } + + /** + * Show information on a timestamp. + * + * @param timestamp The timestamp buffered. + * @return the string representation of the timestamp. + */ + public static String info(Timestamp timestamp) { + if (timestamp == null) { + return "No timestamp"; + } + + String fileHash = Utils.bytesToHex(timestamp.msg).toLowerCase(); + String firstLine = "Hash: " + fileHash + '\n'; + + return firstLine + "Timestamp:\n" + timestamp.strTree(0); + } + + /** + * Create timestamp with the aid of a remote calendar. May be specified multiple times. + * + * @param fileTimestamp The Detached Timestamp File. + * @return The plain array buffer of stamped. + * @throws IOException if fileTimestamp is not valid, or the stamp procedure fails. + */ + public static Timestamp stamp(DetachedTimestampFile fileTimestamp) throws IOException { + return OpenTimestamps.stamp(fileTimestamp, null, 0); + } + + /** + * Create timestamp with the aid of a remote calendar. May be specified multiple times. + * + * @param fileTimestamp The timestamp to stamp. + * @param calendarsUrl The list of calendar urls. + * @param m The number of calendar to use. + * @return The plain array buffer of stamped. + * @throws IOException if fileTimestamp is not valid, or the stamp procedure fails. + */ + public static Timestamp stamp(DetachedTimestampFile fileTimestamp, List calendarsUrl, Integer m) throws IOException { + List fileTimestamps = new ArrayList(); + fileTimestamps.add(fileTimestamp); + + return OpenTimestamps.stamp(fileTimestamps, calendarsUrl, m); + } + + /** + * Create timestamp with the aid of a remote calendar. May be specified multiple times. + * + * @param fileTimestamps The list of timestamp to stamp. + * @param calendarsUrl The list of calendar urls. + * @param m The number of calendar to use. + * @return The plain array buffer of stamped. + * @throws IOException if fileTimestamp is not valid, or the stamp procedure fails. + */ + public static Timestamp stamp(List fileTimestamps, List calendarsUrl, Integer m) throws IOException { + // Parse parameters + if (fileTimestamps == null || fileTimestamps.size() == 0) { + throw new IOException(); + } + + if (calendarsUrl == null || calendarsUrl.size() == 0) { + calendarsUrl = new ArrayList(); + calendarsUrl.add("https://alice.btc.calendar.opentimestamps.org"); + calendarsUrl.add("https://bob.btc.calendar.opentimestamps.org"); + calendarsUrl.add("https://finney.calendar.eternitywall.com"); + } + + if (m == null || m <= 0) { + if (calendarsUrl.size() == 0) { + m = 2; + } else if (calendarsUrl.size() == 1) { + m = 1; + } else { + m = calendarsUrl.size(); + } + } + + if (m < 0 || m > calendarsUrl.size()) { + Log.e("OpenTimestamp", "m cannot be greater than available calendar neither less or equal 0"); + throw new IOException(); + } + + // Build markle tree + Timestamp merkleTip = OpenTimestamps.makeMerkleTree(fileTimestamps); + + if (merkleTip == null) { + throw new IOException(); + } + + // Stamping + Timestamp resultTimestamp = OpenTimestamps.create(merkleTip, calendarsUrl, m); + + if (resultTimestamp == null) { + throw new IOException(); + } + + // Result of timestamp serialization + if (fileTimestamps.size() == 1) { + return fileTimestamps.get(0).timestamp; + } else { + return merkleTip; + } + } + + /** + * Create a timestamp + * + * @param timestamp The timestamp. + * @param calendarUrls List of calendar's to use. + * @param m Number of calendars to use. + * @return The created timestamp. + */ + private static Timestamp create(Timestamp timestamp, List calendarUrls, Integer m) { + int capacity = calendarUrls.size(); + ExecutorService executor = Executors.newFixedThreadPool(4); + ArrayBlockingQueue> queue = new ArrayBlockingQueue<>(capacity); + + // Submit to all public calendars + for (final String calendarUrl : calendarUrls) { + Log.i("OpenTimestamps", "Submitting to remote calendar " + calendarUrl); + + try { + CalendarAsyncSubmit task = new CalendarAsyncSubmit(calendarUrl, timestamp.msg); + task.setQueue(queue); + executor.submit(task); + } catch (Exception e) { + e.printStackTrace(); + } + } + + int count = 0; + + for (count = 0; count < capacity && count < m; count++) { + try { + Optional optionalStamp = queue.take(); + + if (optionalStamp.isPresent()) { + try { + Timestamp time = optionalStamp.get(); + timestamp.merge(time); + Log.i("Open", ""+ timestamp.attestations.size()); + } catch (Exception e) { + e.printStackTrace(); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + if (count < m) { + Log.e("OpenTimestamp", "Failed to create timestamp: requested " + String.valueOf(m) + " attestation" + ((m > 1) ? "s" : "") + " but received only " + String.valueOf(count)); + } + + //shut down the executor service now + executor.shutdown(); + + return timestamp; + } + + /** + * Make Merkle Tree of detached timestamps. + * + * @param fileTimestamps The list of DetachedTimestampFile. + * @return merkle tip timestamp. + */ + public static Timestamp makeMerkleTree(List fileTimestamps) { + List merkleRoots = new ArrayList<>(); + + for (DetachedTimestampFile fileTimestamp : fileTimestamps) { + byte[] bytesRandom16 = new byte[16]; + + try { + bytesRandom16 = Utils.randBytes(16); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + + // nonce_appended_stamp = file_timestamp.timestamp.ops.add(com.vitorpamplona.quartz.ots.op.OpAppend(os.urandom(16))) + Timestamp nonceAppendedStamp = fileTimestamp.timestamp.add(new OpAppend(bytesRandom16)); + // merkle_root = nonce_appended_stamp.ops.add(com.vitorpamplona.quartz.ots.op.OpSHA256()) + Timestamp merkleRoot = nonceAppendedStamp.add(new OpSHA256()); + merkleRoots.add(merkleRoot); + } + + Timestamp merkleTip = Merkle.makeMerkleTree(merkleRoots); + return merkleTip; + } + + /** + * Compare and verify a detached timestamp. + * + * @param ots The DetachedTimestampFile containing the proof to verify. + * @param diggest The hash of the stamped file, in bytes + * @return Hashmap of block heights and timestamps indexed by chain: timestamp in seconds from 1 January 1970. + * @throws Exception if the verification procedure fails. + */ + + public static HashMap verify(DetachedTimestampFile ots, byte[] diggest) throws Exception { + if (!Arrays.equals(ots.fileDigest(), diggest)) { + Log.e("OpenTimestamp", "Expected digest " + Hex.encode(ots.fileDigest()).toLowerCase()); + Log.e("OpenTimestamp", "File does not match original!"); + throw new Exception("File does not match original!"); + } + + return OpenTimestamps.verify(ots.timestamp); + } + + /** + * Verify a timestamp. + * + * @param timestamp The timestamp. + * @return HashMap of block heights and timestamps indexed by chain: timestamp in seconds from 1 January 1970. + * @throws Exception if the verification procedure fails. + */ + public static HashMap verify(Timestamp timestamp) throws Exception { + HashMap verifyResults = new HashMap<>(); + + for (Map.Entry item : timestamp.allAttestations().entrySet()) { + byte[] msg = item.getKey(); + TimeAttestation attestation = item.getValue(); + VerifyResult verifyResult = null; + VerifyResult.Chains chain = null; + + try { + if (attestation instanceof BitcoinBlockHeaderAttestation) { + chain = VerifyResult.Chains.BITCOIN; + Long time = verify((BitcoinBlockHeaderAttestation) attestation, msg); + int height = ((BitcoinBlockHeaderAttestation) attestation).getHeight(); + verifyResult = new VerifyResult(time, height); + } else if (attestation instanceof LitecoinBlockHeaderAttestation) { + chain = VerifyResult.Chains.LITECOIN; + Long time = verify((LitecoinBlockHeaderAttestation) attestation, msg); + int height = ((LitecoinBlockHeaderAttestation) attestation).getHeight(); + verifyResult = new VerifyResult(time, height); + } + + if (verifyResult != null && verifyResults.containsKey(chain)) { + if (verifyResult.height < verifyResults.get(chain).height) { + verifyResults.put(chain, verifyResult); + } + } + + if (verifyResult != null && !verifyResults.containsKey(chain)) { + verifyResults.put(chain, verifyResult); + } + } catch (VerificationException e) { + throw e; + } catch (Exception e) { + String text = ""; + + if (chain == VerifyResult.Chains.BITCOIN) { + text = BitcoinBlockHeaderAttestation.chain; + } else if (chain == VerifyResult.Chains.LITECOIN) { + text = LitecoinBlockHeaderAttestation.chain; + } else if (chain == VerifyResult.Chains.ETHEREUM) { + text = EthereumBlockHeaderAttestation.chain; + } else { + throw e; + } + + Log.e("OpenTimestamp", Utils.toUpperFirstLetter(text) + " verification failed: " + e.getMessage()); + throw e; + } + } + return verifyResults; + } + + /** + * Verify an Bitcoin Block Header Attestation. + * if the node is not reachable or it fails, uses Lite-client verification. + * + * @param attestation The BitcoinBlockHeaderAttestation attestation. + * @param msg The digest to verify. + * @return The unix timestamp in seconds from 1 January 1970. + * @throws VerificationException if it doesn't check the merkle root of the block. + * @throws Exception if the verification procedure fails. + */ + public static Long verify(BitcoinBlockHeaderAttestation attestation, byte[] msg) throws VerificationException, Exception { + Integer height = attestation.getHeight(); + BlockHeader blockInfo; + + try { + String blockHash = Esplora.blockHash(height); + blockInfo = Esplora.block(blockHash); + Log.i("OpenTimestamps", "Lite-client verification, assuming block " + blockHash + " is valid"); + } catch (Exception e2) { + e2.printStackTrace(); + throw e2; + } + + return attestation.verifyAgainstBlockheader(Utils.arrayReverse(msg), blockInfo); + } + + /** + * Verify an Litecoin Block Header Attestation. Litecoin verification uses only lite-client verification. + * + * @param attestation The LitecoinBlockHeaderAttestation attestation. + * @param msg The digest to verify. + * @return The unix timestamp in seconds from 1 January 1970. + * @throws VerificationException if it doesn't check the merkle root of the block. + * @throws Exception if the verification procedure fails. + */ + public static Long verify(LitecoinBlockHeaderAttestation attestation, byte[] msg) throws VerificationException, Exception { + Integer height = attestation.getHeight(); + BlockHeader blockInfo; + + try { + String blockHash = Esplora.blockHash(height); + blockInfo = Esplora.block(blockHash); + Log.i("OpenTimestamps", "Lite-client verification, assuming block " + blockHash + " is valid"); + } catch (Exception e2) { + e2.printStackTrace(); + throw e2; + } + + return attestation.verifyAgainstBlockheader(Utils.arrayReverse(msg), blockInfo); + } + + /** + * Upgrade a timestamp. + * + * @param detachedTimestamp The DetachedTimestampFile containing the proof to verify. + * @return a boolean representing if the timestamp has changed. + * @throws Exception if the upgrading procedure fails. + */ + public static boolean upgrade(DetachedTimestampFile detachedTimestamp) throws Exception { + // Upgrade timestamp + boolean changed = OpenTimestamps.upgrade(detachedTimestamp.timestamp); + return changed; + } + + /** + * Attempt to upgrade an incomplete timestamp to make it verifiable. + * Note that this means if the timestamp that is already complete, False will be returned as nothing has changed. + * + * @param timestamp The timestamp to upgrade. + * @return a boolean representing if the timestamp has changed. + * @throws Exception if the upgrading procedure fails. + */ + public static boolean upgrade(Timestamp timestamp) throws Exception { + // Check remote calendars for upgrades. + // This time we only check PendingAttestations - we can't be as agressive. + + boolean upgraded = false; + Set existingAttestations = timestamp.getAttestations(); + + for (Timestamp subStamp : timestamp.directlyVerified()) { + for (TimeAttestation attestation : subStamp.attestations) { + if (attestation instanceof PendingAttestation && !subStamp.isTimestampComplete()) { + String calendarUrl = new String(((PendingAttestation) attestation).getUri(), StandardCharsets.UTF_8); + // var calendarUrl = calendarUrls[0]; + byte[] commitment = subStamp.msg; + + try { + Calendar calendar = new Calendar(calendarUrl); + Timestamp upgradedStamp = OpenTimestamps.upgrade(subStamp, calendar, commitment, existingAttestations); + + try { + subStamp.merge(upgradedStamp); + } catch (Exception e) { + e.printStackTrace(); + } + + upgraded = true; + } catch (Exception e) { + Log.i("OpenTimestamps", e.getMessage()); + } + } + } + } + + return upgraded; + } + + private static Timestamp upgrade(Timestamp subStamp, Calendar calendar, byte[] commitment, Set existingAttestations) throws Exception { + Timestamp upgradedStamp; + + try { + upgradedStamp = calendar.getTimestamp(commitment); + + if (upgradedStamp == null) { + throw new Exception("Invalid stamp"); + } + } catch (Exception e) { + Log.i("OpenTimestamps", "Calendar " + calendar.getUrl() + ": " + e.getMessage()); + throw e; + } + + Set attsFromRemote = upgradedStamp.getAttestations(); + + if (attsFromRemote.size() > 0) { + Log.i("OpenTimestamps", "Got 1 attestation(s) from " + calendar.getUrl()); + } + + // Set difference from remote attestations & existing attestations + Set newAttestations = attsFromRemote; + newAttestations.removeAll(existingAttestations); + + // changed & found_new_attestations + // foundNewAttestations = true; + // Log.i("OpenTimestamps", attsFromRemote.size + ' attestation(s) from ' + calendar.url); + + // Set union of existingAttestations & newAttestations + existingAttestations.addAll(newAttestations); + + return upgradedStamp; + // subStamp.merge(upgradedStamp); + // args.cache.merge(upgraded_stamp) + // sub_stamp.merge(upgraded_stamp) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/README.md b/quartz/src/main/java/com/vitorpamplona/quartz/ots/README.md new file mode 100644 index 000000000..cc839582c --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/README.md @@ -0,0 +1,17 @@ +This code came from https://github.com/opentimestamps/java-opentimestamps + +And includes modifications to avoid dependencies that do not work on Android + +— + +Original License + +The OpenTimestamps Client is free software: you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public License as published +by the Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +The OpenTimestamps Client is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License +below for more details. \ No newline at end of file diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/StreamDeserializationContext.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/StreamDeserializationContext.java new file mode 100644 index 000000000..b566d816f --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/StreamDeserializationContext.java @@ -0,0 +1,107 @@ +package com.vitorpamplona.quartz.ots; + +import com.vitorpamplona.quartz.ots.exceptions.DeserializationException; +import java.util.Arrays; +import java.util.logging.Logger; + +public class StreamDeserializationContext { + + byte[] buffer; + int counter = 0; + + public StreamDeserializationContext(byte[] stream) { + this.buffer = stream; + this.counter = 0; + } + + public byte[] getOutput() { + return this.buffer; + } + + public int getCounter() { + return this.counter; + } + + public byte[] read(int l) { + if (this.counter == this.buffer.length) { + return null; + } + + if (l+this.counter > this.buffer.length) { + l = this.buffer.length-this.counter; + } + + // const uint8Array = new Uint8Array(this.buffer,this.counter,l); + byte[] uint8Array = Arrays.copyOfRange(this.buffer, this.counter, this.counter + l); + this.counter += l; + + return uint8Array; + } + + public boolean readBool() { + byte b = this.read(1)[0]; + + if (b == 0xff) { + return true; + } else if (b == 0x00) { + return false; + } + + return false; + } + + public int readVaruint() { + int value = 0; + byte shift = 0; + byte b; + + do { + b = this.read(1)[0]; + value |= (b & 0b01111111) << shift; + shift += 7; + } while ((b & 0b10000000) != 0b00000000); + + return value; + } + + public byte[] readBytes(int expectedLength) throws DeserializationException { + + + if (expectedLength == 0) { + return this.readVarbytes(1024, 0); + } + + return this.read(expectedLength); + } + + public byte[] readVarbytes(int maxLen) throws DeserializationException { + return readVarbytes(maxLen, 0); + } + + public byte[] readVarbytes(int maxLen, int minLen) throws DeserializationException { + int l = this.readVaruint(); + + if (l > maxLen) { + throw new DeserializationException("varbytes max length exceeded;"); + } else if (l < minLen) { + throw new DeserializationException("varbytes min length not met;"); + } + + return this.read(l); + } + + public boolean assertMagic(byte[] expectedMagic) { + byte[] actualMagic = this.read(expectedMagic.length); + + return Arrays.equals(expectedMagic, actualMagic); + } + + public boolean assertEof() { + byte[] excess = this.read(1); + return excess != null; + } + + public String toString() { + return Arrays.toString(this.buffer); + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/StreamSerializationContext.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/StreamSerializationContext.java new file mode 100644 index 000000000..dcdc34b44 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/StreamSerializationContext.java @@ -0,0 +1,82 @@ +package com.vitorpamplona.quartz.ots; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Logger; + +public class StreamSerializationContext { + + List buffer = new ArrayList<>(); + + public StreamSerializationContext() { + this.buffer = new ArrayList<>(); + } + + public byte[] getOutput() { + byte[] bytes = new byte[this.buffer.size()]; + + for (int i = 0; i < this.buffer.size(); i++) { + bytes[i] = this.buffer.get(i); + } + + return bytes; + } + + public int getLength() { + return this.buffer.size(); + } + + public void writeBool(boolean value) { + if (value == true) { + this.writeByte((byte) 0xff); + } else { + this.writeByte((byte) 0x00); + } + } + + public void writeVaruint(int value) { + if ((value) == 0b00000000) { + this.writeByte((byte) 0x00); + } else { + while (value != 0) { + byte b = (byte) ((value & 0xff) & 0b01111111); + + if ((value) > 0b01111111) { + b |= 0b10000000; + } + + this.writeByte(b); + + if ((value) <= 0b01111111) { + break; + } + + value = value >> 7; + } + } + } + + public void writeByte(byte value) { + this.buffer.add(new Byte(value)); + } + + public void writeByte(Byte value) { + this.buffer.add(value); + } + + public void writeBytes(byte[] value) { + for (int i = 0; i < value.length; i++) { + this.writeByte(value[i]); + } + } + + public void writeVarbytes(byte[] value) { + this.writeVaruint(value.length); + this.writeBytes(value); + } + + public String toString() { + return Arrays.toString(this.getOutput()); + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/Timestamp.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/Timestamp.java new file mode 100644 index 000000000..f9f7d03c6 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/Timestamp.java @@ -0,0 +1,617 @@ +package com.vitorpamplona.quartz.ots; + +import com.vitorpamplona.quartz.ots.attestation.BitcoinBlockHeaderAttestation; +import com.vitorpamplona.quartz.ots.attestation.TimeAttestation; +import com.vitorpamplona.quartz.encoders.Hex; +import com.vitorpamplona.quartz.ots.exceptions.DeserializationException; +import com.vitorpamplona.quartz.ots.op.Op; +import com.vitorpamplona.quartz.ots.op.OpBinary; +import com.vitorpamplona.quartz.ots.op.OpSHA256; + +import java.util.*; +import java.util.Map.Entry; + +/** + * Proof that one or more attestations commit to a message. + * The proof is in the form of a tree, with each node being a message, and the + * edges being operations acting on those messages. The leafs of the tree are + * attestations that attest to the time that messages in the tree existed prior. + */ +public class Timestamp { + + public byte[] msg; + public List attestations; + public HashMap ops; + + /** + * Create a com.vitorpamplona.quartz.ots.Timestamp object. + * + * @param msg - Desc + */ + public Timestamp(byte[] msg) { + this.msg = msg; + this.attestations = new ArrayList<>(); + this.ops = new HashMap<>(); + } + + /** + * Deserialize a Timestamp. + * + * @param ots - The serialized byte array. + * @param initialMsg - The initial message. + * @return The deserialized Timestamp. + */ + public static Timestamp deserialize(byte[] ots, byte[] initialMsg) throws DeserializationException { + StreamDeserializationContext ctx = new StreamDeserializationContext(ots); + + return Timestamp.deserialize(ctx, initialMsg); + } + + /** + * Deserialize a Timestamp. + * Because the serialization format doesn't include the message that the + * timestamp operates on, you have to provide it so that the correct + * operation results can be calculated. + * The message you provide is assumed to be correct; if it causes a op to + * raise MsgValueError when the results are being calculated (done + * immediately, not lazily) DeserializationError is raised instead. + * + * @param ctx - The stream deserialization context. + * @param initialMsg - The initial message. + * @return The deserialized Timestamp. + */ + public static Timestamp deserialize(StreamDeserializationContext ctx, byte[] initialMsg) + throws DeserializationException { + Timestamp self = new Timestamp(initialMsg); + byte tag = ctx.readBytes(1)[0]; + + while ((tag & 0xff) == 0xff) { + byte current = ctx.readBytes(1)[0]; + doTagOrAttestation(self, ctx, current, initialMsg); + tag = ctx.readBytes(1)[0]; + } + + doTagOrAttestation(self, ctx, tag, initialMsg); + + return self; + } + + private static void doTagOrAttestation(Timestamp self, StreamDeserializationContext ctx, byte tag, byte[] initialMsg) + throws DeserializationException { + if ((tag & 0xff) == 0x00) { + TimeAttestation attestation = TimeAttestation.deserialize(ctx); + self.attestations.add(attestation); + } else { + Op op = Op.deserializeFromTag(ctx, tag); + byte[] result = op.call(initialMsg); + + Timestamp stamp = Timestamp.deserialize(ctx, result); + self.ops.put(op, stamp); + } + } + + /** + * Create a Serialize object. + * + * @return The byte array of the serialized timestamp + */ + public byte[] serialize() { + StreamSerializationContext ctx = new StreamSerializationContext(); + serialize(ctx); + + return ctx.getOutput(); + } + + /** + * Create a Serialize object. + * + * @param ctx - The stream serialization context. + */ + public void serialize(StreamSerializationContext ctx) { + List sortedAttestations = this.attestations; // TODO: Hm, this is just a reference copy... + Collections.sort(sortedAttestations); + + if (sortedAttestations.size() > 1) { + for (int i = 0; i < sortedAttestations.size() - 1; i++) { + ctx.writeBytes(new byte[]{(byte) 0xff, (byte) 0x00}); + sortedAttestations.get(i).serialize(ctx); + } + } + + if (this.ops.isEmpty()) { + ctx.writeByte((byte) 0x00); + + if (!sortedAttestations.isEmpty()) { + sortedAttestations.get(sortedAttestations.size() - 1).serialize(ctx); + } + } else if (!this.ops.isEmpty()) { + if (!sortedAttestations.isEmpty()) { + ctx.writeBytes(new byte[]{(byte) 0xff, (byte) 0x00}); + sortedAttestations.get(sortedAttestations.size() - 1).serialize(ctx); + } + + int counter = 0; + List> list = sortToList(this.ops.entrySet()); + + for (Map.Entry entry : list) { + Timestamp stamp = entry.getValue(); + Op op = entry.getKey(); + + if (counter < this.ops.size() - 1) { + ctx.writeBytes(new byte[]{(byte) 0xff}); + counter++; + } + + op.serialize(ctx); + stamp.serialize(ctx); + } + } + } + + /** + * Add all operations and attestations from another timestamp to this one. + * + * @param other - Initial other com.vitorpamplona.quartz.ots.Timestamp to merge. + * @throws Exception different timestamps messages + */ + public void merge(Timestamp other) throws Exception { + if (!Arrays.equals(this.msg, other.msg)) { + //Log.e("OpenTimestamp", "Can\'t merge timestamps for different messages together"); + throw new Exception("Can\'t merge timestamps for different messages together"); + } + + for (final TimeAttestation attestation : other.attestations) { + this.attestations.add(attestation); + } + + for (Map.Entry entry : other.ops.entrySet()) { + Timestamp otherOpStamp = entry.getValue(); + Op otherOp = entry.getKey(); + + Timestamp ourOpStamp = this.ops.get(otherOp); + + if (ourOpStamp == null) { + ourOpStamp = new Timestamp(otherOp.call(this.msg)); + this.ops.put(otherOp, ourOpStamp); + } + + ourOpStamp.merge(otherOpStamp); + } + } + + /** + * Shrink Timestamp. + * Remove useless pending attestions if exist a full bitcoin attestation. + * + * @return TimeAttestation - the minimal attestation. + * @throws Exception no attestion founds. + */ + public TimeAttestation shrink() throws Exception { + // Get all attestations + HashMap allAttestations = this.allAttestations(); + + if (allAttestations.size() == 0) { + throw new Exception(); + } else if (allAttestations.size() == 1) { + return allAttestations.values().iterator().next(); + } else if (this.ops.size() == 0) { + throw new Exception(); // TODO: Need a descriptive exception string here + } + + // Fore >1 attestations : + // Search first BitcoinBlockHeaderAttestation + TimeAttestation minAttestation = null; + + for (Map.Entry entry : this.ops.entrySet()) { + Timestamp timestamp = entry.getValue(); + //Op op = entry.getKey(); + + for (TimeAttestation attestation : timestamp.getAttestations()) { + if (attestation instanceof BitcoinBlockHeaderAttestation) { + if (minAttestation == null) { + minAttestation = attestation; + } else { + if (minAttestation instanceof BitcoinBlockHeaderAttestation + && attestation instanceof BitcoinBlockHeaderAttestation + && ((BitcoinBlockHeaderAttestation) minAttestation).getHeight() + > ((BitcoinBlockHeaderAttestation) attestation).getHeight()) { + minAttestation = attestation; + } + } + } + } + } + + // Only pending attestations : return the first + if (minAttestation == null) { + return allAttestations.values().iterator().next(); + } + + // Remove attestation if not min attestation + boolean shrinked = false; + + for (Iterator> it = this.ops.entrySet().iterator(); it.hasNext(); ) { + Map.Entry entry = it.next(); + Timestamp timestamp = entry.getValue(); + Op op = entry.getKey(); + Set attestations = timestamp.getAttestations(); + + if (attestations.size() > 0 && attestations.contains(minAttestation) && shrinked == false) { + timestamp.shrink(); + shrinked = true; + } else { + it.remove(); + } + } + + return minAttestation; + } + + /** + * Return the digest of the timestamp. + * + * @return The byte[] digest string. + */ + public byte[] getDigest() { + return this.msg; + } + + /** + * Return as memory hierarchical object. + * + * @param indent - Initial hierarchical indention. + * @return The output string. + */ + public String toString(int indent) { + StringBuilder builder = new StringBuilder(); + builder.append(Timestamp.indention(indent) + "msg: " + Hex.encode(this.msg) + "\n"); + builder.append(Timestamp.indention(indent) + this.attestations.size() + " attestations: \n"); + int i = 0; + + for (final TimeAttestation attestation : this.attestations) { + builder.append(Timestamp.indention(indent) + "[" + i + "] " + attestation.toString() + "\n"); + i++; + } + + i = 0; + builder.append(Timestamp.indention(indent) + this.ops.size() + " ops: \n"); + + for (Map.Entry entry : this.ops.entrySet()) { + Timestamp stamp = entry.getValue(); + Op op = entry.getKey(); + + builder.append(Timestamp.indention(indent) + "[" + i + "] op: " + op.toString() + "\n"); + builder.append(Timestamp.indention(indent) + "[" + i + "] timestamp: \n"); + builder.append(stamp.toString(indent + 1)); + i++; + } + + builder.append('\n'); + + return builder.toString(); + } + + /** + * Indention function for printing tree. + * + * @param pos - Initial hierarchical indention. + * @return The output space string. + */ + public static String indention(int pos) { + StringBuilder builder = new StringBuilder(); + + for (int i = 0; i < pos; i++) { + builder.append(" "); + } + + return builder.toString(); + } + + public String strTree(int indent) { + return strTree(indent, false); + } + + private String strResult(boolean verbosity, byte[] parameter, byte[] result) { + final String ANSI_HEADER = "\u001B[95m"; + final String ANSI_OKBLUE = "\u001B[94m"; + final String ANSI_OKGREEN = "\u001B[92m"; + final String ANSI_WARNING = "\u001B[93m"; + final String ANSI_FAIL = "\u001B[91m"; + final String ANSI_ENDC = "\u001B[0m"; + final String ANSI_BOLD = "\u001B[1m"; + final String ANSI_UNDERLINE = "\u001B[4m"; + + String rr = ""; + + if (verbosity == true && result != null) { + rr += " == "; + String resultHex = Utils.bytesToHex(result); + + if (parameter == null) { + rr += resultHex; + } else { + String parameterHex = Utils.bytesToHex(parameter); + + try { + int index = resultHex.indexOf(parameterHex); + String parameterHexHighlight = ANSI_BOLD + parameterHex + ANSI_ENDC; + + if (index == 0) { + rr += parameterHexHighlight + resultHex.substring(index + parameterHex.length(), resultHex.length()); + } else { + rr += resultHex.substring(0, index) + parameterHexHighlight; + } + } catch (Exception err) { + rr += resultHex; + } + } + } + + return rr; + } + + /** + * Return as tree hierarchical object. + * + * @param indent - Initial hierarchical indention. + * @param verbosity - Verbose option. + * @return The output string. + */ + public String strTree(int indent, boolean verbosity) { + StringBuilder builder = new StringBuilder(); + + if (!this.attestations.isEmpty()) { + for (final TimeAttestation attestation : this.attestations) { + builder.append(Timestamp.indention(indent)); + builder.append("verify " + attestation.toString() + strResult(verbosity, this.msg, null) + "\n"); + + if (attestation instanceof BitcoinBlockHeaderAttestation) { + String tx = Utils.bytesToHex(Utils.arrayReverse(this.msg)); + builder.append(Timestamp.indention(indent) + "# Bitcoin block merkle root " + tx.toLowerCase() + "\n"); + } + } + } + + if (this.ops.size() > 1) { + TreeMap ordered = new TreeMap<>(this.ops); + + for (Map.Entry entry : ordered.entrySet()) { + Timestamp timestamp = entry.getValue(); + Op op = entry.getKey(); + + byte[] curRes = op.call(this.msg); + byte[] curPar = null; + + if (op instanceof OpBinary) { + curPar = ((OpBinary) op).arg; + } + + builder.append(Timestamp.indention(indent) + " -> " + op.toString().toLowerCase() + strResult(verbosity, curPar, curRes).toLowerCase() + "\n"); + builder.append(timestamp.strTree(indent + 1, verbosity)); + } + } else if (this.ops.size() > 0) { + // output += com.eternitywall.ots.Timestamp.indention(indent); + for (Map.Entry entry : this.ops.entrySet()) { + Timestamp timestamp = entry.getValue(); + Op op = entry.getKey(); + + byte[] curRes = op.call(this.msg); + byte[] curPar = null; + + if (op instanceof OpBinary) { + curPar = ((OpBinary) op).arg; + } + + builder.append(Timestamp.indention(indent) + op.toString().toLowerCase() + strResult(verbosity, curPar, curRes).toLowerCase() + "\n"); + builder.append(timestamp.strTree(indent, verbosity)); + } + } + + return builder.toString(); + } + + /** + * Returns a list of all sub timestamps with attestations. + * + * @return List of all sub timestamps with attestations. + */ + public List directlyVerified() { + if (!this.attestations.isEmpty()) { + List list = new ArrayList<>(); + list.add(this); + return list; + } + + List list = new ArrayList<>(); + + for (Map.Entry entry : this.ops.entrySet()) { + Timestamp ts = entry.getValue(); + //Op op = entry.getKey(); + + List result = ts.directlyVerified(); + list.addAll(result); + } + + return list; + } + + /** + * Returns a set of all Attestations. + * + * @return Set of all timestamp attestations. + */ + public Set getAttestations() { + Set set = new HashSet(); + + for (Map.Entry item : this.allAttestations().entrySet()) { + //byte[] msg = item.getKey(); + TimeAttestation attestation = item.getValue(); + set.add(attestation); + } + + return set; + } + + /** + * Determine if timestamp is complete and can be verified. + * + * @return True if the timestamp is complete, False otherwise. + */ + public Boolean isTimestampComplete() { + for (Map.Entry item : this.allAttestations().entrySet()) { + //byte[] msg = item.getKey(); + TimeAttestation attestation = item.getValue(); + + if (attestation instanceof BitcoinBlockHeaderAttestation) { + return true; + } + } + + return false; + } + + /** + * Iterate over all attestations recursively + * + * @return Returns iterable of (msg, attestation) + */ + public HashMap allAttestations() { + HashMap map = new HashMap<>(); + + for (TimeAttestation attestation : this.attestations) { + map.put(this.msg, attestation); + } + + for (Map.Entry entry : this.ops.entrySet()) { + Timestamp ts = entry.getValue(); + //Op op = entry.getKey(); + + HashMap subMap = ts.allAttestations(); + + for (Map.Entry item : subMap.entrySet()) { + byte[] msg = item.getKey(); + TimeAttestation attestation = item.getValue(); + map.put(msg, attestation); + } + } + + return map; + } + + /** + * Iterate over all tips recursively + * + * @return Returns iterable of (msg, attestation) + */ + public Set allTips() { + Set set = new HashSet<>(); + + if (this.ops.size() == 0) { + set.add(this.msg); + } + + for (Map.Entry entry : this.ops.entrySet()) { + Timestamp ts = entry.getValue(); + //Op op = entry.getKey(); + + Set subSet = ts.allTips(); + + for (byte[] msg : subSet) { + set.add(msg); + } + } + + return set; + } + + /** + * Compare timestamps. + * + * @param timestamp the timestamp to compare with + * @return Returns true if timestamps are equals + */ + public boolean equals(Timestamp timestamp) { + if (Arrays.equals(this.getDigest(), timestamp.getDigest()) == false) { + return false; + } + + // Check attestations + if (this.attestations.size() != timestamp.attestations.size()) { + return false; + } + + for (int i = 0; i < this.attestations.size(); i++) { + TimeAttestation ta1 = this.attestations.get(i); + TimeAttestation ta2 = timestamp.attestations.get(i); + + if (!(ta1.equals(ta2))) { + return false; + } + } + + // Check operations + if (this.ops.size() != timestamp.ops.size()) { + return false; + } + + // Order list of operations + List> list1 = sortToList(this.ops.entrySet()); + List> list2 = sortToList(timestamp.ops.entrySet()); + + for (int i = 0; i < list1.size(); i++) { + Op op1 = list1.get(i).getKey(); + Op op2 = list2.get(i).getKey(); + + if (!op1.equals(op2)) { + return false; + } + + Timestamp t1 = list1.get(i).getValue(); + Timestamp t2 = list2.get(i).getValue(); + + if (!t1.equals(t2)) { + return false; + } + } + + return true; + } + + /** + * Add Op to current timestamp and return the sub stamp + * + * @param op - The operation to insert + * @return Returns the sub timestamp + */ + public Timestamp add(Op op) { + // nonce_appended_stamp = timestamp.ops.add(com.vitorpamplona.quartz.ots.op.OpAppend(os.urandom(16))) + //Op opAppend = new OpAppend(bytes); + + if (this.ops.containsKey(op)) { + return this.ops.get(op); + } + + Timestamp stamp = new Timestamp(op.call(this.msg)); + this.ops.put(op, stamp); + + return stamp; + } + + /** + * Retrieve a sorted list of all map entries. + * + * @param setEntries - The entries set of ops hashmap + * @return Returns the sorted list of map entries + */ + public List> sortToList(Set> setEntries) { + List> entries = new ArrayList<>(setEntries); + Collections.sort(entries, new Comparator>() { + @Override + public int compare(Entry a, Entry b) { + return a.getKey().compareTo(b.getKey()); + } + }); + + return entries; + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/Utils.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/Utils.java new file mode 100644 index 000000000..85cb300e8 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/Utils.java @@ -0,0 +1,204 @@ +package com.vitorpamplona.quartz.ots; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.logging.ConsoleHandler; +import java.util.logging.LogRecord; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; + +/** + * Utility functions for (mostly) manipulating byte arrays. + */ +public class Utils { + /** + * Fills a byte array with the given byte value. + * + * @param array the array to fill + * @param value the value to fill the array with + */ + public static void arrayFill(byte[] array, byte value) { + for (int i = 0; i < array.length; i++) { + array[i] = value; + } + } + + /** + * Returns the first value that is not null. If all objects are null, then it returns null. + * + * @param items the array of Ts + * @param This describes my type parameter + * @return the first value that is not null. If all objects are null, then it returns null. + * @deprecated Not used by Java OpenTimestamps itself, and doesn't offer much useful functionality. + */ + @Deprecated + public static T coalesce(T... items) { + for (T i : items) { + if (i != null) { + return i; + } + } + + return null; + } + + /** + * Returns a copy of the byte array argument, or null if the byte array argument is null. + * + * @param data the array of bytes to copy + * @return the copied byte array + */ + public static byte[] arraysCopy(byte[] data) { + if (data == null) { + return null; + } + + byte[] copy = new byte[data.length]; + System.arraycopy(data, 0, copy, 0, data.length); + + return copy; + } + + /** + * Returns a byte array which is the result of concatenating the two passed in byte arrays. + * None of the passed in arrays may be null. + * + * @param array1 the first array of bytes + * @param array2 the second array of bytes + * @return a copy of array1 and array2 concatenated together + */ + public static byte[] arraysConcat(byte[] array1, byte[] array2) { + byte[] array1and2 = new byte[array1.length + array2.length]; + System.arraycopy(array1, 0, array1and2, 0, array1.length); + System.arraycopy(array2, 0, array1and2, array1.length, array2.length); + + return array1and2; + } + + /** + * Returns a given length array of random bytes. + * + * @param length the requested length of the byte array + * @return a given length array of random bytes + * @throws NoSuchAlgorithmException for Java 8 implementations + */ + public static byte[] randBytes(int length) throws NoSuchAlgorithmException { + //Java 6 & 7: + SecureRandom random = new SecureRandom(); + byte[] bytes = new byte[length]; + random.nextBytes(bytes); + + // Java 8 (even more secure): + // SecureRandom.getInstanceStrong().nextBytes(bytes); + + return bytes; + } + + /** + * Returns a reversed copy of the passed in byte array. + * + * @param array the byte array to reverse + * @return a copy of the byte array, reversed + */ + public static byte[] arrayReverse(byte[] array) { + byte[] reversedArray = new byte[array.length]; + + for (int i = array.length - 1, j = 0; i >= 0; i--, j++) { + reversedArray[j] = array[i]; + } + + return reversedArray; + } + + /** + * Compares two byte arrays. + * + * @param left the left byte array to compare with + * @param right the right byte array to compare with + * @return 0 if the arrays are identical, negative if left < right, positive if left > right + */ + public static int compare(byte[] left, byte[] right) { + for (int i = 0, j = 0; i < left.length && j < right.length; i++, j++) { + int a = (left[i] & 0xff); + int b = (right[j] & 0xff); + + if (a != b) { + return a - b; + } + } + + return left.length - right.length; + } + + /** + * Returns a HEX representation of the passed in byte array. + * + * @param bytes the byte array to convert into a string of HEX + * @return a string representation of the byte array + */ + public static String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + + for (byte b : bytes) { + sb.append(String.format("%02X", b)); + } + + return sb.toString(); + } + + /** + * Converts a HEX representation to its byte array representation. The passed in string must not be null. + * + * @param s a string in HEX format to be converted to a corresponding byte array + * @return the byte array representation of the string in HEX format + * @throws IllegalArgumentException if the passed in HEX string can't be converted to a byte array + */ + public static byte[] hexToBytes(String s) throws IllegalArgumentException { + int len = s.length(); + + if (len % 2 == 1) { + throw new IllegalArgumentException(); + } + + byte[] data = new byte[len / 2]; + + for (int i = 0; i < len; i += 2) { + if ((Character.digit(s.charAt(i), 16) == -1) || (Character.digit(s.charAt(i + 1), 16) == -1)) { + throw new IllegalArgumentException(); + } + + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + + Character.digit(s.charAt(i + 1), 16)); + } + + return data; + } + + /** + * Returns a string with the first letter uppercase. + * + * @param string the string to get its first character converted to uppercase + * @return the string, with its first character converted to uppercase + */ + public static String toUpperFirstLetter(String string) { + return string.substring(0, 1).toUpperCase() + string.substring(1).toLowerCase(); + } + + // TODO: This is not the way to do logging. Fix later, possibly with slf4j annotation. Need to read up on the subject. + public static Logger getLogger(String name) { + Logger log = Logger.getLogger(name); + ConsoleHandler handler = new ConsoleHandler(); + + handler.setFormatter(new SimpleFormatter() { + @Override + public synchronized String format(LogRecord lr) { + return lr.getMessage() + "\r\n"; + } + }); + + log.setUseParentHandlers(false); + log.addHandler(handler); + + return log; + } +} \ No newline at end of file diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/VerifyResult.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/VerifyResult.java new file mode 100644 index 000000000..64da0569a --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/VerifyResult.java @@ -0,0 +1,56 @@ +package com.vitorpamplona.quartz.ots; + +import java.text.DateFormatSymbols; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +/** + * Class that lets us compare, sort, store and print timestamps. + */ +public class VerifyResult implements Comparable { + public static enum Chains { + BITCOIN, LITECOIN, ETHEREUM + } + + public Long timestamp; + public int height; + + public VerifyResult(Long timestamp, int height) { + this.timestamp = timestamp; + this.height = height; + } + + /** + * Returns, if existing, a string representation describing the existence of a block attest + */ + public String toString() { + if (height == 0 || timestamp == null) { + return ""; + } + + String pattern = "YYYY-MM-dd z"; + Locale locale = new Locale("en", "UK"); + DateFormatSymbols dateFormatSymbols = new DateFormatSymbols(locale); + SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern, dateFormatSymbols); + String string = simpleDateFormat.format(new Date(timestamp * 1000)); + + return "block " + String.valueOf(height) + " attests data existed as of " + string; + } + + @Override + public int compareTo(VerifyResult vr) { + return this.height - vr.height; + } + + @Override + public boolean equals(Object obj) { + VerifyResult vr = (VerifyResult) obj; + return this.timestamp == vr.timestamp && this.height == vr.height; + } + + @Override + public int hashCode() { + return ((int) (long) (this.timestamp)) ^ this.height; + } +} \ No newline at end of file diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/attestation/BitcoinBlockHeaderAttestation.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/attestation/BitcoinBlockHeaderAttestation.java new file mode 100644 index 000000000..55acbefe6 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/attestation/BitcoinBlockHeaderAttestation.java @@ -0,0 +1,118 @@ +package com.vitorpamplona.quartz.ots.attestation; + +import com.vitorpamplona.quartz.ots.BlockHeader; +import com.vitorpamplona.quartz.ots.StreamDeserializationContext; +import com.vitorpamplona.quartz.ots.StreamSerializationContext; +import com.vitorpamplona.quartz.ots.Utils; +import com.vitorpamplona.quartz.ots.exceptions.VerificationException; + +import java.util.Arrays; +import java.util.logging.Logger; + +/** + * Bitcoin Block Header Attestation. + * The commitment digest will be the merkleroot of the blockheader. + * The block height is recorded so that looking up the correct block header in + * an external block header database doesn't require every header to be stored + * locally (33MB and counting). (remember that a memory-constrained local + * client can save an MMR that commits to all blocks, and use an external service to fill + * in pruned details). + * Otherwise no additional redundant data about the block header is recorded. + * This is very intentional: since the attestation contains (nearly) the + * absolute bare minimum amount of data, we encourage implementations to do + * the correct thing and get the block header from a by-height index, check + * that the merkleroots match, and then calculate the time from the header + * information. Providing more data would encourage implementations to cheat. + * Remember that the only thing that would invalidate the block height is a + * reorg, but in the event of a reorg the merkleroot will be invalid anyway, + * so there's no point to recording data in the attestation like the header + * itself. At best that would just give us extra confirmation that a reorg + * made the attestation invalid; reorgs deep enough to invalidate timestamps are + * exceptionally rare events anyway, so better to just tell the user the timestamp + * can't be verified rather than add almost-never tested code to handle that case + * more gracefully. + * + * @see TimeAttestation + */ +public class BitcoinBlockHeaderAttestation extends TimeAttestation { + + public static byte[] _TAG = {(byte) 0x05, (byte) 0x88, (byte) 0x96, (byte) 0x0d, (byte) 0x73, (byte) 0xd7, (byte) 0x19, (byte) 0x01}; + public static String chain = "bitcoin"; + + @Override + public byte[] _TAG() { + return BitcoinBlockHeaderAttestation._TAG; + } + + private int height = 0; + + public int getHeight() { + return height; + } + + public BitcoinBlockHeaderAttestation(int height_) { + super(); + this.height = height_; + } + + public static BitcoinBlockHeaderAttestation deserialize(StreamDeserializationContext ctxPayload) { + int height = ctxPayload.readVaruint(); + + return new BitcoinBlockHeaderAttestation(height); + } + + @Override + public void serializePayload(StreamSerializationContext ctx) { + ctx.writeVaruint(this.height); + } + + public String toString() { + return "BitcoinBlockHeaderAttestation(" + this.height + ")"; + } + + @Override + public int compareTo(TimeAttestation o) { + BitcoinBlockHeaderAttestation ob = (BitcoinBlockHeaderAttestation) o; + + return this.height - ob.height; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof BitcoinBlockHeaderAttestation)) { + return false; + } + + if (!Arrays.equals(this._TAG(), ((BitcoinBlockHeaderAttestation) obj)._TAG())) { + return false; + } + + if (this.height != ((BitcoinBlockHeaderAttestation) obj).height) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + return Arrays.hashCode(this._TAG()) ^ this.height; + } + + /** + * Verify attestation against a Bitcoin block header. + * @param digest the digest + * @param block the Bitcoin block header + * @return the block time on success; raises VerificationError on failure. + * @throws VerificationException verification exception + */ + public Long verifyAgainstBlockheader(byte[] digest, BlockHeader block) throws VerificationException { + if (digest.length != 32) { + throw new VerificationException("Expected digest with length 32 bytes; got " + digest.length + " bytes"); + } else if (!Arrays.equals(digest, Utils.hexToBytes(block.getMerkleroot()))) { + throw new VerificationException("Digest does not match merkleroot"); + } + + return block.getTime(); + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/attestation/EthereumBlockHeaderAttestation.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/attestation/EthereumBlockHeaderAttestation.java new file mode 100644 index 000000000..bce68c9a8 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/attestation/EthereumBlockHeaderAttestation.java @@ -0,0 +1,79 @@ +package com.vitorpamplona.quartz.ots.attestation; + +import com.vitorpamplona.quartz.ots.StreamDeserializationContext; +import com.vitorpamplona.quartz.ots.StreamSerializationContext; +import com.vitorpamplona.quartz.ots.Utils; + +import java.util.Arrays; +import java.util.logging.Logger; + +/** + * Ethereum Block Header Attestation. + * + * @see TimeAttestation + */ +public class EthereumBlockHeaderAttestation extends TimeAttestation { + + public static byte[] _TAG = {(byte) 0x30, (byte) 0xfe, (byte) 0x80, (byte) 0x87, (byte) 0xb5, (byte) 0xc7, (byte) 0xea, (byte) 0xd7}; + public static String chain = "ethereum"; + + @Override + public byte[] _TAG() { + return EthereumBlockHeaderAttestation._TAG; + } + + private int height = 0; + + public int getHeight() { + return height; + } + + EthereumBlockHeaderAttestation(int height_) { + super(); + this.height = height_; + } + + public static EthereumBlockHeaderAttestation deserialize(StreamDeserializationContext ctxPayload) { + int height = ctxPayload.readVaruint(); + + return new EthereumBlockHeaderAttestation(height); + } + + @Override + public void serializePayload(StreamSerializationContext ctx) { + ctx.writeVaruint(this.height); + } + + public String toString() { + return "EthereumBlockHeaderAttestation(" + this.height + ")"; + } + + @Override + public int compareTo(TimeAttestation o) { + EthereumBlockHeaderAttestation ob = (EthereumBlockHeaderAttestation) o; + + return this.height - ob.height; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof EthereumBlockHeaderAttestation)) { + return false; + } + + if (!Arrays.equals(this._TAG(), ((EthereumBlockHeaderAttestation) obj)._TAG())) { + return false; + } + + if (this.height != ((EthereumBlockHeaderAttestation) obj).height) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + return Arrays.hashCode(this._TAG()) ^ this.height; + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/attestation/LitecoinBlockHeaderAttestation.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/attestation/LitecoinBlockHeaderAttestation.java new file mode 100644 index 000000000..548d88e24 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/attestation/LitecoinBlockHeaderAttestation.java @@ -0,0 +1,99 @@ +package com.vitorpamplona.quartz.ots.attestation; + +import com.vitorpamplona.quartz.ots.BlockHeader; +import com.vitorpamplona.quartz.ots.StreamDeserializationContext; +import com.vitorpamplona.quartz.ots.StreamSerializationContext; +import com.vitorpamplona.quartz.ots.Utils; +import com.vitorpamplona.quartz.ots.exceptions.VerificationException; + +import java.util.Arrays; +import java.util.logging.Logger; + +/** + * Litecoin Block Header Attestation. + * + * @see TimeAttestation + */ +public class LitecoinBlockHeaderAttestation extends TimeAttestation { + + public static byte[] _TAG = {(byte) 0x06, (byte) 0x86, (byte) 0x9a, (byte) 0x0d, (byte) 0x73, (byte) 0xd7, (byte) 0x1b, (byte) 0x45}; + + public static String chain = "litecoin"; + + @Override + public byte[] _TAG() { + return LitecoinBlockHeaderAttestation._TAG; + } + + private int height = 0; + + public int getHeight() { + return height; + } + + public LitecoinBlockHeaderAttestation(int height_) { + super(); + this.height = height_; + } + + public static LitecoinBlockHeaderAttestation deserialize(StreamDeserializationContext ctxPayload) { + int height = ctxPayload.readVaruint(); + + return new LitecoinBlockHeaderAttestation(height); + } + + @Override + public void serializePayload(StreamSerializationContext ctx) { + ctx.writeVaruint(this.height); + } + + public String toString() { + return "LitecoinBlockHeaderAttestation(" + this.height + ")"; + } + + @Override + public int compareTo(TimeAttestation o) { + LitecoinBlockHeaderAttestation ob = (LitecoinBlockHeaderAttestation) o; + + return this.height - ob.height; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof LitecoinBlockHeaderAttestation)) { + return false; + } + + if (!Arrays.equals(this._TAG(), ((LitecoinBlockHeaderAttestation) obj)._TAG())) { + return false; + } + + if (this.height != ((LitecoinBlockHeaderAttestation) obj).height) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + return Arrays.hashCode(this._TAG()) ^ this.height; + } + + /** + * Verify attestation against a Litecoin block header. + * @param digest the digest + * @param block the Litecoin block header + * @return the block time on success; raises VerificationError on failure. + * @throws VerificationException verification exception + */ + public Long verifyAgainstBlockheader(byte[] digest, BlockHeader block) throws VerificationException { + if (digest.length != 32) { + throw new VerificationException("Expected digest with length 32 bytes; got " + digest.length + " bytes"); + } else if (!Arrays.equals(digest, Utils.hexToBytes(block.getMerkleroot()))) { + throw new VerificationException("Digest does not match merkleroot"); + } + + return block.getTime(); + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/attestation/PendingAttestation.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/attestation/PendingAttestation.java new file mode 100644 index 000000000..5e9a44390 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/attestation/PendingAttestation.java @@ -0,0 +1,134 @@ +package com.vitorpamplona.quartz.ots.attestation; + +import android.util.Log; + +import com.vitorpamplona.quartz.ots.StreamDeserializationContext; +import com.vitorpamplona.quartz.ots.StreamSerializationContext; +import com.vitorpamplona.quartz.ots.Utils; + +import com.vitorpamplona.quartz.ots.exceptions.DeserializationException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.logging.Logger; + +/** + * Pending attestations. + * Commitment has been recorded in a remote calendar for future attestation, + * and we have a URI to find a more complete timestamp in the future. + * Nothing other than the URI is recorded, nor is there provision made to add + * extra metadata (other than the URI) in future upgrades. The rational here + * is that remote calendars promise to keep commitments indefinitely, so from + * the moment they are created it should be possible to find the commitment in + * the calendar. Thus if you're not satisfied with the local verifiability of + * a timestamp, the correct thing to do is just ask the remote calendar if + * additional attestations are available and/or when they'll be available. + * While we could additional metadata like what types of attestations the + * remote calendar expects to be able to provide in the future, that metadata + * can easily change in the future too. Given that we don't expect timestamps + * to normally have more than a small number of remote calendar attestations, + * it'd be better to have verifiers get the most recent status of such + * information (possibly with appropriate negative response caching). + * + * @see TimeAttestation + */ +public class PendingAttestation extends TimeAttestation { + + public static byte[] _TAG = {(byte) 0x83, (byte) 0xdf, (byte) 0xe3, (byte) 0x0d, (byte) 0x2e, (byte) 0xf9, (byte) 0x0c, (byte) 0x8e}; + + @Override + public byte[] _TAG() { + return PendingAttestation._TAG; + } + + public static int _MAX_URI_LENGTH = 1000; + + public static String _ALLOWED_URI_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._/:"; + + private byte[] uri; + + public byte[] getUri() { + return uri; + } + + public PendingAttestation(byte[] uri_) { + super(); + this.uri = uri_; + } + + public static boolean checkUri(byte[] uri) { + if (uri.length > PendingAttestation._MAX_URI_LENGTH) { + System.err.print("URI exceeds maximum length"); + + return false; + } + + for (int i = 0; i < uri.length; i++) { + Character c = String.format("%c", uri[i]).charAt(0); + + if (PendingAttestation._ALLOWED_URI_CHARS.indexOf(c) < 0) { + Log.e("OpenTimestamp","URI contains invalid character "); + + return false; + } + } + + return true; + } + + public static PendingAttestation deserialize(StreamDeserializationContext ctxPayload) + throws DeserializationException { + + byte[] utf8Uri; + try { + utf8Uri = ctxPayload.readVarbytes(PendingAttestation._MAX_URI_LENGTH); + } catch (DeserializationException e) { + Log.e("OpenTimestamp","URI too long and thus invalid: "); + throw new DeserializationException("Invalid URI: "); + } + + if (PendingAttestation.checkUri(utf8Uri) == false) { + Log.e("OpenTimestamp","Invalid URI: "); + throw new DeserializationException("Invalid URI: "); + } + + return new PendingAttestation(utf8Uri); + } + + @Override + public void serializePayload(StreamSerializationContext ctx) { + ctx.writeVarbytes(this.uri); + } + + public String toString() { + return "PendingAttestation(\'" + new String(this.uri, StandardCharsets.UTF_8) + "\')"; + } + + @Override + public int compareTo(TimeAttestation o) { + PendingAttestation opa = (PendingAttestation) o; + + return Utils.compare(this.uri, opa.uri); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof PendingAttestation)) { + return false; + } + + if (!Arrays.equals(this._TAG(), ((PendingAttestation) obj)._TAG())) { + return false; + } + + if (!Arrays.equals(this.uri, ((PendingAttestation) obj).uri)) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + return Arrays.hashCode(this._TAG()) ^ Arrays.hashCode(this.uri); + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/attestation/TimeAttestation.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/attestation/TimeAttestation.java new file mode 100644 index 000000000..39325530c --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/attestation/TimeAttestation.java @@ -0,0 +1,83 @@ +package com.vitorpamplona.quartz.ots.attestation; + +import com.vitorpamplona.quartz.ots.StreamDeserializationContext; +import com.vitorpamplona.quartz.ots.StreamSerializationContext; +import com.vitorpamplona.quartz.ots.Utils; + +import com.vitorpamplona.quartz.ots.exceptions.DeserializationException; +import java.util.Arrays; +import java.util.logging.Logger; + +/** + * Class representing {@link com.vitorpamplona.quartz.ots.Timestamp} signature verification + */ +public abstract class TimeAttestation implements Comparable { + + public static int _TAG_SIZE = 8; + + public static int _MAX_PAYLOAD_SIZE = 8192; + + public byte[] _TAG; + + public byte[] _TAG() { + return new byte[]{}; + } + + /** + * Deserialize a general Time Attestation to the specific subclass Attestation. + * + * @param ctx The stream deserialization context. + * @return The specific subclass Attestation. + */ + public static TimeAttestation deserialize(StreamDeserializationContext ctx) throws DeserializationException { + // console.log('attestation deserialize'); + + byte[] tag = ctx.readBytes(_TAG_SIZE); + // console.log('tag: ', com.vitorpamplona.quartz.ots.Utils.bytesToHex(tag)); + + byte[] serializedAttestation = ctx.readVarbytes(_MAX_PAYLOAD_SIZE); + // console.log('serializedAttestation: ', com.vitorpamplona.quartz.ots.Utils.bytesToHex(serializedAttestation)); + + StreamDeserializationContext ctxPayload = new StreamDeserializationContext(serializedAttestation); + + /* eslint no-use-before-define: ["error", { "classes": false }] */ + if (Arrays.equals(tag, PendingAttestation._TAG) == true) { + return PendingAttestation.deserialize(ctxPayload); + } else if (Arrays.equals(tag, BitcoinBlockHeaderAttestation._TAG) == true) { + return BitcoinBlockHeaderAttestation.deserialize(ctxPayload); + } else if (Arrays.equals(tag, LitecoinBlockHeaderAttestation._TAG) == true) { + return LitecoinBlockHeaderAttestation.deserialize(ctxPayload); + } else if (Arrays.equals(tag, EthereumBlockHeaderAttestation._TAG) == true) { + return EthereumBlockHeaderAttestation.deserialize(ctxPayload); + } + + return new UnknownAttestation(tag, serializedAttestation); + } + + /** + * Serialize a a general Time Attestation to the specific subclass Attestation. + * + * @param ctx The output stream serialization context. + */ + public void serialize(StreamSerializationContext ctx) { + ctx.writeBytes(this._TAG()); + StreamSerializationContext ctxPayload = new StreamSerializationContext(); + serializePayload(ctxPayload); + ctx.writeVarbytes(ctxPayload.getOutput()); + } + + public void serializePayload(StreamSerializationContext ctxPayload) { + // TODO: Is this intentional? + } + + @Override + public int compareTo(TimeAttestation o) { + int deltaTag = Utils.compare(this._TAG(), o._TAG()); + + if (deltaTag == 0) { + return this.compareTo(o); + } else { + return deltaTag; + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/attestation/UnknownAttestation.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/attestation/UnknownAttestation.java new file mode 100644 index 000000000..6baf6879a --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/attestation/UnknownAttestation.java @@ -0,0 +1,76 @@ +package com.vitorpamplona.quartz.ots.attestation; + +import com.vitorpamplona.quartz.ots.StreamDeserializationContext; +import com.vitorpamplona.quartz.ots.StreamSerializationContext; +import com.vitorpamplona.quartz.ots.Utils; + +import com.vitorpamplona.quartz.ots.exceptions.DeserializationException; +import java.util.Arrays; +import java.util.logging.Logger; + +/** + * Placeholder for attestations that don't support + * + * @see TimeAttestation + */ +public class UnknownAttestation extends TimeAttestation { + + byte[] payload; + + public static byte[] _TAG = new byte[]{}; + + @Override + public byte[] _TAG() { + return _TAG; + } + + UnknownAttestation(byte[] tag, byte[] payload) { + super(); + this._TAG = tag; + this.payload = payload; + } + + @Override + public void serializePayload(StreamSerializationContext ctx) { + ctx.writeBytes(this.payload); + } + + public static UnknownAttestation deserialize(StreamDeserializationContext ctxPayload, byte[] tag) throws DeserializationException { + byte[] payload = ctxPayload.readVarbytes(_MAX_PAYLOAD_SIZE); + + return new UnknownAttestation(tag, payload); + } + + public String toString() { + return "UnknownAttestation " + Utils.bytesToHex(this._TAG()) + ' ' + Utils.bytesToHex(this.payload); + } + + @Override + public int compareTo(TimeAttestation o) { + UnknownAttestation ota = (UnknownAttestation) o; + + return Utils.compare(this.payload, ota.payload); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof UnknownAttestation)) { + return false; + } + + if (!Arrays.equals(this._TAG(), ((UnknownAttestation) obj)._TAG())) { + return false; + } + + if (!Arrays.equals(this.payload, ((UnknownAttestation) obj).payload)) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + return Arrays.hashCode(this._TAG()) ^ Arrays.hashCode(this.payload); + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/crypto/Digest.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/crypto/Digest.java new file mode 100644 index 000000000..c0c3bd7f2 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/crypto/Digest.java @@ -0,0 +1,52 @@ +package com.vitorpamplona.quartz.ots.crypto; + +/** + * Message digest interface + */ +public interface Digest { + /** + * Return the algorithm name + * + * @return the algorithm name + */ + public String getAlgorithmName(); + + /** + * Return the size, in bytes, of the digest produced by this message digest. + * + * @return the size, in bytes, of the digest produced by this message digest. + */ + public int getDigestSize(); + + /** + * Update the message digest with a single byte. + * + * @param in the input byte to be entered. + */ + public void update(byte in); + + /** + * Update the message digest with a block of bytes. + * + * @param in the byte array containing the data. + * @param inOff the offset into the byte array where the data starts. + * @param len the length of the data. + */ + public void update(byte[] in, int inOff, int len); + + /** + * Close the digest, producing the final digest value. The doFinal + * call also resets the digest. + * + * @param out the array the digest is to be copied into. + * @param outOff the offset into the out array the digest is to start at. + * @return something + * @see #reset() + */ + public int doFinal(byte[] out, int outOff); + + /** + * Reset the digest back to it's initial state. + */ + public void reset(); +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/crypto/ExtendedDigest.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/crypto/ExtendedDigest.java new file mode 100644 index 000000000..df08f1191 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/crypto/ExtendedDigest.java @@ -0,0 +1,11 @@ +package com.vitorpamplona.quartz.ots.crypto; + +public interface ExtendedDigest extends Digest { + /** + * Return the size in bytes of the internal buffer the digest applies it's compression + * function to. + * + * @return byte length of the digests internal buffer. + */ + public int getByteLength(); +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/crypto/GeneralDigest.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/crypto/GeneralDigest.java new file mode 100644 index 000000000..021eb4675 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/crypto/GeneralDigest.java @@ -0,0 +1,138 @@ +package com.vitorpamplona.quartz.ots.crypto; + +/** + * Base implementation of MD4 family style digest as outlined in + * "Handbook of Applied Cryptography", pages 344 - 347. + */ +public abstract class GeneralDigest implements ExtendedDigest, Memoable { + + private static final int BYTE_LENGTH = 64; + + private final byte[] xBuf = new byte[4]; + private int xBufOff; + + private long byteCount; + + /** + * Standard constructor + */ + protected GeneralDigest() { + xBufOff = 0; + } + + /** + * Copy constructor. We are using copy constructors in place + * of the Object.clone() interface as this interface is not + * supported by J2ME. + * + * @param t the GeneralDigest + */ + protected GeneralDigest(GeneralDigest t) { + copyIn(t); + } + + protected GeneralDigest(byte[] encodedState) { + System.arraycopy(encodedState, 0, xBuf, 0, xBuf.length); + xBufOff = Pack.bigEndianToInt(encodedState, 4); + byteCount = Pack.bigEndianToLong(encodedState, 8); + } + + protected void copyIn(GeneralDigest t) { + System.arraycopy(t.xBuf, 0, xBuf, 0, t.xBuf.length); + + xBufOff = t.xBufOff; + byteCount = t.byteCount; + } + + public void update(byte in) { + xBuf[xBufOff++] = in; + + if (xBufOff == xBuf.length) { + processWord(xBuf, 0); + xBufOff = 0; + } + + byteCount++; + } + + public void update(byte[] in, int inOff, int len) { + len = Math.max(0, len); + + // + // fill the current word + // + int i = 0; + + if (xBufOff != 0) { + while (i < len) { + xBuf[xBufOff++] = in[inOff + i++]; + + if (xBufOff == 4) { + processWord(xBuf, 0); + xBufOff = 0; + break; + } + } + } + + // + // process whole words. + // + int limit = ((len - i) & ~3) + i; + + for (; i < limit; i += 4) { + processWord(in, inOff + i); + } + + // + // load in the remainder. + // + while (i < len) { + xBuf[xBufOff++] = in[inOff + i++]; + } + + byteCount += len; + } + + public void finish() { + long bitLength = (byteCount << 3); + + // + // add the pad bytes. + // + update((byte) 128); + + while (xBufOff != 0) { + update((byte) 0); + } + + processLength(bitLength); + processBlock(); + } + + public void reset() { + byteCount = 0; + + xBufOff = 0; + + for (int i = 0; i < xBuf.length; i++) { + xBuf[i] = 0; + } + } + + protected void populateState(byte[] state) { + System.arraycopy(xBuf, 0, state, 0, xBufOff); + Pack.intToBigEndian(xBufOff, state, 4); + Pack.longToBigEndian(byteCount, state, 8); + } + + public int getByteLength() { + return BYTE_LENGTH; + } + + protected abstract void processWord(byte[] in, int inOff); + + protected abstract void processLength(long bitLength); + + protected abstract void processBlock(); +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/crypto/KeccakDigest.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/crypto/KeccakDigest.java new file mode 100644 index 000000000..6053f72dc --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/crypto/KeccakDigest.java @@ -0,0 +1,473 @@ +package com.vitorpamplona.quartz.ots.crypto; + +import com.vitorpamplona.quartz.ots.Utils; + +/** + * Implementation of Keccak based on following KeccakNISTInterface.c from http://keccak.noekeon.org/ + *

+ * Following the naming conventions used in the C source code to enable easy review of the implementation. + */ +public class KeccakDigest implements ExtendedDigest { + + private static long[] KeccakRoundConstants = keccakInitializeRoundConstants(); + + private static int[] KeccakRhoOffsets = keccakInitializeRhoOffsets(); + + private static long[] keccakInitializeRoundConstants() { + long[] keccakRoundConstants = new long[24]; + byte[] LFSRstate = new byte[1]; + + LFSRstate[0] = 0x01; + int i, j, bitPosition; + + for (i = 0; i < 24; i++) { + keccakRoundConstants[i] = 0; + + for (j = 0; j < 7; j++) { + bitPosition = (1 << j) - 1; + + if (LFSR86540(LFSRstate)) { + keccakRoundConstants[i] ^= 1L << bitPosition; + } + } + } + + return keccakRoundConstants; + } + + private static boolean LFSR86540(byte[] LFSR) { + boolean result = (((LFSR[0]) & 0x01) != 0); + + if (((LFSR[0]) & 0x80) != 0) { + LFSR[0] = (byte) (((LFSR[0]) << 1) ^ 0x71); + } else { + LFSR[0] <<= 1; + } + + return result; + } + + private static int[] keccakInitializeRhoOffsets() { + int[] keccakRhoOffsets = new int[25]; + int x, y, t, newX, newY; + + keccakRhoOffsets[(((0) % 5) + 5 * ((0) % 5))] = 0; + x = 1; + y = 0; + + for (t = 0; t < 24; t++) { + keccakRhoOffsets[(((x) % 5) + 5 * ((y) % 5))] = ((t + 1) * (t + 2) / 2) % 64; + newX = (0 * x + 1 * y) % 5; + newY = (2 * x + 3 * y) % 5; + x = newX; + y = newY; + } + + return keccakRhoOffsets; + } + + protected byte[] state = new byte[(1600 / 8)]; + protected byte[] dataQueue = new byte[(1536 / 8)]; + protected int rate; + protected int bitsInQueue; + protected int fixedOutputLength; + protected boolean squeezing; + protected int bitsAvailableForSqueezing; + protected byte[] chunk; + protected byte[] oneByte; + + private void clearDataQueueSection(int off, int len) { + for (int i = off; i != off + len; i++) { + dataQueue[i] = 0; + } + } + + public KeccakDigest() { + this(288); + } + + public KeccakDigest(int bitLength) { + init(bitLength); + } + + public KeccakDigest(KeccakDigest source) { + System.arraycopy(source.state, 0, this.state, 0, source.state.length); + System.arraycopy(source.dataQueue, 0, this.dataQueue, 0, source.dataQueue.length); + this.rate = source.rate; + this.bitsInQueue = source.bitsInQueue; + this.fixedOutputLength = source.fixedOutputLength; + this.squeezing = source.squeezing; + this.bitsAvailableForSqueezing = source.bitsAvailableForSqueezing; + this.chunk = Utils.arraysCopy(source.chunk); + this.oneByte = Utils.arraysCopy(source.oneByte); + } + + public String getAlgorithmName() { + return "Keccak-" + fixedOutputLength; + } + + public int getDigestSize() { + return fixedOutputLength / 8; + } + + public void update(byte in) { + oneByte[0] = in; + + absorb(oneByte, 0, 8L); + } + + public void update(byte[] in, int inOff, int len) { + absorb(in, inOff, len * 8L); + } + + public int doFinal(byte[] out, int outOff) { + squeeze(out, outOff, fixedOutputLength); + reset(); + + return getDigestSize(); + } + + /* + * TODO Possible API change to support partial-byte suffixes. + */ + protected int doFinal(byte[] out, int outOff, byte partialByte, int partialBits) { + if (partialBits > 0) { + oneByte[0] = partialByte; + absorb(oneByte, 0, partialBits); + } + + squeeze(out, outOff, fixedOutputLength); + reset(); + + return getDigestSize(); + } + + public void reset() { + init(fixedOutputLength); + } + + /** + * Return the size of block that the compression function is applied to in bytes. + * + * @return internal byte length of a block. + */ + public int getByteLength() { + return rate / 8; + } + + private void init(int bitLength) { + switch (bitLength) { + case 288: + initSponge(1024, 576); + break; + case 128: + initSponge(1344, 256); + break; + case 224: + initSponge(1152, 448); + break; + case 256: + initSponge(1088, 512); + break; + case 384: + initSponge(832, 768); + break; + case 512: + initSponge(576, 1024); + break; + default: + throw new IllegalArgumentException("bitLength must be one of 128, 224, 256, 288, 384, or 512."); + } + } + + private void initSponge(int rate, int capacity) { + if (rate + capacity != 1600) { + throw new IllegalStateException("rate + capacity != 1600"); + } + + if ((rate <= 0) || (rate >= 1600) || ((rate % 64) != 0)) { + throw new IllegalStateException("invalid rate value"); + } + + this.rate = rate; + // this is never read, need to check to see why we want to save it + // this.capacity = capacity; + Utils.arrayFill(this.state, (byte) 0); + Utils.arrayFill(this.dataQueue, (byte) 0); + this.bitsInQueue = 0; + this.squeezing = false; + this.bitsAvailableForSqueezing = 0; + this.fixedOutputLength = capacity / 2; + this.chunk = new byte[rate / 8]; + this.oneByte = new byte[1]; + } + + private void absorbQueue() { + KeccakAbsorb(state, dataQueue, rate / 8); + bitsInQueue = 0; + } + + protected void absorb(byte[] data, int off, long databitlen) { + long i, j, wholeBlocks; + + if ((bitsInQueue % 8) != 0) { + throw new IllegalStateException("attempt to absorb with odd length queue"); + } + + if (squeezing) { + throw new IllegalStateException("attempt to absorb while squeezing"); + } + + i = 0; + + while (i < databitlen) { + if ((bitsInQueue == 0) && (databitlen >= rate) && (i <= (databitlen - rate))) { + wholeBlocks = (databitlen - i) / rate; + + for (j = 0; j < wholeBlocks; j++) { + System.arraycopy(data, (int) (off + (i / 8) + (j * chunk.length)), chunk, 0, chunk.length); + +// displayIntermediateValues.displayBytes(1, "Block to be absorbed", curData, rate / 8); + + KeccakAbsorb(state, chunk, chunk.length); + } + + i += wholeBlocks * rate; + } else { + int partialBlock = (int) (databitlen - i); + + if (partialBlock + bitsInQueue > rate) { + partialBlock = rate - bitsInQueue; + } + + int partialByte = partialBlock % 8; + partialBlock -= partialByte; + System.arraycopy(data, off + (int) (i / 8), dataQueue, bitsInQueue / 8, partialBlock / 8); + + bitsInQueue += partialBlock; + i += partialBlock; + + if (bitsInQueue == rate) { + absorbQueue(); + } + + if (partialByte > 0) { + int mask = (1 << partialByte) - 1; + dataQueue[bitsInQueue / 8] = (byte) (data[off + ((int) (i / 8))] & mask); + bitsInQueue += partialByte; + i += partialByte; + } + } + } + } + + private void padAndSwitchToSqueezingPhase() { + if (bitsInQueue + 1 == rate) { + dataQueue[bitsInQueue / 8] |= 1 << (bitsInQueue % 8); + absorbQueue(); + clearDataQueueSection(0, rate / 8); + } else { + clearDataQueueSection((bitsInQueue + 7) / 8, rate / 8 - (bitsInQueue + 7) / 8); + dataQueue[bitsInQueue / 8] |= 1 << (bitsInQueue % 8); + } + + dataQueue[(rate - 1) / 8] |= 1 << ((rate - 1) % 8); + absorbQueue(); + +// displayIntermediateValues.displayText(1, "--- Switching to squeezing phase ---"); + + if (rate == 1024) { + KeccakExtract1024bits(state, dataQueue); + bitsAvailableForSqueezing = 1024; + } else { + KeccakExtract(state, dataQueue, rate / 64); + bitsAvailableForSqueezing = rate; + } + +// displayIntermediateValues.displayBytes(1, "Block available for squeezing", dataQueue, bitsAvailableForSqueezing / 8); + + squeezing = true; + } + + protected void squeeze(byte[] output, int offset, long outputLength) { + long i; + int partialBlock; + + if (!squeezing) { + padAndSwitchToSqueezingPhase(); + } + + if ((outputLength % 8) != 0) { + throw new IllegalStateException("outputLength not a multiple of 8"); + } + + i = 0; + + while (i < outputLength) { + if (bitsAvailableForSqueezing == 0) { + keccakPermutation(state); + + if (rate == 1024) { + KeccakExtract1024bits(state, dataQueue); + bitsAvailableForSqueezing = 1024; + } else { + KeccakExtract(state, dataQueue, rate / 64); + bitsAvailableForSqueezing = rate; + } + +// displayIntermediateValues.displayBytes(1, "Block available for squeezing", dataQueue, bitsAvailableForSqueezing / 8); + + } + + partialBlock = bitsAvailableForSqueezing; + + if ((long) partialBlock > outputLength - i) { + partialBlock = (int) (outputLength - i); + } + + System.arraycopy(dataQueue, (rate - bitsAvailableForSqueezing) / 8, output, offset + (int) (i / 8), partialBlock / 8); + bitsAvailableForSqueezing -= partialBlock; + i += partialBlock; + } + } + + private void fromBytesToWords(long[] stateAsWords, byte[] state) { + for (int i = 0; i < (1600 / 64); i++) { + stateAsWords[i] = 0; + int index = i * (64 / 8); + + for (int j = 0; j < (64 / 8); j++) { + stateAsWords[i] |= ((long) state[index + j] & 0xff) << ((8 * j)); + } + } + } + + private void fromWordsToBytes(byte[] state, long[] stateAsWords) { + for (int i = 0; i < (1600 / 64); i++) { + int index = i * (64 / 8); + + for (int j = 0; j < (64 / 8); j++) { + state[index + j] = (byte) ((stateAsWords[i] >>> ((8 * j))) & 0xFF); + } + } + } + + private void keccakPermutation(byte[] state) { + long[] longState = new long[state.length / 8]; + + fromBytesToWords(longState, state); + +// displayIntermediateValues.displayStateAsBytes(1, "Input of permutation", longState); + + keccakPermutationOnWords(longState); + +// displayIntermediateValues.displayStateAsBytes(1, "State after permutation", longState); + + fromWordsToBytes(state, longState); + } + + private void keccakPermutationAfterXor(byte[] state, byte[] data, int dataLengthInBytes) { + int i; + + for (i = 0; i < dataLengthInBytes; i++) { + state[i] ^= data[i]; + } + + keccakPermutation(state); + } + + private void keccakPermutationOnWords(long[] state) { + int i; + +// displayIntermediateValues.displayStateAs64bitWords(3, "Same, with lanes as 64-bit words", state); + + for (i = 0; i < 24; i++) { +// displayIntermediateValues.displayRoundNumber(3, i); + + theta(state); +// displayIntermediateValues.displayStateAs64bitWords(3, "After theta", state); + + rho(state); +// displayIntermediateValues.displayStateAs64bitWords(3, "After rho", state); + + pi(state); +// displayIntermediateValues.displayStateAs64bitWords(3, "After pi", state); + + chi(state); +// displayIntermediateValues.displayStateAs64bitWords(3, "After chi", state); + + iota(state, i); +// displayIntermediateValues.displayStateAs64bitWords(3, "After iota", state); + } + } + + long[] C = new long[5]; + + private void theta(long[] A) { + for (int x = 0; x < 5; x++) { + C[x] = 0; + + for (int y = 0; y < 5; y++) { + C[x] ^= A[x + 5 * y]; + } + } + for (int x = 0; x < 5; x++) { + long dX = ((((C[(x + 1) % 5]) << 1) ^ ((C[(x + 1) % 5]) >>> (64 - 1)))) ^ C[(x + 4) % 5]; + + for (int y = 0; y < 5; y++) { + A[x + 5 * y] ^= dX; + } + } + } + + private void rho(long[] A) { + for (int x = 0; x < 5; x++) { + for (int y = 0; y < 5; y++) { + int index = x + 5 * y; + A[index] = ((KeccakRhoOffsets[index] != 0) ? (((A[index]) << KeccakRhoOffsets[index]) ^ ((A[index]) >>> (64 - KeccakRhoOffsets[index]))) : A[index]); + } + } + } + + long[] tempA = new long[25]; + + private void pi(long[] A) { + System.arraycopy(A, 0, tempA, 0, tempA.length); + + for (int x = 0; x < 5; x++) { + for (int y = 0; y < 5; y++) { + A[y + 5 * ((2 * x + 3 * y) % 5)] = tempA[x + 5 * y]; + } + } + } + + long[] chiC = new long[5]; + + private void chi(long[] A) { + for (int y = 0; y < 5; y++) { + for (int x = 0; x < 5; x++) { + chiC[x] = A[x + 5 * y] ^ ((~A[(((x + 1) % 5) + 5 * y)]) & A[(((x + 2) % 5) + 5 * y)]); + } + + for (int x = 0; x < 5; x++) { + A[x + 5 * y] = chiC[x]; + } + } + } + + private void iota(long[] A, int indexRound) { + A[(((0) % 5) + 5 * ((0) % 5))] ^= KeccakRoundConstants[indexRound]; + } + + private void KeccakAbsorb(byte[] byteState, byte[] data, int dataInBytes) { + keccakPermutationAfterXor(byteState, data, dataInBytes); + } + + private void KeccakExtract1024bits(byte[] byteState, byte[] data) { + System.arraycopy(byteState, 0, data, 0, 128); + } + + private void KeccakExtract(byte[] byteState, byte[] data, int laneCount) { + System.arraycopy(byteState, 0, data, 0, laneCount * 8); + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/crypto/Memoable.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/crypto/Memoable.java new file mode 100644 index 000000000..062b836a3 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/crypto/Memoable.java @@ -0,0 +1,28 @@ +package com.vitorpamplona.quartz.ots.crypto; + +/** + * Interface for Memoable objects. Memoable objects allow the taking of a snapshot of their internal state + * via the {@link #copy copy()} method and then resetting the object back to that state later using the + * {@link #reset reset()} method. + */ +public interface Memoable { + /** + * Produce a copy of this object with its configuration and in its current state. + *

+ * The returned object may be used simply to store the state, or may be used as a similar object + * starting from the copied state. + * + * @return Memoable object + */ + Memoable copy(); + + /** + * Restore a copied object state into this object. + *

+ * Implementations of this method should try to avoid or minimise memory allocation to perform the reset. + * + * @param other an object originally {@link #copy() copied} from an object of the same type as this instance. + * @throws ClassCastException if the provided object is not of the correct type. + */ + void reset(Memoable other); +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/crypto/Pack.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/crypto/Pack.java new file mode 100644 index 000000000..17c839703 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/crypto/Pack.java @@ -0,0 +1,242 @@ +package com.vitorpamplona.quartz.ots.crypto; + +/** + * Utility methods for converting byte arrays into ints and longs, and back again. + */ +public abstract class Pack +{ + public static short bigEndianToShort(byte[] bs, int off) + { + int n = (bs[ off] & 0xff) << 8; + n |= (bs[++off] & 0xff); + return (short)n; + } + + public static int bigEndianToInt(byte[] bs, int off) + { + int n = bs[ off] << 24; + n |= (bs[++off] & 0xff) << 16; + n |= (bs[++off] & 0xff) << 8; + n |= (bs[++off] & 0xff); + return n; + } + + public static void bigEndianToInt(byte[] bs, int off, int[] ns) + { + for (int i = 0; i < ns.length; ++i) + { + ns[i] = bigEndianToInt(bs, off); + off += 4; + } + } + + public static byte[] intToBigEndian(int n) + { + byte[] bs = new byte[4]; + intToBigEndian(n, bs, 0); + return bs; + } + + public static void intToBigEndian(int n, byte[] bs, int off) + { + bs[ off] = (byte)(n >>> 24); + bs[++off] = (byte)(n >>> 16); + bs[++off] = (byte)(n >>> 8); + bs[++off] = (byte)(n ); + } + + public static byte[] intToBigEndian(int[] ns) + { + byte[] bs = new byte[4 * ns.length]; + intToBigEndian(ns, bs, 0); + return bs; + } + + public static void intToBigEndian(int[] ns, byte[] bs, int off) + { + for (int i = 0; i < ns.length; ++i) + { + intToBigEndian(ns[i], bs, off); + off += 4; + } + } + + public static long bigEndianToLong(byte[] bs, int off) + { + int hi = bigEndianToInt(bs, off); + int lo = bigEndianToInt(bs, off + 4); + return ((long)(hi & 0xffffffffL) << 32) | (long)(lo & 0xffffffffL); + } + + public static void bigEndianToLong(byte[] bs, int off, long[] ns) + { + for (int i = 0; i < ns.length; ++i) + { + ns[i] = bigEndianToLong(bs, off); + off += 8; + } + } + + public static byte[] longToBigEndian(long n) + { + byte[] bs = new byte[8]; + longToBigEndian(n, bs, 0); + return bs; + } + + public static void longToBigEndian(long n, byte[] bs, int off) + { + intToBigEndian((int)(n >>> 32), bs, off); + intToBigEndian((int)(n & 0xffffffffL), bs, off + 4); + } + + public static byte[] longToBigEndian(long[] ns) + { + byte[] bs = new byte[8 * ns.length]; + longToBigEndian(ns, bs, 0); + return bs; + } + + public static void longToBigEndian(long[] ns, byte[] bs, int off) + { + for (int i = 0; i < ns.length; ++i) + { + longToBigEndian(ns[i], bs, off); + off += 8; + } + } + + public static short littleEndianToShort(byte[] bs, int off) + { + int n = bs[ off] & 0xff; + n |= (bs[++off] & 0xff) << 8; + return (short)n; + } + + public static int littleEndianToInt(byte[] bs, int off) + { + int n = bs[ off] & 0xff; + n |= (bs[++off] & 0xff) << 8; + n |= (bs[++off] & 0xff) << 16; + n |= bs[++off] << 24; + return n; + } + + public static void littleEndianToInt(byte[] bs, int off, int[] ns) + { + for (int i = 0; i < ns.length; ++i) + { + ns[i] = littleEndianToInt(bs, off); + off += 4; + } + } + + public static void littleEndianToInt(byte[] bs, int bOff, int[] ns, int nOff, int count) + { + for (int i = 0; i < count; ++i) + { + ns[nOff + i] = littleEndianToInt(bs, bOff); + bOff += 4; + } + } + + public static int[] littleEndianToInt(byte[] bs, int off, int count) + { + int[] ns = new int[count]; + for (int i = 0; i < ns.length; ++i) + { + ns[i] = littleEndianToInt(bs, off); + off += 4; + } + return ns; + } + + public static byte[] shortToLittleEndian(short n) + { + byte[] bs = new byte[2]; + shortToLittleEndian(n, bs, 0); + return bs; + } + + public static void shortToLittleEndian(short n, byte[] bs, int off) + { + bs[ off] = (byte)(n ); + bs[++off] = (byte)(n >>> 8); + } + + public static byte[] intToLittleEndian(int n) + { + byte[] bs = new byte[4]; + intToLittleEndian(n, bs, 0); + return bs; + } + + public static void intToLittleEndian(int n, byte[] bs, int off) + { + bs[ off] = (byte)(n ); + bs[++off] = (byte)(n >>> 8); + bs[++off] = (byte)(n >>> 16); + bs[++off] = (byte)(n >>> 24); + } + + public static byte[] intToLittleEndian(int[] ns) + { + byte[] bs = new byte[4 * ns.length]; + intToLittleEndian(ns, bs, 0); + return bs; + } + + public static void intToLittleEndian(int[] ns, byte[] bs, int off) + { + for (int i = 0; i < ns.length; ++i) + { + intToLittleEndian(ns[i], bs, off); + off += 4; + } + } + + public static long littleEndianToLong(byte[] bs, int off) + { + int lo = littleEndianToInt(bs, off); + int hi = littleEndianToInt(bs, off + 4); + return ((long)(hi & 0xffffffffL) << 32) | (long)(lo & 0xffffffffL); + } + + public static void littleEndianToLong(byte[] bs, int off, long[] ns) + { + for (int i = 0; i < ns.length; ++i) + { + ns[i] = littleEndianToLong(bs, off); + off += 8; + } + } + + public static byte[] longToLittleEndian(long n) + { + byte[] bs = new byte[8]; + longToLittleEndian(n, bs, 0); + return bs; + } + + public static void longToLittleEndian(long n, byte[] bs, int off) + { + intToLittleEndian((int)(n & 0xffffffffL), bs, off); + intToLittleEndian((int)(n >>> 32), bs, off + 4); + } + + public static byte[] longToLittleEndian(long[] ns) + { + byte[] bs = new byte[8 * ns.length]; + longToLittleEndian(ns, bs, 0); + return bs; + } + + public static void longToLittleEndian(long[] ns, byte[] bs, int off) + { + for (int i = 0; i < ns.length; ++i) + { + longToLittleEndian(ns[i], bs, off); + off += 8; + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/crypto/RIPEMD160Digest.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/crypto/RIPEMD160Digest.java new file mode 100644 index 000000000..51dfc8a69 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/crypto/RIPEMD160Digest.java @@ -0,0 +1,442 @@ +package com.vitorpamplona.quartz.ots.crypto; + +/** + * Implementation of RIPEMD + * + * @see ripemd160 + */ +public class RIPEMD160Digest + extends GeneralDigest +{ + private static final int DIGEST_LENGTH = 20; + + private int H0, H1, H2, H3, H4; // IV's + + private int[] X = new int[16]; + private int xOff; + + /** + * Standard constructor + */ + public RIPEMD160Digest() + { + reset(); + } + + /** + * Copy constructor. This will copy the state of the provided + * message digest. + * @param t RIPEMD160Digest + */ + public RIPEMD160Digest(RIPEMD160Digest t) + { + super(t); + + copyIn(t); + } + + private void copyIn(RIPEMD160Digest t) + { + super.copyIn(t); + + H0 = t.H0; + H1 = t.H1; + H2 = t.H2; + H3 = t.H3; + H4 = t.H4; + + System.arraycopy(t.X, 0, X, 0, t.X.length); + xOff = t.xOff; + } + + public String getAlgorithmName() + { + return "RIPEMD160"; + } + + public int getDigestSize() + { + return DIGEST_LENGTH; + } + + protected void processWord( + byte[] in, + int inOff) + { + X[xOff++] = (in[inOff] & 0xff) | ((in[inOff + 1] & 0xff) << 8) + | ((in[inOff + 2] & 0xff) << 16) | ((in[inOff + 3] & 0xff) << 24); + + if (xOff == 16) + { + processBlock(); + } + } + + protected void processLength( + long bitLength) + { + if (xOff > 14) + { + processBlock(); + } + + X[14] = (int)(bitLength & 0xffffffff); + X[15] = (int)(bitLength >>> 32); + } + + private void unpackWord( + int word, + byte[] out, + int outOff) + { + out[outOff] = (byte)word; + out[outOff + 1] = (byte)(word >>> 8); + out[outOff + 2] = (byte)(word >>> 16); + out[outOff + 3] = (byte)(word >>> 24); + } + + public int doFinal( + byte[] out, + int outOff) + { + finish(); + + unpackWord(H0, out, outOff); + unpackWord(H1, out, outOff + 4); + unpackWord(H2, out, outOff + 8); + unpackWord(H3, out, outOff + 12); + unpackWord(H4, out, outOff + 16); + + reset(); + + return DIGEST_LENGTH; + } + + /** + * reset the chaining variables to the IV values. + */ + public void reset() + { + super.reset(); + + H0 = 0x67452301; + H1 = 0xefcdab89; + H2 = 0x98badcfe; + H3 = 0x10325476; + H4 = 0xc3d2e1f0; + + xOff = 0; + + for (int i = 0; i != X.length; i++) + { + X[i] = 0; + } + } + + /* + * rotate int x left n bits. + */ + private int RL( + int x, + int n) + { + return (x << n) | (x >>> (32 - n)); + } + + /* + * f1,f2,f3,f4,f5 are the basic RIPEMD160 functions. + */ + + /* + * rounds 0-15 + */ + private int f1( + int x, + int y, + int z) + { + return x ^ y ^ z; + } + + /* + * rounds 16-31 + */ + private int f2( + int x, + int y, + int z) + { + return (x & y) | (~x & z); + } + + /* + * rounds 32-47 + */ + private int f3( + int x, + int y, + int z) + { + return (x | ~y) ^ z; + } + + /* + * rounds 48-63 + */ + private int f4( + int x, + int y, + int z) + { + return (x & z) | (y & ~z); + } + + /* + * rounds 64-79 + */ + private int f5( + int x, + int y, + int z) + { + return x ^ (y | ~z); + } + + protected void processBlock() + { + int a, aa; + int b, bb; + int c, cc; + int d, dd; + int e, ee; + + a = aa = H0; + b = bb = H1; + c = cc = H2; + d = dd = H3; + e = ee = H4; + + // + // Rounds 1 - 16 + // + // left + a = RL(a + f1(b,c,d) + X[ 0], 11) + e; c = RL(c, 10); + e = RL(e + f1(a,b,c) + X[ 1], 14) + d; b = RL(b, 10); + d = RL(d + f1(e,a,b) + X[ 2], 15) + c; a = RL(a, 10); + c = RL(c + f1(d,e,a) + X[ 3], 12) + b; e = RL(e, 10); + b = RL(b + f1(c,d,e) + X[ 4], 5) + a; d = RL(d, 10); + a = RL(a + f1(b,c,d) + X[ 5], 8) + e; c = RL(c, 10); + e = RL(e + f1(a,b,c) + X[ 6], 7) + d; b = RL(b, 10); + d = RL(d + f1(e,a,b) + X[ 7], 9) + c; a = RL(a, 10); + c = RL(c + f1(d,e,a) + X[ 8], 11) + b; e = RL(e, 10); + b = RL(b + f1(c,d,e) + X[ 9], 13) + a; d = RL(d, 10); + a = RL(a + f1(b,c,d) + X[10], 14) + e; c = RL(c, 10); + e = RL(e + f1(a,b,c) + X[11], 15) + d; b = RL(b, 10); + d = RL(d + f1(e,a,b) + X[12], 6) + c; a = RL(a, 10); + c = RL(c + f1(d,e,a) + X[13], 7) + b; e = RL(e, 10); + b = RL(b + f1(c,d,e) + X[14], 9) + a; d = RL(d, 10); + a = RL(a + f1(b,c,d) + X[15], 8) + e; c = RL(c, 10); + + // right + aa = RL(aa + f5(bb,cc,dd) + X[ 5] + 0x50a28be6, 8) + ee; cc = RL(cc, 10); + ee = RL(ee + f5(aa,bb,cc) + X[14] + 0x50a28be6, 9) + dd; bb = RL(bb, 10); + dd = RL(dd + f5(ee,aa,bb) + X[ 7] + 0x50a28be6, 9) + cc; aa = RL(aa, 10); + cc = RL(cc + f5(dd,ee,aa) + X[ 0] + 0x50a28be6, 11) + bb; ee = RL(ee, 10); + bb = RL(bb + f5(cc,dd,ee) + X[ 9] + 0x50a28be6, 13) + aa; dd = RL(dd, 10); + aa = RL(aa + f5(bb,cc,dd) + X[ 2] + 0x50a28be6, 15) + ee; cc = RL(cc, 10); + ee = RL(ee + f5(aa,bb,cc) + X[11] + 0x50a28be6, 15) + dd; bb = RL(bb, 10); + dd = RL(dd + f5(ee,aa,bb) + X[ 4] + 0x50a28be6, 5) + cc; aa = RL(aa, 10); + cc = RL(cc + f5(dd,ee,aa) + X[13] + 0x50a28be6, 7) + bb; ee = RL(ee, 10); + bb = RL(bb + f5(cc,dd,ee) + X[ 6] + 0x50a28be6, 7) + aa; dd = RL(dd, 10); + aa = RL(aa + f5(bb,cc,dd) + X[15] + 0x50a28be6, 8) + ee; cc = RL(cc, 10); + ee = RL(ee + f5(aa,bb,cc) + X[ 8] + 0x50a28be6, 11) + dd; bb = RL(bb, 10); + dd = RL(dd + f5(ee,aa,bb) + X[ 1] + 0x50a28be6, 14) + cc; aa = RL(aa, 10); + cc = RL(cc + f5(dd,ee,aa) + X[10] + 0x50a28be6, 14) + bb; ee = RL(ee, 10); + bb = RL(bb + f5(cc,dd,ee) + X[ 3] + 0x50a28be6, 12) + aa; dd = RL(dd, 10); + aa = RL(aa + f5(bb,cc,dd) + X[12] + 0x50a28be6, 6) + ee; cc = RL(cc, 10); + + // + // Rounds 16-31 + // + // left + e = RL(e + f2(a,b,c) + X[ 7] + 0x5a827999, 7) + d; b = RL(b, 10); + d = RL(d + f2(e,a,b) + X[ 4] + 0x5a827999, 6) + c; a = RL(a, 10); + c = RL(c + f2(d,e,a) + X[13] + 0x5a827999, 8) + b; e = RL(e, 10); + b = RL(b + f2(c,d,e) + X[ 1] + 0x5a827999, 13) + a; d = RL(d, 10); + a = RL(a + f2(b,c,d) + X[10] + 0x5a827999, 11) + e; c = RL(c, 10); + e = RL(e + f2(a,b,c) + X[ 6] + 0x5a827999, 9) + d; b = RL(b, 10); + d = RL(d + f2(e,a,b) + X[15] + 0x5a827999, 7) + c; a = RL(a, 10); + c = RL(c + f2(d,e,a) + X[ 3] + 0x5a827999, 15) + b; e = RL(e, 10); + b = RL(b + f2(c,d,e) + X[12] + 0x5a827999, 7) + a; d = RL(d, 10); + a = RL(a + f2(b,c,d) + X[ 0] + 0x5a827999, 12) + e; c = RL(c, 10); + e = RL(e + f2(a,b,c) + X[ 9] + 0x5a827999, 15) + d; b = RL(b, 10); + d = RL(d + f2(e,a,b) + X[ 5] + 0x5a827999, 9) + c; a = RL(a, 10); + c = RL(c + f2(d,e,a) + X[ 2] + 0x5a827999, 11) + b; e = RL(e, 10); + b = RL(b + f2(c,d,e) + X[14] + 0x5a827999, 7) + a; d = RL(d, 10); + a = RL(a + f2(b,c,d) + X[11] + 0x5a827999, 13) + e; c = RL(c, 10); + e = RL(e + f2(a,b,c) + X[ 8] + 0x5a827999, 12) + d; b = RL(b, 10); + + // right + ee = RL(ee + f4(aa,bb,cc) + X[ 6] + 0x5c4dd124, 9) + dd; bb = RL(bb, 10); + dd = RL(dd + f4(ee,aa,bb) + X[11] + 0x5c4dd124, 13) + cc; aa = RL(aa, 10); + cc = RL(cc + f4(dd,ee,aa) + X[ 3] + 0x5c4dd124, 15) + bb; ee = RL(ee, 10); + bb = RL(bb + f4(cc,dd,ee) + X[ 7] + 0x5c4dd124, 7) + aa; dd = RL(dd, 10); + aa = RL(aa + f4(bb,cc,dd) + X[ 0] + 0x5c4dd124, 12) + ee; cc = RL(cc, 10); + ee = RL(ee + f4(aa,bb,cc) + X[13] + 0x5c4dd124, 8) + dd; bb = RL(bb, 10); + dd = RL(dd + f4(ee,aa,bb) + X[ 5] + 0x5c4dd124, 9) + cc; aa = RL(aa, 10); + cc = RL(cc + f4(dd,ee,aa) + X[10] + 0x5c4dd124, 11) + bb; ee = RL(ee, 10); + bb = RL(bb + f4(cc,dd,ee) + X[14] + 0x5c4dd124, 7) + aa; dd = RL(dd, 10); + aa = RL(aa + f4(bb,cc,dd) + X[15] + 0x5c4dd124, 7) + ee; cc = RL(cc, 10); + ee = RL(ee + f4(aa,bb,cc) + X[ 8] + 0x5c4dd124, 12) + dd; bb = RL(bb, 10); + dd = RL(dd + f4(ee,aa,bb) + X[12] + 0x5c4dd124, 7) + cc; aa = RL(aa, 10); + cc = RL(cc + f4(dd,ee,aa) + X[ 4] + 0x5c4dd124, 6) + bb; ee = RL(ee, 10); + bb = RL(bb + f4(cc,dd,ee) + X[ 9] + 0x5c4dd124, 15) + aa; dd = RL(dd, 10); + aa = RL(aa + f4(bb,cc,dd) + X[ 1] + 0x5c4dd124, 13) + ee; cc = RL(cc, 10); + ee = RL(ee + f4(aa,bb,cc) + X[ 2] + 0x5c4dd124, 11) + dd; bb = RL(bb, 10); + + // + // Rounds 32-47 + // + // left + d = RL(d + f3(e,a,b) + X[ 3] + 0x6ed9eba1, 11) + c; a = RL(a, 10); + c = RL(c + f3(d,e,a) + X[10] + 0x6ed9eba1, 13) + b; e = RL(e, 10); + b = RL(b + f3(c,d,e) + X[14] + 0x6ed9eba1, 6) + a; d = RL(d, 10); + a = RL(a + f3(b,c,d) + X[ 4] + 0x6ed9eba1, 7) + e; c = RL(c, 10); + e = RL(e + f3(a,b,c) + X[ 9] + 0x6ed9eba1, 14) + d; b = RL(b, 10); + d = RL(d + f3(e,a,b) + X[15] + 0x6ed9eba1, 9) + c; a = RL(a, 10); + c = RL(c + f3(d,e,a) + X[ 8] + 0x6ed9eba1, 13) + b; e = RL(e, 10); + b = RL(b + f3(c,d,e) + X[ 1] + 0x6ed9eba1, 15) + a; d = RL(d, 10); + a = RL(a + f3(b,c,d) + X[ 2] + 0x6ed9eba1, 14) + e; c = RL(c, 10); + e = RL(e + f3(a,b,c) + X[ 7] + 0x6ed9eba1, 8) + d; b = RL(b, 10); + d = RL(d + f3(e,a,b) + X[ 0] + 0x6ed9eba1, 13) + c; a = RL(a, 10); + c = RL(c + f3(d,e,a) + X[ 6] + 0x6ed9eba1, 6) + b; e = RL(e, 10); + b = RL(b + f3(c,d,e) + X[13] + 0x6ed9eba1, 5) + a; d = RL(d, 10); + a = RL(a + f3(b,c,d) + X[11] + 0x6ed9eba1, 12) + e; c = RL(c, 10); + e = RL(e + f3(a,b,c) + X[ 5] + 0x6ed9eba1, 7) + d; b = RL(b, 10); + d = RL(d + f3(e,a,b) + X[12] + 0x6ed9eba1, 5) + c; a = RL(a, 10); + + // right + dd = RL(dd + f3(ee,aa,bb) + X[15] + 0x6d703ef3, 9) + cc; aa = RL(aa, 10); + cc = RL(cc + f3(dd,ee,aa) + X[ 5] + 0x6d703ef3, 7) + bb; ee = RL(ee, 10); + bb = RL(bb + f3(cc,dd,ee) + X[ 1] + 0x6d703ef3, 15) + aa; dd = RL(dd, 10); + aa = RL(aa + f3(bb,cc,dd) + X[ 3] + 0x6d703ef3, 11) + ee; cc = RL(cc, 10); + ee = RL(ee + f3(aa,bb,cc) + X[ 7] + 0x6d703ef3, 8) + dd; bb = RL(bb, 10); + dd = RL(dd + f3(ee,aa,bb) + X[14] + 0x6d703ef3, 6) + cc; aa = RL(aa, 10); + cc = RL(cc + f3(dd,ee,aa) + X[ 6] + 0x6d703ef3, 6) + bb; ee = RL(ee, 10); + bb = RL(bb + f3(cc,dd,ee) + X[ 9] + 0x6d703ef3, 14) + aa; dd = RL(dd, 10); + aa = RL(aa + f3(bb,cc,dd) + X[11] + 0x6d703ef3, 12) + ee; cc = RL(cc, 10); + ee = RL(ee + f3(aa,bb,cc) + X[ 8] + 0x6d703ef3, 13) + dd; bb = RL(bb, 10); + dd = RL(dd + f3(ee,aa,bb) + X[12] + 0x6d703ef3, 5) + cc; aa = RL(aa, 10); + cc = RL(cc + f3(dd,ee,aa) + X[ 2] + 0x6d703ef3, 14) + bb; ee = RL(ee, 10); + bb = RL(bb + f3(cc,dd,ee) + X[10] + 0x6d703ef3, 13) + aa; dd = RL(dd, 10); + aa = RL(aa + f3(bb,cc,dd) + X[ 0] + 0x6d703ef3, 13) + ee; cc = RL(cc, 10); + ee = RL(ee + f3(aa,bb,cc) + X[ 4] + 0x6d703ef3, 7) + dd; bb = RL(bb, 10); + dd = RL(dd + f3(ee,aa,bb) + X[13] + 0x6d703ef3, 5) + cc; aa = RL(aa, 10); + + // + // Rounds 48-63 + // + // left + c = RL(c + f4(d,e,a) + X[ 1] + 0x8f1bbcdc, 11) + b; e = RL(e, 10); + b = RL(b + f4(c,d,e) + X[ 9] + 0x8f1bbcdc, 12) + a; d = RL(d, 10); + a = RL(a + f4(b,c,d) + X[11] + 0x8f1bbcdc, 14) + e; c = RL(c, 10); + e = RL(e + f4(a,b,c) + X[10] + 0x8f1bbcdc, 15) + d; b = RL(b, 10); + d = RL(d + f4(e,a,b) + X[ 0] + 0x8f1bbcdc, 14) + c; a = RL(a, 10); + c = RL(c + f4(d,e,a) + X[ 8] + 0x8f1bbcdc, 15) + b; e = RL(e, 10); + b = RL(b + f4(c,d,e) + X[12] + 0x8f1bbcdc, 9) + a; d = RL(d, 10); + a = RL(a + f4(b,c,d) + X[ 4] + 0x8f1bbcdc, 8) + e; c = RL(c, 10); + e = RL(e + f4(a,b,c) + X[13] + 0x8f1bbcdc, 9) + d; b = RL(b, 10); + d = RL(d + f4(e,a,b) + X[ 3] + 0x8f1bbcdc, 14) + c; a = RL(a, 10); + c = RL(c + f4(d,e,a) + X[ 7] + 0x8f1bbcdc, 5) + b; e = RL(e, 10); + b = RL(b + f4(c,d,e) + X[15] + 0x8f1bbcdc, 6) + a; d = RL(d, 10); + a = RL(a + f4(b,c,d) + X[14] + 0x8f1bbcdc, 8) + e; c = RL(c, 10); + e = RL(e + f4(a,b,c) + X[ 5] + 0x8f1bbcdc, 6) + d; b = RL(b, 10); + d = RL(d + f4(e,a,b) + X[ 6] + 0x8f1bbcdc, 5) + c; a = RL(a, 10); + c = RL(c + f4(d,e,a) + X[ 2] + 0x8f1bbcdc, 12) + b; e = RL(e, 10); + + // right + cc = RL(cc + f2(dd,ee,aa) + X[ 8] + 0x7a6d76e9, 15) + bb; ee = RL(ee, 10); + bb = RL(bb + f2(cc,dd,ee) + X[ 6] + 0x7a6d76e9, 5) + aa; dd = RL(dd, 10); + aa = RL(aa + f2(bb,cc,dd) + X[ 4] + 0x7a6d76e9, 8) + ee; cc = RL(cc, 10); + ee = RL(ee + f2(aa,bb,cc) + X[ 1] + 0x7a6d76e9, 11) + dd; bb = RL(bb, 10); + dd = RL(dd + f2(ee,aa,bb) + X[ 3] + 0x7a6d76e9, 14) + cc; aa = RL(aa, 10); + cc = RL(cc + f2(dd,ee,aa) + X[11] + 0x7a6d76e9, 14) + bb; ee = RL(ee, 10); + bb = RL(bb + f2(cc,dd,ee) + X[15] + 0x7a6d76e9, 6) + aa; dd = RL(dd, 10); + aa = RL(aa + f2(bb,cc,dd) + X[ 0] + 0x7a6d76e9, 14) + ee; cc = RL(cc, 10); + ee = RL(ee + f2(aa,bb,cc) + X[ 5] + 0x7a6d76e9, 6) + dd; bb = RL(bb, 10); + dd = RL(dd + f2(ee,aa,bb) + X[12] + 0x7a6d76e9, 9) + cc; aa = RL(aa, 10); + cc = RL(cc + f2(dd,ee,aa) + X[ 2] + 0x7a6d76e9, 12) + bb; ee = RL(ee, 10); + bb = RL(bb + f2(cc,dd,ee) + X[13] + 0x7a6d76e9, 9) + aa; dd = RL(dd, 10); + aa = RL(aa + f2(bb,cc,dd) + X[ 9] + 0x7a6d76e9, 12) + ee; cc = RL(cc, 10); + ee = RL(ee + f2(aa,bb,cc) + X[ 7] + 0x7a6d76e9, 5) + dd; bb = RL(bb, 10); + dd = RL(dd + f2(ee,aa,bb) + X[10] + 0x7a6d76e9, 15) + cc; aa = RL(aa, 10); + cc = RL(cc + f2(dd,ee,aa) + X[14] + 0x7a6d76e9, 8) + bb; ee = RL(ee, 10); + + // + // Rounds 64-79 + // + // left + b = RL(b + f5(c,d,e) + X[ 4] + 0xa953fd4e, 9) + a; d = RL(d, 10); + a = RL(a + f5(b,c,d) + X[ 0] + 0xa953fd4e, 15) + e; c = RL(c, 10); + e = RL(e + f5(a,b,c) + X[ 5] + 0xa953fd4e, 5) + d; b = RL(b, 10); + d = RL(d + f5(e,a,b) + X[ 9] + 0xa953fd4e, 11) + c; a = RL(a, 10); + c = RL(c + f5(d,e,a) + X[ 7] + 0xa953fd4e, 6) + b; e = RL(e, 10); + b = RL(b + f5(c,d,e) + X[12] + 0xa953fd4e, 8) + a; d = RL(d, 10); + a = RL(a + f5(b,c,d) + X[ 2] + 0xa953fd4e, 13) + e; c = RL(c, 10); + e = RL(e + f5(a,b,c) + X[10] + 0xa953fd4e, 12) + d; b = RL(b, 10); + d = RL(d + f5(e,a,b) + X[14] + 0xa953fd4e, 5) + c; a = RL(a, 10); + c = RL(c + f5(d,e,a) + X[ 1] + 0xa953fd4e, 12) + b; e = RL(e, 10); + b = RL(b + f5(c,d,e) + X[ 3] + 0xa953fd4e, 13) + a; d = RL(d, 10); + a = RL(a + f5(b,c,d) + X[ 8] + 0xa953fd4e, 14) + e; c = RL(c, 10); + e = RL(e + f5(a,b,c) + X[11] + 0xa953fd4e, 11) + d; b = RL(b, 10); + d = RL(d + f5(e,a,b) + X[ 6] + 0xa953fd4e, 8) + c; a = RL(a, 10); + c = RL(c + f5(d,e,a) + X[15] + 0xa953fd4e, 5) + b; e = RL(e, 10); + b = RL(b + f5(c,d,e) + X[13] + 0xa953fd4e, 6) + a; d = RL(d, 10); + + // right + bb = RL(bb + f1(cc,dd,ee) + X[12], 8) + aa; dd = RL(dd, 10); + aa = RL(aa + f1(bb,cc,dd) + X[15], 5) + ee; cc = RL(cc, 10); + ee = RL(ee + f1(aa,bb,cc) + X[10], 12) + dd; bb = RL(bb, 10); + dd = RL(dd + f1(ee,aa,bb) + X[ 4], 9) + cc; aa = RL(aa, 10); + cc = RL(cc + f1(dd,ee,aa) + X[ 1], 12) + bb; ee = RL(ee, 10); + bb = RL(bb + f1(cc,dd,ee) + X[ 5], 5) + aa; dd = RL(dd, 10); + aa = RL(aa + f1(bb,cc,dd) + X[ 8], 14) + ee; cc = RL(cc, 10); + ee = RL(ee + f1(aa,bb,cc) + X[ 7], 6) + dd; bb = RL(bb, 10); + dd = RL(dd + f1(ee,aa,bb) + X[ 6], 8) + cc; aa = RL(aa, 10); + cc = RL(cc + f1(dd,ee,aa) + X[ 2], 13) + bb; ee = RL(ee, 10); + bb = RL(bb + f1(cc,dd,ee) + X[13], 6) + aa; dd = RL(dd, 10); + aa = RL(aa + f1(bb,cc,dd) + X[14], 5) + ee; cc = RL(cc, 10); + ee = RL(ee + f1(aa,bb,cc) + X[ 0], 15) + dd; bb = RL(bb, 10); + dd = RL(dd + f1(ee,aa,bb) + X[ 3], 13) + cc; aa = RL(aa, 10); + cc = RL(cc + f1(dd,ee,aa) + X[ 9], 11) + bb; ee = RL(ee, 10); + bb = RL(bb + f1(cc,dd,ee) + X[11], 11) + aa; dd = RL(dd, 10); + + dd += c + H1; + H1 = H2 + d + ee; + H2 = H3 + e + aa; + H3 = H4 + a + bb; + H4 = H0 + b + cc; + H0 = dd; + + // + // reset the offset and clean out the word buffer. + // + xOff = 0; + for (int i = 0; i != X.length; i++) + { + X[i] = 0; + } + } + + public Memoable copy() + { + return new RIPEMD160Digest(this); + } + + public void reset(Memoable other) + { + RIPEMD160Digest d = (RIPEMD160Digest)other; + + copyIn(d); + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/exceptions/CommitmentNotFoundException.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/exceptions/CommitmentNotFoundException.java new file mode 100644 index 000000000..14e3fd457 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/exceptions/CommitmentNotFoundException.java @@ -0,0 +1,7 @@ +package com.vitorpamplona.quartz.ots.exceptions; + +public class CommitmentNotFoundException extends Exception { + public CommitmentNotFoundException(String message) { + super(message); + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/exceptions/DeserializationException.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/exceptions/DeserializationException.java new file mode 100644 index 000000000..76da9998b --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/exceptions/DeserializationException.java @@ -0,0 +1,7 @@ +package com.vitorpamplona.quartz.ots.exceptions; + +public class DeserializationException extends Exception { + public DeserializationException(String message) { + super(message); + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/exceptions/ExceededSizeException.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/exceptions/ExceededSizeException.java new file mode 100644 index 000000000..5a921de9d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/exceptions/ExceededSizeException.java @@ -0,0 +1,7 @@ +package com.vitorpamplona.quartz.ots.exceptions; + +public class ExceededSizeException extends Exception { + public ExceededSizeException(String message) { + super(message); + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/exceptions/UrlException.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/exceptions/UrlException.java new file mode 100644 index 000000000..a3d56c192 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/exceptions/UrlException.java @@ -0,0 +1,7 @@ +package com.vitorpamplona.quartz.ots.exceptions; + +public class UrlException extends Exception { + public UrlException(String message) { + super(message); + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/exceptions/VerificationException.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/exceptions/VerificationException.java new file mode 100644 index 000000000..7d53dfedb --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/exceptions/VerificationException.java @@ -0,0 +1,7 @@ +package com.vitorpamplona.quartz.ots.exceptions; + +public class VerificationException extends Exception { + public VerificationException(String message) { + super(message); + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/http/Request.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/http/Request.java new file mode 100644 index 000000000..7a65deac6 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/http/Request.java @@ -0,0 +1,119 @@ +package com.vitorpamplona.quartz.ots.http; + +import android.util.Log; + +import java.io.DataOutputStream; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Callable; +import java.util.zip.GZIPInputStream; + +/** + * For making an HTTP request. + */ +public class Request implements Callable { + private URL url; + private byte[] data; + private Map headers; + private BlockingQueue queue; + + public Request(URL url) { + this.url = url; + } + + public void setData(byte[] data) { + this.data = data; + } + + public void setHeaders(Map headers) { + this.headers = headers; + } + + public void setQueue(BlockingQueue queue) { + this.queue = queue; + } + + @Override + public Response call() throws Exception { + Response response = new Response(); + + try { + HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection(); + httpURLConnection.setReadTimeout(10000); + httpURLConnection.setConnectTimeout(10000); + httpURLConnection.setRequestProperty("User-Agent", "OpenTimestamps Java"); + httpURLConnection.setRequestProperty("Accept", "application/json"); + httpURLConnection.setRequestProperty("Accept-Encoding", "gzip"); + + if (headers != null) { + for (Map.Entry entry : headers.entrySet()) { + httpURLConnection.setRequestProperty(entry.getKey(), entry.getValue()); + } + } + + if (data != null) { + httpURLConnection.setDoOutput(true); + httpURLConnection.setRequestMethod("POST"); + httpURLConnection.setRequestProperty("Content-Length", "" + Integer.toString(this.data.length)); + DataOutputStream wr = new DataOutputStream(httpURLConnection.getOutputStream()); + wr.write(this.data, 0, this.data.length); + wr.flush(); + wr.close(); + } else { + httpURLConnection.setRequestMethod("GET"); + } + + httpURLConnection.connect(); + + int responseCode = httpURLConnection.getResponseCode(); + + Log.i("OpenTimestamp", responseCode + " responseCode "); + + response.setStatus(responseCode); + response.setFromUrl(url.toString()); + InputStream is = httpURLConnection.getInputStream(); + if ("gzip".equals(httpURLConnection.getContentEncoding())) { + is = new GZIPInputStream(is); + } + response.setStream(is); + } catch (Exception e) { + Log.w("OpenTimestamp", url.toString() + " exception " + e); + } finally { + if (queue != null) { + queue.offer(response); + } + } + + return response; + } + + public static String urlEncodeUTF8(String s) { + try { + return URLEncoder.encode(s, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new UnsupportedOperationException(e); + } + } + + public static String urlEncodeUTF8(Map map) { + StringBuilder sb = new StringBuilder(); + + for (Map.Entry entry : map.entrySet()) { + if (sb.length() > 0) { + sb.append("&"); + } + + sb.append(String.format("%s=%s", + urlEncodeUTF8(entry.getKey().toString()), + urlEncodeUTF8(entry.getValue().toString()) + )); + } + + return sb.toString(); + } +} \ No newline at end of file diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/http/Response.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/http/Response.java new file mode 100644 index 000000000..836b61462 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/http/Response.java @@ -0,0 +1,78 @@ +package com.vitorpamplona.quartz.ots.http; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +/** + * Holds the response from an HTTP request. + */ +public class Response { + private InputStream stream; + private String fromUrl; + private Integer status; + + public Response() { + } + + public Response(InputStream stream) { + this.stream = stream; + } + + public void setStream(InputStream stream) { + this.stream = stream; + } + + public Integer getStatus() { + return status; + } + + public void setStatus(Integer status) { + this.status = status; + } + + public boolean isOk() { + return getStatus() != null && 200 == getStatus(); + } + + public String getFromUrl() { + return fromUrl; + } + + public void setFromUrl(String fromUrl) { + this.fromUrl = fromUrl; + } + + public InputStream getStream() { + return this.stream; + } + + public String getString() throws IOException { + return new String(getBytes(), StandardCharsets.UTF_8); + } + + public byte[] getBytes() throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + int nRead; + byte[] data = new byte[16384]; + + while ((nRead = this.stream.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, nRead); + } + + buffer.flush(); + + return buffer.toByteArray(); + } + + public JSONObject getJson() throws IOException, JSONException { + String jsonString = getString(); + JSONObject json = new JSONObject(jsonString); + + return json; + } +} \ No newline at end of file diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/Op.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/Op.java new file mode 100644 index 000000000..9dc7700a3 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/Op.java @@ -0,0 +1,139 @@ +package com.vitorpamplona.quartz.ots.op; + +import android.util.Log; + +import com.vitorpamplona.quartz.ots.StreamDeserializationContext; +import com.vitorpamplona.quartz.ots.StreamSerializationContext; + +import com.vitorpamplona.quartz.ots.exceptions.DeserializationException; +import java.util.logging.Logger; + +/** + * Operations are the edges in the timestamp tree, with each operation taking a message and zero or more arguments to produce a result. + */ +public abstract class Op implements Comparable { + + /** + * Maximum length of an com.vitorpamplona.quartz.ots.op.Op result + *

+ * For a verifier, this limit is what limits the maximum amount of memory you + * need at any one time to verify a particular timestamp path; while verifying + * a particular commitment operation path previously calculated results can be + * discarded. + *

+ * Of course, if everything was a merkle tree you never need to append/prepend + * anything near 4KiB of data; 64 bytes would be plenty even with SHA512. The + * main need for this is compatibility with existing systems like Bitcoin + * timestamps and Certificate Transparency servers. While the pathological + * limits required by both are quite large - 1MB and 16MiB respectively - 4KiB + * is perfectly adequate in both cases for more reasonable usage. + *

+ * @see Op subclasses should set this limit even lower if doing so is appropriate + * for them. + */ + public static int _MAX_RESULT_LENGTH = 4096; + + /** + * Maximum length of the message an com.vitorpamplona.quartz.ots.op.Op can be applied too. + *

+ * Similar to the result length limit, this limit gives implementations a sane + * constraint to work with; the maximum result-length limit implicitly + * constrains maximum message length anyway. + *

+ * com.vitorpamplona.quartz.ots.op.Op subclasses should set this limit even lower if doing so is appropriate + * for them. + */ + public static int _MAX_MSG_LENGTH = 4096; + + public static byte _TAG = (byte) 0x00; + + public String _TAG_NAME() { + return ""; + } + + public byte _TAG() { + return Op._TAG; + } + + /** + * Deserialize operation from a buffer. + * + * @param ctx The stream deserialization context. + * @return The subclass Operation. + */ + public static Op deserialize(StreamDeserializationContext ctx) throws DeserializationException { + byte tag = ctx.readBytes(1)[0]; + + return Op.deserializeFromTag(ctx, tag); + } + + /** + * Deserialize operation from a buffer. + * + * @param ctx The stream deserialization context. + * @param tag The tag of the operation. + * @return The subclass Operation. + */ + public static Op deserializeFromTag(StreamDeserializationContext ctx, byte tag) + throws DeserializationException { + if (tag == OpAppend._TAG) { + return OpAppend.deserializeFromTag(ctx, tag); + } else if (tag == OpPrepend._TAG) { + return OpPrepend.deserializeFromTag(ctx, tag); + } else if (tag == OpSHA1._TAG) { + return OpSHA1.deserializeFromTag(ctx, tag); + } else if (tag == OpSHA256._TAG) { + return OpSHA256.deserializeFromTag(ctx, tag); + } else if (tag == OpRIPEMD160._TAG) { + return OpRIPEMD160.deserializeFromTag(ctx, tag); + } else if (tag == OpKECCAK256._TAG) { + return OpKECCAK256.deserializeFromTag(ctx, tag); + } else { + Log.e("OpenTimestamp", "Unknown operation tag: " + tag + " 0x" + String.format("%02x", tag)); + return null; // TODO: Is this OK? Won't it blow up later? Better to throw? + } + } + + /** + * Serialize operation. + * + * @param ctx The stream serialization context. + */ + public void serialize(StreamSerializationContext ctx) { + if (this._TAG() == 0x00) { + Log.e("OpenTimestamp", "No valid serialized Op"); + // TODO: Is it OK to just log and carry on? Won't it blow up later? Better to throw? + } + + ctx.writeByte(this._TAG()); + } + + /** + * Apply the operation to a message. + * Raises MsgValueError if the message value is invalid, such as it being + * too long, or it causing the result to be too long. + * + * @param msg The message. + * @return the msg after the operation has been applied + */ + public byte[] call(byte[] msg) { + if (msg.length > _MAX_MSG_LENGTH) { + Log.e("OpenTimestamp", "Error : Message too long;"); + return new byte[]{}; // TODO: Is this OK? Won't it blow up later? Better to throw? + } + + byte[] r = this.call(msg); + + if (r.length > _MAX_RESULT_LENGTH) { + Log.e("OpenTimestamp", "Error : Result too long;"); + // TODO: Is it OK to just log and carry on? Won't it blow up later? Better to throw? + } + + return r; + } + + @Override + public int compareTo(Op o) { + return this._TAG() - o._TAG(); + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpAppend.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpAppend.java new file mode 100644 index 000000000..0b81fe726 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpAppend.java @@ -0,0 +1,63 @@ +package com.vitorpamplona.quartz.ots.op; + +import com.vitorpamplona.quartz.ots.StreamDeserializationContext; +import com.vitorpamplona.quartz.ots.Utils; + +import com.vitorpamplona.quartz.ots.exceptions.DeserializationException; +import java.util.Arrays; +import java.util.logging.Logger; + +/** + * Append a suffix to a message. + * + * @see OpBinary + */ +public class OpAppend extends OpBinary { + byte[] arg; + + public static byte _TAG = (byte) 0xf0; + + @Override + public byte _TAG() { + return OpAppend._TAG; + } + + @Override + public String _TAG_NAME() { + return "append"; + } + + public OpAppend() { + super(); + this.arg = new byte[]{}; + } + + public OpAppend(byte[] arg_) { + super(arg_); + this.arg = arg_; + } + + @Override + public byte[] call(byte[] msg) { + return Utils.arraysConcat(msg, this.arg); + } + + public static Op deserializeFromTag(StreamDeserializationContext ctx, byte tag) + throws DeserializationException { + return OpBinary.deserializeFromTag(ctx, tag); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof OpAppend)) { + return false; + } + + return Arrays.equals(this.arg, ((OpAppend) obj).arg); + } + + @Override + public int hashCode() { + return _TAG ^ Arrays.hashCode(this.arg); + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpBinary.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpBinary.java new file mode 100644 index 000000000..217337e50 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpBinary.java @@ -0,0 +1,75 @@ +package com.vitorpamplona.quartz.ots.op; + +import android.util.Log; + +import com.vitorpamplona.quartz.encoders.Hex; +import com.vitorpamplona.quartz.ots.StreamDeserializationContext; +import com.vitorpamplona.quartz.ots.StreamSerializationContext; +import com.vitorpamplona.quartz.ots.Utils; + +import com.vitorpamplona.quartz.ots.exceptions.DeserializationException; +import java.util.Arrays; + +/** + * Operations that act on a message and a single argument. + * + * @see OpUnary + */ +public abstract class OpBinary extends Op implements Comparable { + + public byte[] arg; + + @Override + public String _TAG_NAME() { + return ""; + } + + public OpBinary() { + super(); + this.arg = new byte[]{}; + } + + public OpBinary(byte[] arg_) { + super(); + this.arg = arg_; + } + + public static Op deserializeFromTag(StreamDeserializationContext ctx, byte tag) + throws DeserializationException { + byte[] arg = ctx.readVarbytes(_MAX_RESULT_LENGTH, 1); + + if (tag == OpAppend._TAG) { + return new OpAppend(arg); + } else if (tag == OpPrepend._TAG) { + return new OpPrepend(arg); + } else { + Log.e("OpenTimestamp", "Unknown operation tag: " + tag + " 0x" + String.format("%02x", tag)); + return null; // TODO: Is this OK? Won't it blow up later? Better to throw? + } + } + + @Override + public void serialize(StreamSerializationContext ctx) { + super.serialize(ctx); + ctx.writeVarbytes(this.arg); + } + + @Override + public String toString() { + return this._TAG_NAME() + ' ' + Hex.encode(this.arg).toLowerCase(); + } + + @Override + public int compareTo(Op o) { + if (o instanceof OpBinary && this._TAG() == o._TAG()) { + return Utils.compare(this.arg, ((OpBinary) o).arg); + } + + return this._TAG() - o._TAG(); + } + + @Override + public int hashCode() { + return _TAG ^ Arrays.hashCode(this.arg); + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpCrypto.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpCrypto.java new file mode 100644 index 000000000..28e6b1aaa --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpCrypto.java @@ -0,0 +1,97 @@ +package com.vitorpamplona.quartz.ots.op; + +import android.util.Log; + +import com.vitorpamplona.quartz.ots.StreamDeserializationContext; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Cryptographic transformations. + * These transformations have the unique property that for any length message, + * the size of the result they return is fixed. Additionally, they're the only + * type of operation that can be applied directly to a stream. + * + * @see OpUnary + */ +public class OpCrypto extends OpUnary { + + public String _TAG_NAME = ""; + + public String _HASHLIB_NAME() { + return ""; + } + + public int _DIGEST_LENGTH() { + return 0; + } + + OpCrypto() { + super(); + } + + public static Op deserializeFromTag(StreamDeserializationContext ctx, byte tag) { + return OpUnary.deserializeFromTag(ctx, tag); + } + + @Override + public byte[] call(byte[] msg) { + // For Sha1 & Sha256 use java.security.MessageDigest library + try { + MessageDigest digest = MessageDigest.getInstance(this._HASHLIB_NAME()); + byte[] hash = digest.digest(msg); + + return hash; + } catch (NoSuchAlgorithmException e) { + Log.e("OpenTimestamp", "NoSuchAlgorithmException"); + e.printStackTrace(); + + return new byte[]{}; // TODO: Is this OK? Won't it blow up later? Better to throw? + } + } + + public byte[] hashFd(StreamDeserializationContext ctx) throws NoSuchAlgorithmException { + MessageDigest digest = MessageDigest.getInstance(this._HASHLIB_NAME()); + byte[] chunk = ctx.read(1048576); + + while (chunk != null && chunk.length > 0) { + digest.update(chunk); + chunk = ctx.read(1048576); + } + + byte[] hash = digest.digest(); + + return hash; + } + + public byte[] hashFd(File file) throws IOException, NoSuchAlgorithmException { + return hashFd(new FileInputStream(file)); + } + + public byte[] hashFd(byte[] bytes) throws IOException, NoSuchAlgorithmException { + StreamDeserializationContext ctx = new StreamDeserializationContext(bytes); + + return hashFd(ctx); + } + + public byte[] hashFd(InputStream inputStream) throws IOException, NoSuchAlgorithmException { + MessageDigest digest = MessageDigest.getInstance(this._HASHLIB_NAME()); + byte[] chunk = new byte[1048576]; + int count = inputStream.read(chunk, 0, 1048576); + + while (count >= 0) { + digest.update(chunk, 0, count); + count = inputStream.read(chunk, 0, 1048576); + } + + inputStream.close(); + byte[] hash = digest.digest(); + + return hash; + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpKECCAK256.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpKECCAK256.java new file mode 100644 index 000000000..c77582759 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpKECCAK256.java @@ -0,0 +1,62 @@ +package com.vitorpamplona.quartz.ots.op; + +import com.vitorpamplona.quartz.ots.StreamDeserializationContext; +import com.vitorpamplona.quartz.ots.Utils; +import com.vitorpamplona.quartz.ots.crypto.KeccakDigest; + +import java.util.logging.Logger; + +/** + * Cryptographic Keccak256 operation. + * Cryptographic operation tag numbers taken from RFC4880, although it's not + * guaranteed that they'll continue to match that RFC in the future. + * + * @see OpCrypto + */ +public class OpKECCAK256 extends OpCrypto { + private KeccakDigest digest = new KeccakDigest(256); + + public static byte _TAG = (byte) 103; + + @Override + public byte _TAG() { + return OpKECCAK256._TAG; + } + + @Override + public String _TAG_NAME() { + return "keccak256"; + } + + @Override + public String _HASHLIB_NAME() { + return "keccak256"; + } + + @Override + public int _DIGEST_LENGTH() { + return digest.getDigestSize(); + } + + public OpKECCAK256() { + super(); + } + + @Override + public byte[] call(byte[] msg) { + digest.update(msg, 0, msg.length); + byte[] hash = new byte[digest.getDigestSize()]; + digest.doFinal(hash, 0); + + return hash; + } + + public static Op deserializeFromTag(StreamDeserializationContext ctx, byte tag) { + return OpCrypto.deserializeFromTag(ctx, tag); + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof OpKECCAK256); + } +} \ No newline at end of file diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpPrepend.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpPrepend.java new file mode 100644 index 000000000..89c3d89e5 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpPrepend.java @@ -0,0 +1,64 @@ +package com.vitorpamplona.quartz.ots.op; + +import com.vitorpamplona.quartz.ots.StreamDeserializationContext; +import com.vitorpamplona.quartz.ots.Utils; + +import com.vitorpamplona.quartz.ots.exceptions.DeserializationException; +import java.util.Arrays; +import java.util.logging.Logger; + +/** + * Prepend a prefix to a message. + * + * @see OpBinary + */ +public class OpPrepend extends OpBinary { + + byte[] arg; + + public static byte _TAG = (byte) 0xf1; + + @Override + public byte _TAG() { + return OpPrepend._TAG; + } + + @Override + public String _TAG_NAME() { + return "prepend"; + } + + public OpPrepend() { + super(); + this.arg = new byte[]{}; + } + + public OpPrepend(byte[] arg_) { + super(arg_); + this.arg = arg_; + } + + @Override + public byte[] call(byte[] msg) { + return Utils.arraysConcat(this.arg, msg); + } + + public static Op deserializeFromTag(StreamDeserializationContext ctx, byte tag) + throws DeserializationException { + return OpBinary.deserializeFromTag(ctx, tag); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof OpPrepend)) { + return false; + } + + return Arrays.equals(this.arg, ((OpPrepend) obj).arg); + } + + @Override + public int hashCode() { + return _TAG ^ Arrays.hashCode(this.arg); + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpRIPEMD160.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpRIPEMD160.java new file mode 100644 index 000000000..cf89834fd --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpRIPEMD160.java @@ -0,0 +1,67 @@ +package com.vitorpamplona.quartz.ots.op; + +import com.vitorpamplona.quartz.ots.StreamDeserializationContext; +import com.vitorpamplona.quartz.ots.Utils; +import com.vitorpamplona.quartz.ots.crypto.RIPEMD160Digest; + +import java.util.logging.Logger; + +/** + * Cryptographic RIPEMD160 operation. + * Cryptographic operation tag numbers taken from RFC4880, although it's not + * guaranteed that they'll continue to match that RFC in the future. + * + * @see OpCrypto + */ +public class OpRIPEMD160 extends OpCrypto { + + public static byte _TAG = 0x03; + + @Override + public byte _TAG() { + return OpRIPEMD160._TAG; + } + + @Override + public String _TAG_NAME() { + return "ripemd160"; + } + + @Override + public String _HASHLIB_NAME() { + return "ripemd160"; + } + + @Override + public int _DIGEST_LENGTH() { + return 20; + } + + public OpRIPEMD160() { + super(); + } + + @Override + public byte[] call(byte[] msg) { + RIPEMD160Digest digest = new RIPEMD160Digest(); + digest.update(msg, 0, msg.length); + byte[] hash = new byte[digest.getDigestSize()]; + digest.doFinal(hash, 0); + + return hash; + } + + public static Op deserializeFromTag(StreamDeserializationContext ctx, byte tag) { + return OpCrypto.deserializeFromTag(ctx, tag); + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof OpRIPEMD160); + } + + @Override + public int hashCode() { + return _TAG; + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpSHA1.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpSHA1.java new file mode 100644 index 000000000..0a8080090 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpSHA1.java @@ -0,0 +1,68 @@ +package com.vitorpamplona.quartz.ots.op; + +import com.vitorpamplona.quartz.ots.StreamDeserializationContext; +import com.vitorpamplona.quartz.ots.Utils; + +import java.util.logging.Logger; + +/** + * Cryptographic SHA1 operation. + * Cryptographic operation tag numbers taken from RFC4880, although it's not + * guaranteed that they'll continue to match that RFC in the future. + * Remember that for timestamping, hash algorithms with collision attacks + * *are* secure! We've still proven that both messages existed prior to some + * point in time - the fact that they both have the same hash digest doesn't + * change that. + * Heck, even md5 is still secure enough for timestamping... but that's + * pushing our luck... + * + * @see OpCrypto + */ +public class OpSHA1 extends OpCrypto { + + + public static byte _TAG = 0x02; + + @Override + public byte _TAG() { + return OpSHA1._TAG; + } + + @Override + public String _TAG_NAME() { + return "sha1"; + } + + @Override + public String _HASHLIB_NAME() { + return "SHA-1"; + } + + @Override + public int _DIGEST_LENGTH() { + return 20; + } + + public OpSHA1() { + super(); + } + + @Override + public byte[] call(byte[] msg) { + return super.call(msg); + } + + public static Op deserializeFromTag(StreamDeserializationContext ctx, byte tag) { + return OpCrypto.deserializeFromTag(ctx, tag); + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof OpSHA1); + } + + @Override + public int hashCode() { + return _TAG; + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpSHA256.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpSHA256.java new file mode 100644 index 000000000..892713a46 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpSHA256.java @@ -0,0 +1,62 @@ +package com.vitorpamplona.quartz.ots.op; + +import com.vitorpamplona.quartz.ots.StreamDeserializationContext; +import com.vitorpamplona.quartz.ots.Utils; + +import java.util.logging.Logger; + +/** + * Cryptographic SHA256 operation. + * Cryptographic operation tag numbers taken from RFC4880, although it's not + * guaranteed that they'll continue to match that RFC in the future. + * + * @see OpCrypto + */ +public class OpSHA256 extends OpCrypto { + + + public static byte _TAG = 0x08; + + @Override + public byte _TAG() { + return OpSHA256._TAG; + } + + @Override + public String _TAG_NAME() { + return "sha256"; + } + + @Override + public String _HASHLIB_NAME() { + return "SHA-256"; + } + + @Override + public int _DIGEST_LENGTH() { + return 32; + } + + public OpSHA256() { + super(); + } + + @Override + public byte[] call(byte[] msg) { + return super.call(msg); + } + + public static Op deserializeFromTag(StreamDeserializationContext ctx, byte tag) { + return OpCrypto.deserializeFromTag(ctx, tag); + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof OpSHA256); + } + + @Override + public int hashCode() { + return _TAG; + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpUnary.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpUnary.java new file mode 100644 index 000000000..d1b5894e1 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/op/OpUnary.java @@ -0,0 +1,46 @@ +package com.vitorpamplona.quartz.ots.op; + +import android.util.Log; + +import com.vitorpamplona.quartz.ots.StreamDeserializationContext; +import com.vitorpamplona.quartz.ots.Utils; + +import java.util.logging.Logger; + +/** + * Operations that act on a single message. + * + * @see Op + */ +public abstract class OpUnary extends Op { + + @Override + public String _TAG_NAME() { + return ""; + } + + public OpUnary() { + super(); + } + + public static Op deserializeFromTag(StreamDeserializationContext ctx, byte tag) { + if (tag == OpSHA1._TAG) { + return new OpSHA1(); + } else if (tag == OpSHA256._TAG) { + return new OpSHA256(); + } else if (tag == OpRIPEMD160._TAG) { + return new OpRIPEMD160(); + } else if (tag == OpKECCAK256._TAG) { + return new OpKECCAK256(); + } else { + Log.e("OpenTimestamp", "Unknown operation tag: " + tag); + + return null; // TODO: Is this OK? Won't it blow up later? Better to throw? + } + } + + @Override + public String toString() { + return this._TAG_NAME(); + } +} From 9eefb65047cc34c5e571f90a154f1ee62818037d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 25 Feb 2024 15:43:01 -0500 Subject: [PATCH 156/176] Generalizes the interface for block explorers in OTS --- .../vitorpamplona/quartz/events/OtsEvent.kt | 7 ++- .../quartz/ots/BitcoinExplorer.java | 21 +++++++ ...{Esplora.java => BlockstreamExplorer.java} | 24 ++++---- .../quartz/ots/OpenTimestamps.java | 58 ++++++++++--------- 4 files changed, 68 insertions(+), 42 deletions(-) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/BitcoinExplorer.java rename quartz/src/main/java/com/vitorpamplona/quartz/ots/{Esplora.java => BlockstreamExplorer.java} (74%) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/OtsEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/OtsEvent.kt index a88bc406d..988ec5836 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/OtsEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/OtsEvent.kt @@ -24,6 +24,7 @@ import android.util.Log import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.hexToByteArray +import com.vitorpamplona.quartz.ots.BlockstreamExplorer import com.vitorpamplona.quartz.ots.DetachedTimestampFile import com.vitorpamplona.quartz.ots.Hash import com.vitorpamplona.quartz.ots.OpenTimestamps @@ -57,7 +58,7 @@ class OtsEvent( val detachedOts = DetachedTimestampFile.deserialize(otsByteArray()) - val result = OpenTimestamps.verify(detachedOts, digest) + val result = OpenTimestamps(BlockstreamExplorer()).verify(detachedOts, digest) if (result == null || result.isEmpty()) { return null } else { @@ -72,7 +73,7 @@ class OtsEvent( fun info(): String { val detachedOts = DetachedTimestampFile.deserialize(otsByteArray()) - return OpenTimestamps.info(detachedOts) + return OpenTimestamps(BlockstreamExplorer()).info(detachedOts) } companion object { @@ -82,7 +83,7 @@ class OtsEvent( fun stamp(eventId: HexKey): ByteArray { val hash = Hash(eventId.hexToByteArray(), OpSHA256._TAG) val file = DetachedTimestampFile.from(hash) - val timestamp = OpenTimestamps.stamp(file) + val timestamp = OpenTimestamps(BlockstreamExplorer()).stamp(file) val detachedToSerialize = DetachedTimestampFile(hash.getOp(), timestamp) return detachedToSerialize.serialize() } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/BitcoinExplorer.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/BitcoinExplorer.java new file mode 100644 index 000000000..5a5bbd811 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/BitcoinExplorer.java @@ -0,0 +1,21 @@ +package com.vitorpamplona.quartz.ots; + +public interface BitcoinExplorer { + /** + * Retrieve the block information from the block hash. + * + * @param hash Hash of the block. + * @return the blockheader of the hash + * @throws Exception desc + */ + public BlockHeader block(final String hash) throws Exception; + + /** + * Retrieve the block hash from the block height. + * + * @param height Height of the block. + * @return the hash of the block at height height + * @throws Exception desc + */ + public String blockHash(final Integer height) throws Exception; +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/Esplora.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/BlockstreamExplorer.java similarity index 74% rename from quartz/src/main/java/com/vitorpamplona/quartz/ots/Esplora.java rename to quartz/src/main/java/com/vitorpamplona/quartz/ots/BlockstreamExplorer.java index f2ca8e6e6..01c84254f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/ots/Esplora.java +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/BlockstreamExplorer.java @@ -1,17 +1,15 @@ package com.vitorpamplona.quartz.ots; +import android.util.Log; + +import com.fasterxml.jackson.databind.JsonNode; import com.vitorpamplona.quartz.ots.http.Request; import com.vitorpamplona.quartz.ots.http.Response; -import org.json.JSONObject; - import java.net.URL; import java.util.concurrent.*; -import java.util.logging.Logger; -public class Esplora { - - private static final Logger log = Utils.getLogger(Esplora.class.getName()); +public class BlockstreamExplorer implements BitcoinExplorer { private static final String esploraUrl = "https://blockstream.info/api"; /** @@ -21,7 +19,7 @@ public class Esplora { * @return the blockheader of the hash * @throws Exception desc */ - public static BlockHeader block(final String hash) throws Exception { + public BlockHeader block(final String hash) throws Exception { final URL url = new URL(esploraUrl + "/block/" + hash); final Request task = new Request(url); final ExecutorService executor = Executors.newSingleThreadExecutor(); @@ -31,14 +29,14 @@ public class Esplora { if (!take.isOk()) throw new Exception(); - final JSONObject jsonObject = take.getJson(); - final String merkleroot = jsonObject.getString("merkle_root"); - final String time = String.valueOf(jsonObject.getInt("timestamp")); + final JsonNode jsonObject = take.getJson(); + final String merkleroot = jsonObject.get("merkle_root").asText(); + final String time = String.valueOf(jsonObject.get("timestamp").asInt()); final BlockHeader blockHeader = new BlockHeader(); blockHeader.setMerkleroot(merkleroot); blockHeader.setTime(time); blockHeader.setBlockHash(hash); - log.info(take.getFromUrl() + " " + blockHeader); + Log.i("BlockstreamExplorer", take.getFromUrl() + " " + blockHeader); return blockHeader; //log.warning("Cannot parse merkleroot from body: " + jsonObject + ": " + e.getMessage()); } @@ -50,7 +48,7 @@ public class Esplora { * @return the hash of the block at height height * @throws Exception desc */ - public static String blockHash(final Integer height) throws Exception { + public String blockHash(final Integer height) throws Exception { final URL url = new URL(esploraUrl + "/block-height/" + height); final Request task = new Request(url); final ExecutorService executor = Executors.newSingleThreadExecutor(); @@ -60,7 +58,7 @@ public class Esplora { if (!take.isOk()) throw new Exception(); final String blockHash = take.getString(); - log.info(take.getFromUrl() + " " + blockHash); + Log.i("BlockstreamExplorer", take.getFromUrl() + " " + blockHash); return blockHash; } } \ No newline at end of file diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/OpenTimestamps.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/OpenTimestamps.java index f2ba09282..648885639 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/ots/OpenTimestamps.java +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/OpenTimestamps.java @@ -32,13 +32,19 @@ import java.util.concurrent.Executors; */ public class OpenTimestamps { + BitcoinExplorer explorer; + + public OpenTimestamps(BitcoinExplorer explorer) { + this.explorer = explorer; + } + /** * Show information on a detached timestamp. * * @param detachedTimestampFile The DetachedTimestampFile ots. * @return the string representation of the timestamp. */ - public static String info(DetachedTimestampFile detachedTimestampFile) { + public String info(DetachedTimestampFile detachedTimestampFile) { return info(detachedTimestampFile, false); } @@ -49,7 +55,7 @@ public class OpenTimestamps { * @param verbose Show verbose output. * @return the string representation of the timestamp. */ - public static String info(DetachedTimestampFile detachedTimestampFile, boolean verbose) { + public String info(DetachedTimestampFile detachedTimestampFile, boolean verbose) { if (detachedTimestampFile == null) { return "No ots file"; } @@ -68,7 +74,7 @@ public class OpenTimestamps { * @param timestamp The timestamp buffered. * @return the string representation of the timestamp. */ - public static String info(Timestamp timestamp) { + public String info(Timestamp timestamp) { if (timestamp == null) { return "No timestamp"; } @@ -86,8 +92,8 @@ public class OpenTimestamps { * @return The plain array buffer of stamped. * @throws IOException if fileTimestamp is not valid, or the stamp procedure fails. */ - public static Timestamp stamp(DetachedTimestampFile fileTimestamp) throws IOException { - return OpenTimestamps.stamp(fileTimestamp, null, 0); + public Timestamp stamp(DetachedTimestampFile fileTimestamp) throws IOException { + return stamp(fileTimestamp, null, 0); } /** @@ -99,11 +105,11 @@ public class OpenTimestamps { * @return The plain array buffer of stamped. * @throws IOException if fileTimestamp is not valid, or the stamp procedure fails. */ - public static Timestamp stamp(DetachedTimestampFile fileTimestamp, List calendarsUrl, Integer m) throws IOException { + public Timestamp stamp(DetachedTimestampFile fileTimestamp, List calendarsUrl, Integer m) throws IOException { List fileTimestamps = new ArrayList(); fileTimestamps.add(fileTimestamp); - return OpenTimestamps.stamp(fileTimestamps, calendarsUrl, m); + return stamp(fileTimestamps, calendarsUrl, m); } /** @@ -115,7 +121,7 @@ public class OpenTimestamps { * @return The plain array buffer of stamped. * @throws IOException if fileTimestamp is not valid, or the stamp procedure fails. */ - public static Timestamp stamp(List fileTimestamps, List calendarsUrl, Integer m) throws IOException { + public Timestamp stamp(List fileTimestamps, List calendarsUrl, Integer m) throws IOException { // Parse parameters if (fileTimestamps == null || fileTimestamps.size() == 0) { throw new IOException(); @@ -144,14 +150,14 @@ public class OpenTimestamps { } // Build markle tree - Timestamp merkleTip = OpenTimestamps.makeMerkleTree(fileTimestamps); + Timestamp merkleTip = makeMerkleTree(fileTimestamps); if (merkleTip == null) { throw new IOException(); } // Stamping - Timestamp resultTimestamp = OpenTimestamps.create(merkleTip, calendarsUrl, m); + Timestamp resultTimestamp = create(merkleTip, calendarsUrl, m); if (resultTimestamp == null) { throw new IOException(); @@ -173,7 +179,7 @@ public class OpenTimestamps { * @param m Number of calendars to use. * @return The created timestamp. */ - private static Timestamp create(Timestamp timestamp, List calendarUrls, Integer m) { + private Timestamp create(Timestamp timestamp, List calendarUrls, Integer m) { int capacity = calendarUrls.size(); ExecutorService executor = Executors.newFixedThreadPool(4); ArrayBlockingQueue> queue = new ArrayBlockingQueue<>(capacity); @@ -227,7 +233,7 @@ public class OpenTimestamps { * @param fileTimestamps The list of DetachedTimestampFile. * @return merkle tip timestamp. */ - public static Timestamp makeMerkleTree(List fileTimestamps) { + public Timestamp makeMerkleTree(List fileTimestamps) { List merkleRoots = new ArrayList<>(); for (DetachedTimestampFile fileTimestamp : fileTimestamps) { @@ -259,14 +265,14 @@ public class OpenTimestamps { * @throws Exception if the verification procedure fails. */ - public static HashMap verify(DetachedTimestampFile ots, byte[] diggest) throws Exception { + public HashMap verify(DetachedTimestampFile ots, byte[] diggest) throws Exception { if (!Arrays.equals(ots.fileDigest(), diggest)) { Log.e("OpenTimestamp", "Expected digest " + Hex.encode(ots.fileDigest()).toLowerCase()); Log.e("OpenTimestamp", "File does not match original!"); throw new Exception("File does not match original!"); } - return OpenTimestamps.verify(ots.timestamp); + return verify(ots.timestamp); } /** @@ -276,7 +282,7 @@ public class OpenTimestamps { * @return HashMap of block heights and timestamps indexed by chain: timestamp in seconds from 1 January 1970. * @throws Exception if the verification procedure fails. */ - public static HashMap verify(Timestamp timestamp) throws Exception { + public HashMap verify(Timestamp timestamp) throws Exception { HashMap verifyResults = new HashMap<>(); for (Map.Entry item : timestamp.allAttestations().entrySet()) { @@ -339,13 +345,13 @@ public class OpenTimestamps { * @throws VerificationException if it doesn't check the merkle root of the block. * @throws Exception if the verification procedure fails. */ - public static Long verify(BitcoinBlockHeaderAttestation attestation, byte[] msg) throws VerificationException, Exception { + public Long verify(BitcoinBlockHeaderAttestation attestation, byte[] msg) throws VerificationException, Exception { Integer height = attestation.getHeight(); BlockHeader blockInfo; try { - String blockHash = Esplora.blockHash(height); - blockInfo = Esplora.block(blockHash); + String blockHash = explorer.blockHash(height); + blockInfo = explorer.block(blockHash); Log.i("OpenTimestamps", "Lite-client verification, assuming block " + blockHash + " is valid"); } catch (Exception e2) { e2.printStackTrace(); @@ -364,13 +370,13 @@ public class OpenTimestamps { * @throws VerificationException if it doesn't check the merkle root of the block. * @throws Exception if the verification procedure fails. */ - public static Long verify(LitecoinBlockHeaderAttestation attestation, byte[] msg) throws VerificationException, Exception { + public Long verify(LitecoinBlockHeaderAttestation attestation, byte[] msg) throws VerificationException, Exception { Integer height = attestation.getHeight(); BlockHeader blockInfo; try { - String blockHash = Esplora.blockHash(height); - blockInfo = Esplora.block(blockHash); + String blockHash = explorer.blockHash(height); + blockInfo = explorer.block(blockHash); Log.i("OpenTimestamps", "Lite-client verification, assuming block " + blockHash + " is valid"); } catch (Exception e2) { e2.printStackTrace(); @@ -387,9 +393,9 @@ public class OpenTimestamps { * @return a boolean representing if the timestamp has changed. * @throws Exception if the upgrading procedure fails. */ - public static boolean upgrade(DetachedTimestampFile detachedTimestamp) throws Exception { + public boolean upgrade(DetachedTimestampFile detachedTimestamp) throws Exception { // Upgrade timestamp - boolean changed = OpenTimestamps.upgrade(detachedTimestamp.timestamp); + boolean changed = upgrade(detachedTimestamp.timestamp); return changed; } @@ -401,7 +407,7 @@ public class OpenTimestamps { * @return a boolean representing if the timestamp has changed. * @throws Exception if the upgrading procedure fails. */ - public static boolean upgrade(Timestamp timestamp) throws Exception { + public boolean upgrade(Timestamp timestamp) throws Exception { // Check remote calendars for upgrades. // This time we only check PendingAttestations - we can't be as agressive. @@ -417,7 +423,7 @@ public class OpenTimestamps { try { Calendar calendar = new Calendar(calendarUrl); - Timestamp upgradedStamp = OpenTimestamps.upgrade(subStamp, calendar, commitment, existingAttestations); + Timestamp upgradedStamp = upgrade(subStamp, calendar, commitment, existingAttestations); try { subStamp.merge(upgradedStamp); @@ -436,7 +442,7 @@ public class OpenTimestamps { return upgraded; } - private static Timestamp upgrade(Timestamp subStamp, Calendar calendar, byte[] commitment, Set existingAttestations) throws Exception { + private Timestamp upgrade(Timestamp subStamp, Calendar calendar, byte[] commitment, Set existingAttestations) throws Exception { Timestamp upgradedStamp; try { From 1c0854eeaa450e962027bcc9442ea193cc4df372 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 25 Feb 2024 15:43:18 -0500 Subject: [PATCH 157/176] Moves from org.json to jackson in OTS replies --- .../com/vitorpamplona/quartz/ots/http/Response.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/http/Response.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/http/Response.java index 836b61462..adb378f38 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/ots/http/Response.java +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/http/Response.java @@ -1,7 +1,7 @@ package com.vitorpamplona.quartz.ots.http; -import org.json.JSONException; -import org.json.JSONObject; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.json.JsonMapper; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -69,10 +69,9 @@ public class Response { return buffer.toByteArray(); } - public JSONObject getJson() throws IOException, JSONException { + public JsonNode getJson() throws IOException { String jsonString = getString(); - JSONObject json = new JSONObject(jsonString); - - return json; + JsonMapper builder = JsonMapper.builder().build(); + return builder.readTree(jsonString); } } \ No newline at end of file From d6fd407632c24d0680acdc6a205664f7e3ed343d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 25 Feb 2024 15:43:42 -0500 Subject: [PATCH 158/176] Moves external signer from org.json to jackson parser --- .../quartz/signers/ExternalSignerLauncher.kt | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/signers/ExternalSignerLauncher.kt b/quartz/src/main/java/com/vitorpamplona/quartz/signers/ExternalSignerLauncher.kt index 951494889..bff1e2eb8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/signers/ExternalSignerLauncher.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/signers/ExternalSignerLauncher.kt @@ -34,9 +34,9 @@ import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.EventInterface import com.vitorpamplona.quartz.events.LnZapRequestEvent -import org.json.JSONArray enum class SignerType { SIGN_EVENT, @@ -86,15 +86,13 @@ class Result( fun fromJson(json: String): Result = mapper.readValue(json, Result::class.java) + /** + * Parses the json with a string of events to an Array of Event objects. + */ fun fromJsonArray(json: String): Array { - val result: MutableList = mutableListOf() - val array = JSONArray(json) - (0 until array.length()).forEach { - val resultJson = array.getJSONObject(it) - val localResult = fromJson(resultJson.toString()) - result.add(localResult) - } - return result.toTypedArray() + return Event.mapper.readTree(json).map { + fromJson(it.asText()) + }.toTypedArray() } } } From 880915e702c2053104d4bd825d7b7bc6ca408a43 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 25 Feb 2024 15:43:59 -0500 Subject: [PATCH 159/176] Better explains why the fork of javaOTS was needed. --- quartz/src/main/java/com/vitorpamplona/quartz/ots/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/README.md b/quartz/src/main/java/com/vitorpamplona/quartz/ots/README.md index cc839582c..5b2bc9521 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/ots/README.md +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/README.md @@ -1,6 +1,10 @@ This code came from https://github.com/opentimestamps/java-opentimestamps -And includes modifications to avoid dependencies that do not work on Android +And includes modifications to +1 - Avoid dependencies that do not work on Android +2 - Move from org.json to jackson +3 - Move from basic Url connection to OkHttp (and obey Tor settings) +4 - Generalize the use of Blockstream as a Bitcoin Block explorer. — From 6a7d33e58f1086341ec0323443f7d39cccdd8a56 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 25 Feb 2024 17:04:51 -0500 Subject: [PATCH 160/176] - Generalizing OTS Calendar access - Developing OTS access points with OkHttp --- .../vitorpamplona/amethyst/OkHttpOtsTest.kt | 93 ++++++++++++ .../com/vitorpamplona/amethyst/Amethyst.kt | 6 + .../service/ots/OkHttpBlockstreamExplorer.kt | 103 ++++++++++++++ .../amethyst/service/ots/OkHttpCalendar.kt | 133 ++++++++++++++++++ .../service/ots/OkHttpCalendarAsyncSubmit.kt | 73 ++++++++++ .../service/ots/OkHttpCalendarBuilder.kt | 38 +++++ .../vitorpamplona/quartz/events/OtsEvent.kt | 9 +- .../vitorpamplona/quartz/ots/Calendar.java | 4 +- .../quartz/ots/CalendarAsyncSubmit.java | 3 +- .../quartz/ots/CalendarBuilder.java | 6 + .../quartz/ots/CalendarPureJavaBuilder.java | 10 ++ .../vitorpamplona/quartz/ots/ICalendar.java | 13 ++ .../quartz/ots/ICalendarAsyncSubmit.java | 9 ++ .../quartz/ots/OpenTimestamps.java | 5 +- .../quartz/ots/http/Request.java | 25 ---- 15 files changed, 498 insertions(+), 32 deletions(-) create mode 100644 app/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpBlockstreamExplorer.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendar.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendarAsyncSubmit.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendarBuilder.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/CalendarBuilder.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/CalendarPureJavaBuilder.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/ICalendar.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/ots/ICalendarAsyncSubmit.java diff --git a/app/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt b/app/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt new file mode 100644 index 000000000..7dd5626fe --- /dev/null +++ b/app/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2024 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 + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.amethyst.service.ots.OkHttpBlockstreamExplorer +import com.vitorpamplona.amethyst.service.ots.OkHttpCalendarBuilder +import com.vitorpamplona.quartz.crypto.KeyPair +import com.vitorpamplona.quartz.events.Event +import com.vitorpamplona.quartz.events.OtsEvent +import com.vitorpamplona.quartz.ots.OpenTimestamps +import com.vitorpamplona.quartz.signers.NostrSignerInternal +import junit.framework.TestCase.assertEquals +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +@RunWith(AndroidJUnit4::class) +class OkHttpOtsTest { + val otsEvent = "{\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqCiiW0FlsU9lqK5f1A+cL6CGJ1Ah4V/A1yNJY/stUE3wECJz6ng/QxU5Z6xwaMx97qkI//AQqJv8bEMrGTplGWRv5qm4DgjxIHkcQqzpL0Fjr9VBAAijDe0IsQYpOhw1SIjZIgQa6i16CPEEZck7CvAIxR0AloJzCZoAg9/jDS75DI4uLWh0dHBzOi8vYWxpY2UuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wEOwPtjIkKI1hmtv9t1kuxZcI8QRlyTsK8Ahl0wrCSggZzgCD3+MNLvkMjiwraHR0cHM6Ly9ib2IuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wEE1dVGa8JCuf2ek0c5ybDKII8SCBoVz8Sal45Kd1O8STWIGJTcl5JPtAZBZitqk3BE9MqAjxBGXJOwrwCHoGVgAZi9q9AIPf4w0u+QyOKShodHRwczovL2Zpbm5leS5jYWxlbmRhci5ldGVybml0eXdhbGwuY29t8BAFZFXFYg7DJJ0OzjmJ0FKWCPEEZck7CvAI0M49IcBR5bf/AIPf4w0u+QyOIyJodHRwczovL2J0Yy5jYWxlbmRhci5jYXRhbGxheHkuY29tCPEgoE3IfYTmxxo4W/x/QYp/NGX6Wu93gSQkwbpjpOhZORcI8SDWLLurVQaXHdUuwivCfTfuxYCaq+AzypSGqLDAVocrEgjwIBfgjta16y13Gp4etQOCa9YiKEcM+/9AieG/vZolr3IDCPAgMR2zFCb384CEi8tVuI2fHgLT3I9zpe7oqJTzCcEqxWEI8SAJSdgeeosr7IxdOt8r7f0ipWc8FI6GAhgep8zSRgWikAjxIGxYmtCsC79Tx4z4YsT1WuMo+ycMkwhGQsQltF597cchCPAgCLrBf9vR1Aex6yY+vSkXAvLjMKdMqM/a1g8zNPwLeJcI8SBDCbTk4CczTuiIyZeUyYRVh31BZdjaSd2nU/pBQxu+6QjwIOwqE9/WqGC6CHH0i+tr7edvYX5PstSDf08KmnMqsqCoCPEgQIdEBg358vfek3Qjfyrgl51iCU6WUWmThsGLPDTcB0QI8SAX4dp64iI8pBx+zBqAQwUN6XgZ1cEfT8+2vha/9I1vzwjxWQEAAAAB8YJxvxJJth8OnxIV6UOXveIZAcJPTHcAkWgnucpuYqYAAAAAAP3///8CRhoMAAAAAAAWABTUUZK82uAvbU3vVyaaPIOddZBicwAAAAAAAAAAImog8ARCqgwACAjwIB9TcMDLhzgeS1Uw647lNvCfWECkkUvfrrOe6nay0sGdCAjwICYfs90sbPggoMICyOHGYbmOzop2L9mlnh4xqiBLY7yPCAjxINDiVWOBHnRmGJleQdB9myvJAJbNJ9kciZlTOkgJy89mCAjwIHfxqDLdwycj1Vtyth2CaSDdLQwiey9oV6Xov4stLpWNCAjxIGurJYpKJnKp9+y7MAdC+gXgHOiAu5P3RRUFW9l5hGaCCAjxICXqs9hdY0QMP/MNeqlt6s7xaIYtEXZ1CLvou5gaZNEICAjxIHdYxVeI76NXbT2zHcv6lw+v819Ooib7KWxc1GAsiX2fCAjwICPWdi3uBXOlIdmYi+V9C7wAqLyGE4DMoHD+GtvLizqiCAjwIOF2vENtWN5okEMMS+JSf1SGTY9yYP9j0JjXLbC1s+N1CAjxINWsgCtsPxhRNbe372k8/20WDbiL9e8934hGF256DvRECAjwII3mi+Li06j10ORxg0dYMkcsyGb115Jiqq1YEV3K/u+aCAgABYiWDXPXGQEDw9Qy\",\"created_at\":1707690688,\"id\":\"759f9da5846e936fab06766a524b36ba71c03bbc69ad0944fb8ee4bb1f3dd705\",\"kind\":1040,\"pubkey\":\"82fbb08c8ef45c4d71c88368d0ae805bc62fb92f166ab04a0b7a0c83d8cbc29a\",\"sig\":\"07c7896c8cbb97b5d7483097590c9d31b73f35c1ad9e752002bb5c1776cbd852e1d32704333d6930c9bc3e40f8b899a1f2e9f91cc3bf797d86acdecba7792576\",\"tags\":[[\"e\",\"a828a25b4165b14f65a8ae5fd40f9c2fa086275021e15fc0d7234963fb2d504d\"],[\"p\",\"595ca8eaace5899cb6ab7e2542bfc972136376f2eabc09287f1857eb8f167e53\"],[\"alt\",\"NIP-03 time stamp\"]]}" + val otsEvent2 = "{\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqGNPU2jhd4no+zg2ytDkuf5PIoivr8KHI8BL68aKGNbwENyCNtiEN98IzIZgEu3cl6YI//AQ6TkSRd3BTGhDHCK1KkJc+AjxIAHaizG++NNL3Vm13BJrIhT7Br6tEYpb0TVRGaadgiUMCPAgOSDREH9v1Y50UHu79LfC4Lcd9WklQJzRQpw+Unb/pyII8QRltDD58AgqrxfAVrLw7QCD3+MNLvkMji4taHR0cHM6Ly9hbGljZS5idGMuY2FsZW5kYXIub3BlbnRpbWVzdGFtcHMub3Jn//AQQMq/CLpGwY60nmddPS7OVgjxIDKxqd9nl+Mej41vP52Wd7gv7004r3n1rFGDObS8icRvCPAgH9TB/kwvXJEEw+h9Ce6fLaI3MORjtTEge0GbAefT6W4I8QRltDD58AhRcoU3gAo/swCD3+MNLvkMjiwraHR0cHM6Ly9ib2IuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wECWtWsKo0uvSr8BYonjs3DEI8CBlsh2ng1Spl0K4oStYElGuMJsjd2uo5nXB+apo5A7ipwjxIM8oxynBwNA+QS/X7Ebtl1kyhFgfoOQioASNfCBzZ4gaCPEEZbQw+fAId6Yd5cw5gioAg9/jDS75DI4pKGh0dHBzOi8vZmlubmV5LmNhbGVuZGFyLmV0ZXJuaXR5d2FsbC5jb23wEJmPzXQbxv0AFTIyjTWjMskI8CAurbkrfrBtlinZXSDxj+m/oIkze57hGjTSxu1Xs87XYQjwIPk/LMD0zIgKoEE2dfeoYrrdHuO6dwmghTwUFajH2QzkCPEEZbQw+fAIE+Pq1/Wmdpj/AIPf4w0u+QyOIyJodHRwczovL2J0Yy5jYWxlbmRhci5jYXRhbGxheHkuY29tCPAgB2CbqkV7VpjRKIl3Ea6cBmB/EHcSN/YCgcc1E+mc07QI8CAfpkZ2Hh4Rukz3x4il3tZqQtlDlbna+I6so2t2YSEmMQjwIJOv32jbsMa2HJwpleRCKLEhgYOoHCSfpv1ZO0YNNNFsCPAgLMM7eFfCjokQfU4gdU5WpG/wBLkO9lDRF0GktL6ujt8I8SCRxJ0bC1PQ8qFmI/1jh8AS5d1/6VRJNMt1Hz41QmNr3QjwIBmgrKBF+OZ3y+XOMv2E7IZ4WwLr2u2H+ehsBfy7cPlICPEgm4ZMCSXzZVWu40d+zk2edaur6KOauo8X7V2KaFBR1VoI8SBKVVOiyq6IFqGn/15kLwk7L8upMAIZ0znjhYxYqSTQCQjwIMBD1twPZ33GxbwTiuOCeJPkoP++6R2wYpCii8UBTdgwCPAg84VkgMXwrt2xxRoeC1/6CtsFctki+w3m8Rs5/6g/IhEI8CCnzZQDhJyicX7bS7U8PMUObuC9Y4TXe+4THoXBMMXkxwjxIMZ0oAvshpcwowR3qPEDbwKZ6B4NPSU4Hz/+4PnD74gnCPAgIfLZEKqAvkMNXfakXoNq1UVqGSzL4Z86z5GzUfbvw8UI8SC8KoIeLvjd4vJ/xhNVphakPRd80YKeNkeYEuVH8k2EtAjwIPRtinLLxzt8iuw0XZtpTDzEstZOTNYVm+Bi3fEzdeIuCPFZAQAAAAE8vsasINN5DKon0KakX2HNdCB126ZLKXrw4PfvyEfqbwAAAAAA/f///wLPGw8AAAAAABYAFCvgxlh6msa4ZOtgvlc5KiZCx7IvAAAAAAAAAAAiaiDwBKygDAAICPEgB/2DJ3s6gMky/PceGZocTFRXjZUiCCAhHGYwQk/8yrUICPEgDuSd6+PJHUMuEHyLcKFxw7xfvRHRfInjkV3/Zy3BxqAICPEgZDgQ+4VXzlOIkGoO8EVxDgs2cWaeh4EEiaqa/y50gKAICPAgFI+zIuYcMF69GmPQVsXa9oy8eng7MeRZdIxArQyeX3oICPEgX5HYIuImpiSTEapgEssEW4l+W+4aRfNCG3pZf7z0hCoICPEgPkAbOSjFdtS4NT7MXgMYVQoQhI1JZtdFxUu4J3NTt7IICPEguW3qyuyGjctu5d9rM9P9ZCs/ZK4vAc+z21b9ygklgWAICPEgu4e2645xtvGhI1Zzuiv23vRhwE8uC9vj1TAgNg/C8UcICPAgwoQX8X0nY4HoQLRsJ0z8JCWQDzRh2iL2QXEb8z3gbjIICPAgzTwlPRtStsLJWhz3Q/0l8tMnrPSHVuh+zCiGk95dW2MICPAgGcBEuYZyzFNapHOfnJ9Q515QzO2VbIRhlVI0vIhd4jwICPAgidRMoM2pA+KmVJenVrLcbollsbUg9lL9bmv1C1dSxswICPAgZinGakwhbHdanTaRJeBkEUlbhfNokvj8b5KneyG+wzIICAAFiJYNc9cZAQOtwTI=\",\"created_at\":1706324334,\"id\":\"2ad074ddb7724eb13b4244b49cf2321b1057f37fdf8ce102e6329b839cf763a9\",\"kind\":1040,\"pubkey\":\"82fbb08c8ef45c4d71c88368d0ae805bc62fb92f166ab04a0b7a0c83d8cbc29a\",\"sig\":\"ad7274bb32ba9e9cfdbd52f4887e8a2fda1047c75a7185b2ab7ff254ebac14ed48a2b60737494d655e24c9400eeeec7e29293a77bfcaafaecd94b350c9a2c22b\",\"tags\":[[\"e\",\"a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6\"],[\"p\",\"c31e22c3715c1bde5608b7e0d04904f22f5fc453ba1806d21c9f2382e1e58c6c\"],[\"alt\",\"NIP-03 time stamp\"]]}" + val otsPendingEvent = "{\"id\":\"12fa15ad4b4cf9dc5940389325b69b93c5c1f59c049c701ee669b275299fdaf1\",\"pubkey\":\"dcaa6c8a2f47b6fef4a34b20e8843c59dbe7c5f07a402338c09fd147dd01d22b\",\"created_at\":1708877521,\"kind\":1040,\"tags\":[[\"e\",\"a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6\"],[\"alt\",\"Opentimestamps Attestation\"]],\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqGNPU2jhd4no+zg2ytDkuf5PIoivr8KHI8BL68aKGNbwELidvzr0usf55CkpKf6OABQI//AQK3sWd2tq+7KO8YNJIARJugjxBGXbZtPwCL0H4/7GL5+SAIPf4w0u+QyOLCtodHRwczovL2JvYi5idGMuY2FsZW5kYXIub3BlbnRpbWVzdGFtcHMub3Jn//AQPDZsJgN1TnJXoUzlsgo93wjwIIfBc7LUqkCbC1BLZRZ+6LXztK50UdH5xe7fn40bupkrCPEEZdtm0/AI0CADXN5ZIncAg9/jDS75DI4uLWh0dHBzOi8vYWxpY2UuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ/AQcELcSrE04cuGKlZQf2LeVwjwILUDSf9vK2GaefKTpn/LV2oUsQaA5WbqaP3C+1ZxQfRNCPEEZdtm0/AIbCtb+yRXFqUAg9/jDS75DI4pKGh0dHBzOi8vZmlubmV5LmNhbGVuZGFyLmV0ZXJuaXR5d2FsbC5jb20=\",\"sig\":\"f6854c0228c15c08aeb70bbabe9ed87bbb7289fab31b13cabac15138bb71179553e06080b83f4a813fbdaf614f63293beea3fc73fe865da6551193fa4d38de04\"}" + + val otsEvent2Digest = "a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6" + + @Before + fun setup() { + OtsEvent.otsInstance = OpenTimestamps(OkHttpBlockstreamExplorer(), OkHttpCalendarBuilder()) + } + + @Test + fun verifyNostrEvent() { + val ots = Event.fromJson(otsEvent) as OtsEvent + println(ots.info()) + assertEquals(1707688818L, ots.verify()) + } + + @Test + fun verifyNostrEvent2() { + val ots = Event.fromJson(otsEvent2) as OtsEvent + println(ots.info()) + assertEquals(1706322179L, ots.verify()) + } + + @Test + fun verifyNostrPendingEvent() { + val ots = Event.fromJson(otsPendingEvent) as OtsEvent + println(ots.info()) + assertEquals(null, ots.verify()) + } + + @Test + fun createOTSEventAndVerify() { + val signer = NostrSignerInternal(KeyPair()) + var ots: OtsEvent? = null + + val countDownLatch = CountDownLatch(1) + + OtsEvent.create(otsEvent2Digest, signer) { + ots = it + countDownLatch.countDown() + } + + Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) + + println(ots!!.toJson()) + println(ots!!.info()) + + // Should not be valid + assertEquals(null, ots!!.verify()) + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt b/app/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt index 33893662f..ad21a7f0f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt @@ -29,7 +29,11 @@ import android.util.Log import androidx.security.crypto.EncryptedSharedPreferences import coil.ImageLoader import coil.disk.DiskCache +import com.vitorpamplona.amethyst.service.ots.OkHttpBlockstreamExplorer +import com.vitorpamplona.amethyst.service.ots.OkHttpCalendarBuilder import com.vitorpamplona.amethyst.service.playback.VideoCache +import com.vitorpamplona.quartz.events.OtsEvent +import com.vitorpamplona.quartz.ots.OpenTimestamps import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -65,6 +69,8 @@ class Amethyst : Application() { super.onCreate() instance = this + OtsEvent.otsInstance = OpenTimestamps(OkHttpBlockstreamExplorer(), OkHttpCalendarBuilder()) + if (BuildConfig.DEBUG) { StrictMode.setThreadPolicy( ThreadPolicy.Builder().detectAll().penaltyLog().build(), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpBlockstreamExplorer.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpBlockstreamExplorer.kt new file mode 100644 index 000000000..9b074670a --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpBlockstreamExplorer.kt @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2024 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.ots + +import android.util.Log +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.vitorpamplona.amethyst.BuildConfig +import com.vitorpamplona.amethyst.service.HttpClientManager +import com.vitorpamplona.quartz.ots.BitcoinExplorer +import com.vitorpamplona.quartz.ots.BlockHeader +import com.vitorpamplona.quartz.ots.exceptions.UrlException +import okhttp3.Request + +class OkHttpBlockstreamExplorer : BitcoinExplorer { + /** + * Retrieve the block information from the block hash. + * + * @param hash Hash of the block. + * @return the blockheader of the hash + * @throws Exception desc + */ + override fun block(hash: String): BlockHeader { + val client = HttpClientManager.getHttpClient() + val url = "$BLOCKSTREAM_API_URL/block/$hash" + + val request = + Request.Builder() + .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") + .header("Accept", "application/json") + .url(url) + .get() + .build() + + client.newCall(request).execute().use { + if (it.isSuccessful) { + val jsonObject = jacksonObjectMapper().readTree(it.body.string()) + + val blockHeader = BlockHeader() + blockHeader.merkleroot = jsonObject["merkle_root"].asText() + blockHeader.setTime(jsonObject["timestamp"].asInt().toString()) + blockHeader.blockHash = hash + Log.d("OkHttpBlockstreamExplorer", "$BLOCKSTREAM_API_URL/block/$hash") + return blockHeader + } else { + throw UrlException("Couldn't open $url: " + it.message + " " + it.code) + } + } + } + + /** + * Retrieve the block hash from the block height. + * + * @param height Height of the block. + * @return the hash of the block at height height + * @throws Exception desc + */ + @Throws(Exception::class) + override fun blockHash(height: Int): String { + val client = HttpClientManager.getHttpClient() + + val url = "$BLOCKSTREAM_API_URL/block-height/$height" + + val request = + Request.Builder() + .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") + .url(url) + .get() + .build() + + client.newCall(request).execute().use { + if (it.isSuccessful) { + val blockHash = it.body.string() + + Log.d("OkHttpBlockstreamExplorer", "$url $blockHash") + return blockHash + } else { + throw UrlException(it.message) + } + } + } + + companion object { + private const val BLOCKSTREAM_API_URL = "https://blockstream.info/api" + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendar.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendar.kt new file mode 100644 index 000000000..f14d5048a --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendar.kt @@ -0,0 +1,133 @@ +/** + * Copyright (c) 2024 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.ots + +import com.vitorpamplona.amethyst.BuildConfig +import com.vitorpamplona.amethyst.service.HttpClientManager +import com.vitorpamplona.quartz.encoders.Hex +import com.vitorpamplona.quartz.ots.ICalendar +import com.vitorpamplona.quartz.ots.StreamDeserializationContext +import com.vitorpamplona.quartz.ots.Timestamp +import com.vitorpamplona.quartz.ots.exceptions.CommitmentNotFoundException +import com.vitorpamplona.quartz.ots.exceptions.DeserializationException +import com.vitorpamplona.quartz.ots.exceptions.ExceededSizeException +import com.vitorpamplona.quartz.ots.exceptions.UrlException +import com.vitorpamplona.quartz.ots.http.Request +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody + +/** + * Class representing remote calendar server interface. + */ +class OkHttpCalendar(val url: String) : ICalendar { + /** + * Submitting a digest to remote calendar. Returns a com.eternitywall.ots.Timestamp committing to that digest. + * + * @param digest The digest hash to send. + * @return the Timestamp received from the calendar. + * @throws ExceededSizeException if response is too big. + * @throws UrlException if url is not reachable. + * @throws DeserializationException if the data is corrupt + */ + @Throws(ExceededSizeException::class, UrlException::class, DeserializationException::class) + override fun submit(digest: ByteArray): Timestamp { + try { + val client = HttpClientManager.getHttpClient() + val url = "$url/digest" + + val mediaType = "application/x-www-form-urlencoded; charset=utf-8".toMediaType() + val requestBody = digest.toRequestBody(mediaType) + + val request = + okhttp3.Request.Builder() + .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") + .header("Accept", "application/vnd.opentimestamps.v1") + .header("Content-Type", "application/x-www-form-urlencoded") + .url(url) + .post(requestBody) + .build() + + client.newCall(request).execute().use { + if (it.isSuccessful) { + val ctx = StreamDeserializationContext(it.body.bytes()) + return Timestamp.deserialize(ctx, digest) + } else { + throw UrlException("Failed to open $url") + } + } + } catch (e: ExceededSizeException) { + throw e + } catch (e: DeserializationException) { + throw e + } catch (e: Exception) { + throw UrlException(e.message) + } + } + + /** + * Get a timestamp for a given commitment. + * + * @param commitment The digest hash to send. + * @return the Timestamp from the calendar server (with blockchain information if already written). + * @throws ExceededSizeException if response is too big. + * @throws UrlException if url is not reachable. + * @throws CommitmentNotFoundException if commit is not found. + * @throws DeserializationException if the data is corrupt + */ + @Throws( + DeserializationException::class, + ExceededSizeException::class, + CommitmentNotFoundException::class, + UrlException::class, + ) + override fun getTimestamp(commitment: ByteArray): Timestamp { + try { + val client = HttpClientManager.getHttpClient() + val url = url + "/timestamp/" + Hex.encode(commitment) + + val request = + okhttp3.Request.Builder() + .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") + .header("Accept", "application/vnd.opentimestamps.v1") + .header("Content-Type", "application/x-www-form-urlencoded") + .url(url) + .get() + .build() + + client.newCall(request).execute().use { + if (it.isSuccessful) { + val ctx = StreamDeserializationContext(it.body.bytes()) + return Timestamp.deserialize(ctx, commitment) + } else { + throw CommitmentNotFoundException("Calendar response a status code != 200: " + it.code) + } + } + } catch (e: DeserializationException) { + throw e + } catch (e: ExceededSizeException) { + throw e + } catch (e: CommitmentNotFoundException) { + throw e + } catch (e: Exception) { + throw UrlException(e.message) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendarAsyncSubmit.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendarAsyncSubmit.kt new file mode 100644 index 000000000..be082413f --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendarAsyncSubmit.kt @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2024 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.ots + +import com.vitorpamplona.amethyst.BuildConfig +import com.vitorpamplona.amethyst.service.HttpClientManager +import com.vitorpamplona.quartz.ots.ICalendarAsyncSubmit +import com.vitorpamplona.quartz.ots.StreamDeserializationContext +import com.vitorpamplona.quartz.ots.Timestamp +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +import java.util.Optional +import java.util.concurrent.BlockingQueue + +/** + * For making async calls to a calendar server + */ +class OkHttpCalendarAsyncSubmit(private val url: String, private val digest: ByteArray) : ICalendarAsyncSubmit { + private var queue: BlockingQueue>? = null + + fun setQueue(queue: BlockingQueue>?) { + this.queue = queue + } + + @Throws(Exception::class) + override fun call(): Optional { + val client = HttpClientManager.getHttpClient() + val url = "$url/digest" + + val mediaType = "application/x-www-form-urlencoded; charset=utf-8".toMediaType() + val requestBody = digest.toRequestBody(mediaType) + + val request = + okhttp3.Request.Builder() + .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") + .header("Accept", "application/vnd.opentimestamps.v1") + .header("Content-Type", "application/x-www-form-urlencoded") + .url(url) + .post(requestBody) + .build() + + client.newCall(request).execute().use { + if (it.isSuccessful) { + val ctx = StreamDeserializationContext(it.body.bytes()) + val timestamp = Timestamp.deserialize(ctx, digest) + val of = Optional.of(timestamp) + queue!!.add(of) + return of + } else { + queue!!.add(Optional.empty()) + return Optional.empty() + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendarBuilder.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendarBuilder.kt new file mode 100644 index 000000000..b8f9408a8 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendarBuilder.kt @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2024 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.ots + +import com.vitorpamplona.quartz.ots.CalendarBuilder +import com.vitorpamplona.quartz.ots.ICalendar +import com.vitorpamplona.quartz.ots.ICalendarAsyncSubmit + +class OkHttpCalendarBuilder : CalendarBuilder { + override fun newSyncCalendar(url: String): ICalendar { + return OkHttpCalendar(url) + } + + override fun newAsyncCalendar( + url: String, + digest: ByteArray, + ): ICalendarAsyncSubmit { + return OkHttpCalendarAsyncSubmit(url, digest) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/OtsEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/OtsEvent.kt index 988ec5836..60dfe74f2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/OtsEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/OtsEvent.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.hexToByteArray import com.vitorpamplona.quartz.ots.BlockstreamExplorer +import com.vitorpamplona.quartz.ots.CalendarPureJavaBuilder import com.vitorpamplona.quartz.ots.DetachedTimestampFile import com.vitorpamplona.quartz.ots.Hash import com.vitorpamplona.quartz.ots.OpenTimestamps @@ -58,7 +59,7 @@ class OtsEvent( val detachedOts = DetachedTimestampFile.deserialize(otsByteArray()) - val result = OpenTimestamps(BlockstreamExplorer()).verify(detachedOts, digest) + val result = otsInstance.verify(detachedOts, digest) if (result == null || result.isEmpty()) { return null } else { @@ -73,17 +74,19 @@ class OtsEvent( fun info(): String { val detachedOts = DetachedTimestampFile.deserialize(otsByteArray()) - return OpenTimestamps(BlockstreamExplorer()).info(detachedOts) + return otsInstance.info(detachedOts) } companion object { const val KIND = 1040 const val ALT = "Opentimestamps Attestation" + var otsInstance = OpenTimestamps(BlockstreamExplorer(), CalendarPureJavaBuilder()) + fun stamp(eventId: HexKey): ByteArray { val hash = Hash(eventId.hexToByteArray(), OpSHA256._TAG) val file = DetachedTimestampFile.from(hash) - val timestamp = OpenTimestamps(BlockstreamExplorer()).stamp(file) + val timestamp = otsInstance.stamp(file) val detachedToSerialize = DetachedTimestampFile(hash.getOp(), timestamp) return detachedToSerialize.serialize() } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/Calendar.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/Calendar.java index 8de9f03b8..27c870d8a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/ots/Calendar.java +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/Calendar.java @@ -14,7 +14,7 @@ import java.util.Map; /** * Class representing remote calendar server interface. */ -public class Calendar { +public class Calendar implements ICalendar { private String url; @@ -46,6 +46,7 @@ public class Calendar { * @throws UrlException if url is not reachable. * @throws DeserializationException if the data is corrupt */ + @Override public Timestamp submit(byte[] digest) throws ExceededSizeException, UrlException, DeserializationException { try { @@ -85,6 +86,7 @@ public class Calendar { * @throws CommitmentNotFoundException if commit is not found. * @throws DeserializationException if the data is corrupt */ + @Override public Timestamp getTimestamp(byte[] commitment) throws DeserializationException, ExceededSizeException, CommitmentNotFoundException, UrlException { try { Map headers = new HashMap<>(); diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/CalendarAsyncSubmit.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/CalendarAsyncSubmit.java index 9d772fe84..08b69a261 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/ots/CalendarAsyncSubmit.java +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/CalendarAsyncSubmit.java @@ -10,12 +10,11 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.BlockingQueue; -import java.util.concurrent.Callable; /** * For making async calls to a calendar server */ -public class CalendarAsyncSubmit implements Callable> { +public class CalendarAsyncSubmit implements ICalendarAsyncSubmit { private String url; private byte[] digest; diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/CalendarBuilder.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/CalendarBuilder.java new file mode 100644 index 000000000..f630401e4 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/CalendarBuilder.java @@ -0,0 +1,6 @@ +package com.vitorpamplona.quartz.ots; + +public interface CalendarBuilder { + public ICalendar newSyncCalendar(String url); + public ICalendarAsyncSubmit newAsyncCalendar(String url, byte[] digest); +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/CalendarPureJavaBuilder.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/CalendarPureJavaBuilder.java new file mode 100644 index 000000000..d75b65b4c --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/CalendarPureJavaBuilder.java @@ -0,0 +1,10 @@ +package com.vitorpamplona.quartz.ots; + +public class CalendarPureJavaBuilder implements CalendarBuilder { + public ICalendar newSyncCalendar(String url) { + return new Calendar(url); + } + public ICalendarAsyncSubmit newAsyncCalendar(String url, byte[] digest) { + return new CalendarAsyncSubmit(url, digest); + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/ICalendar.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/ICalendar.java new file mode 100644 index 000000000..5f04fa40b --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/ICalendar.java @@ -0,0 +1,13 @@ +package com.vitorpamplona.quartz.ots; + +import com.vitorpamplona.quartz.ots.exceptions.CommitmentNotFoundException; +import com.vitorpamplona.quartz.ots.exceptions.DeserializationException; +import com.vitorpamplona.quartz.ots.exceptions.ExceededSizeException; +import com.vitorpamplona.quartz.ots.exceptions.UrlException; + +public interface ICalendar { + Timestamp submit(byte[] digest) + throws ExceededSizeException, UrlException, DeserializationException; + + Timestamp getTimestamp(byte[] commitment) throws DeserializationException, ExceededSizeException, CommitmentNotFoundException, UrlException; +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/ICalendarAsyncSubmit.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/ICalendarAsyncSubmit.java new file mode 100644 index 000000000..9dd821150 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/ICalendarAsyncSubmit.java @@ -0,0 +1,9 @@ +package com.vitorpamplona.quartz.ots; + +import java.util.Optional; +import java.util.concurrent.Callable; + +public interface ICalendarAsyncSubmit extends Callable> { + @Override + Optional call() throws Exception; +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/OpenTimestamps.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/OpenTimestamps.java index 648885639..d422144ce 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/ots/OpenTimestamps.java +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/OpenTimestamps.java @@ -33,9 +33,12 @@ import java.util.concurrent.Executors; public class OpenTimestamps { BitcoinExplorer explorer; + CalendarBuilder calBuilder; - public OpenTimestamps(BitcoinExplorer explorer) { + + public OpenTimestamps(BitcoinExplorer explorer, CalendarBuilder builder) { this.explorer = explorer; + this.calBuilder = builder; } /** diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/http/Request.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/http/Request.java index 7a65deac6..0df2db21d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/ots/http/Request.java +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/http/Request.java @@ -91,29 +91,4 @@ public class Request implements Callable { return response; } - - public static String urlEncodeUTF8(String s) { - try { - return URLEncoder.encode(s, "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new UnsupportedOperationException(e); - } - } - - public static String urlEncodeUTF8(Map map) { - StringBuilder sb = new StringBuilder(); - - for (Map.Entry entry : map.entrySet()) { - if (sb.length() > 0) { - sb.append("&"); - } - - sb.append(String.format("%s=%s", - urlEncodeUTF8(entry.getKey().toString()), - urlEncodeUTF8(entry.getValue().toString()) - )); - } - - return sb.toString(); - } } \ No newline at end of file From 2ea80272e3e2871efda8d2d012cf10438d322760 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Sun, 25 Feb 2024 22:06:59 +0000 Subject: [PATCH 161/176] New Crowdin translations by GitHub Action --- app/src/main/res/values-cs/strings.xml | 4 ++++ app/src/main/res/values-de/strings.xml | 4 ++++ app/src/main/res/values-pt-rBR/strings.xml | 4 ++++ app/src/main/res/values-sv-rSE/strings.xml | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 8441e2ccf..93c3138b5 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -655,4 +655,8 @@ Děkujeme! Maximální limit Omezené zápisy + Rozštěpováno od + Git repositář: %1$s + Internet: + Klon: diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index e264fd6d4..d8f916137 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -660,4 +660,8 @@ anz der Bedingungen ist erforderlich Vielen Dank! Maximallimit Eingeschränkte Schriften + Geforkt von + Git Repository: %1$s + Internet: + Klonen: diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index dfbbdb79c..25a3e7e47 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -655,4 +655,8 @@ Obrigado! Limite máximo Gravações Restritas + Forkado de + Repositório Git: %1$s + Site: + Clonar: diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index b69803aff..7cbd2d550 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -654,4 +654,8 @@ Tack! Maximal gräns Begränsade skrivningar + Forkad från + Git Repository: %1$s + Webbplats: + Klona: From de17aab7abea8ae1819d87465b703dd71a953328 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 25 Feb 2024 17:53:15 -0500 Subject: [PATCH 162/176] Fixing stack overflow with more than 200 zaps in a single note. --- .../vitorpamplona/amethyst/model/Account.kt | 2 +- .../com/vitorpamplona/amethyst/model/Note.kt | 63 ++++++++++++------- .../amethyst/ui/note/PollNoteViewModel.kt | 2 +- 3 files changed, 44 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 7ff939e23..21331740e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -729,7 +729,7 @@ class Account( zapResponseEvent.response(signer, onReady) } - fun calculateIfNoteWasZappedByAccount( + suspend fun calculateIfNoteWasZappedByAccount( zappedNote: Note?, onWasZapped: () -> Unit, ) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 646af317c..7602992b7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -56,6 +56,7 @@ import com.vitorpamplona.quartz.events.LiveActivitiesEvent import com.vitorpamplona.quartz.events.LnZapEvent import com.vitorpamplona.quartz.events.LnZapPaymentRequestEvent import com.vitorpamplona.quartz.events.LnZapPaymentResponseEvent +import com.vitorpamplona.quartz.events.LnZapRequestEvent import com.vitorpamplona.quartz.events.LongTextNoteEvent import com.vitorpamplona.quartz.events.PayInvoiceSuccessResponse import com.vitorpamplona.quartz.events.RepostEvent @@ -66,10 +67,13 @@ import com.vitorpamplona.quartz.utils.containsAny import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeoutOrNull import java.math.BigDecimal import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter +import kotlin.coroutines.resume @Stable class AddressableNote(val address: ATag) : Note(address.toTag()) { @@ -521,7 +525,7 @@ open class Note(val idHex: String) { } } - private fun recursiveIsZappedByCalculation( + private suspend fun isZappedByCalculation( option: Int?, user: User, account: Account, @@ -532,48 +536,65 @@ open class Note(val idHex: String) { return } - val next = remainingZapEvents.first() + remainingZapEvents.forEach { next -> + val zapRequest = next.first.event as LnZapRequestEvent + val zapEvent = next.second?.event as? LnZapEvent - if (next.first.author?.pubkeyHex == user.pubkeyHex) { - onWasZappedByAuthor() - } else { - account.decryptZapContentAuthor(next.first) { - if ( - it.pubKey == user.pubkeyHex && - (option == null || option == (it as? LnZapEvent)?.zappedPollOption()) - ) { + if (!zapRequest.isPrivateZap()) { + // public events + if (zapRequest.pubKey == user.pubkeyHex && (option == null || option == zapEvent?.zappedPollOption())) { onWasZappedByAuthor() + return + } + } else { + // private events + + // if has already decrypted + val privateZap = zapRequest.cachedPrivateZap() + if (privateZap != null) { + if (privateZap.pubKey == user.pubkeyHex && (option == null || option == zapEvent?.zappedPollOption())) { + onWasZappedByAuthor() + return + } } else { - recursiveIsZappedByCalculation( - option, - user, - account, - remainingZapEvents.minus(next), - onWasZappedByAuthor, - ) + if (account.isWriteable()) { + val result = + withTimeoutOrNull(1000) { + suspendCancellableCoroutine { continuation -> + zapRequest.decryptPrivateZap(account.signer) { + continuation.resume(it) + } + } + } + + if (result?.pubKey == user.pubkeyHex && (option == null || option == zapEvent?.zappedPollOption())) { + onWasZappedByAuthor() + return + } + } } } } } - fun isZappedBy( + suspend fun isZappedBy( user: User, account: Account, onWasZappedByAuthor: () -> Unit, ) { - recursiveIsZappedByCalculation(null, user, account, zaps.toList(), onWasZappedByAuthor) + isZappedByCalculation(null, user, account, zaps.toList(), onWasZappedByAuthor) if (account.userProfile() == user) { recursiveIsPaidByCalculation(account, zapPayments.toList(), onWasZappedByAuthor) } } - fun isZappedBy( + suspend fun isZappedBy( option: Int?, user: User, account: Account, onWasZappedByAuthor: () -> Unit, ) { - recursiveIsZappedByCalculation(option, user, account, zaps.toList(), onWasZappedByAuthor) + isZappedByCalculation(option, user, account, zaps.toList(), onWasZappedByAuthor) } fun getReactionBy(user: User): String? { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt index 965dc95b3..0fb6008fa 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt @@ -209,7 +209,7 @@ class PollNoteViewModel : ViewModel() { return false } - fun isPollOptionZappedBy( + suspend fun isPollOptionZappedBy( option: Int, user: User, onWasZappedByAuthor: () -> Unit, From 782926fbeae137aa26dddb97ea5a036830bbb5f0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 25 Feb 2024 18:24:50 -0500 Subject: [PATCH 163/176] Separates the `q` filter from the `e` filter to avoid and moves `q` to the last position since it can be cut off and won't affect the app. --- .../service/NostrSingleEventDataSource.kt | 64 +++++++++++++------ 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt index 3bfaa538f..6d5f14e21 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt @@ -120,27 +120,50 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") { } return groupByEOSEPresence(eventsToWatch).map { - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - JsonFilter( - kinds = - listOf( - TextNoteEvent.KIND, - ReactionEvent.KIND, - RepostEvent.KIND, - GenericRepostEvent.KIND, - ReportEvent.KIND, - LnZapEvent.KIND, - PollNoteEvent.KIND, - ), - tags = mapOf("e" to it.map { it.idHex }, "q" to it.map { it.idHex }), - since = findMinimumEOSEs(it), - // Max amount of "replies" to download on a specific event. - limit = 1000, - ), + listOf( + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + JsonFilter( + kinds = + listOf( + TextNoteEvent.KIND, + ReactionEvent.KIND, + RepostEvent.KIND, + GenericRepostEvent.KIND, + ReportEvent.KIND, + LnZapEvent.KIND, + PollNoteEvent.KIND, + ), + tags = mapOf("e" to it.map { it.idHex }), + since = findMinimumEOSEs(it), + // Max amount of "replies" to download on a specific event. + limit = 1000, + ), + ), ) + }.flatten() + } + + private fun createQuotesFilter(): List? { + if (eventsToWatch.isEmpty()) { + return null } + + return groupByEOSEPresence(eventsToWatch).map { + listOf( + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + JsonFilter( + tags = mapOf("q" to it.map { it.idHex }), + since = findMinimumEOSEs(it), + // Max amount of "replies" to download on a specific event. + limit = 1000, + ), + ), + ) + }.flatten() } fun createLoadEventsIfNotLoadedFilter(): List? { @@ -205,9 +228,10 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") { val missing = createLoadEventsIfNotLoadedFilter() val addresses = createAddressFilter() val addressReactions = createReactionsToWatchInAddressFilter() + val quotes = createQuotesFilter() singleEventChannel.typedFilters = - listOfNotNull(missing, addresses, reactions, addressReactions).flatten().ifEmpty { null } + listOfNotNull(missing, addresses, reactions, addressReactions, quotes).flatten().ifEmpty { null } } fun add(eventId: Note) { From 3c36f52bafa7fe879c81ff6f4595e561c15cb6c0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 26 Feb 2024 15:17:17 -0500 Subject: [PATCH 164/176] Visual support for Kind1 forks. --- .../vitorpamplona/amethyst/model/Account.kt | 2 + .../amethyst/ui/actions/NewPostView.kt | 3 +- .../amethyst/ui/actions/NewPostViewModel.kt | 46 +++++++++- .../amethyst/ui/buttons/ChannelFabColumn.kt | 2 +- .../amethyst/ui/components/ClickableRoute.kt | 7 +- .../amethyst/ui/components/SplitItem.kt | 9 ++ .../amethyst/ui/elements/DisplayReward.kt | 1 + .../amethyst/ui/note/NoteCompose.kt | 89 +++++++++++++++++-- .../amethyst/ui/note/ReactionsRow.kt | 53 ++++++++++- .../amethyst/ui/screen/ThreadFeedView.kt | 7 +- .../ui/screen/loggedIn/VideoScreen.kt | 9 +- app/src/main/res/values/strings.xml | 2 + .../quartz/events/BaseTextNoteEvent.kt | 13 +++ .../quartz/events/TextNoteEvent.kt | 5 ++ .../quartz/events/WikiNoteEvent.kt | 10 --- 15 files changed, 226 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 21331740e..1cc7b8423 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1325,6 +1325,7 @@ class Account( replyingTo: String?, root: String?, directMentions: Set, + forkedFrom: Event?, relayList: List? = null, geohash: String? = null, nip94attachments: List? = null, @@ -1349,6 +1350,7 @@ class Account( directMentions = directMentions, geohash = geohash, nip94attachments = nip94attachments, + forkedFrom = forkedFrom, signer = signer, ) { Client.send(it, relayList = relayList) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index 5ebf74489..9294f7081 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -186,6 +186,7 @@ fun NewPostView( onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = null, + fork: Note? = null, enableMessageInterface: Boolean = false, accountViewModel: AccountViewModel, nav: (String) -> Unit, @@ -201,7 +202,7 @@ fun NewPostView( var relayList = remember { accountViewModel.account.activeWriteRelays().toImmutableList() } LaunchedEffect(Unit) { - postViewModel.load(accountViewModel, baseReplyTo, quote) + postViewModel.load(accountViewModel, baseReplyTo, quote, fork) launch(Dispatchers.IO) { postViewModel.imageUploadingError.collect { error -> diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index f25bdeb94..944f01097 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -55,6 +55,7 @@ import com.vitorpamplona.quartz.events.BaseTextNoteEvent import com.vitorpamplona.quartz.events.ChatMessageEvent import com.vitorpamplona.quartz.events.ClassifiedsEvent import com.vitorpamplona.quartz.events.CommunityDefinitionEvent +import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileStorageEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent @@ -85,10 +86,10 @@ open class NewPostViewModel() : ViewModel() { var requiresNIP24: Boolean = false var originalNote: Note? = null + var forkedFromNote: Note? = null var pTags by mutableStateOf?>(null) var eTags by mutableStateOf?>(null) - var imetaTags = mutableStateListOf>() var nip94attachments by mutableStateOf>(emptyList()) var nip95attachments by @@ -166,6 +167,7 @@ open class NewPostViewModel() : ViewModel() { accountViewModel: AccountViewModel, replyingTo: Note?, quote: Note?, + fork: Note?, ) { this.accountViewModel = accountViewModel this.account = accountViewModel.account @@ -214,6 +216,37 @@ open class NewPostViewModel() : ViewModel() { zapRaiserAmount = null forwardZapTo = Split() forwardZapToEditting = TextFieldValue("") + + fork?.let { + message = TextFieldValue(it.event?.content() ?: "") + urlPreview = findUrlInMessage() + + it.event?.isSensitive()?.let { + if (it) wantsToMarkAsSensitive = true + } + + it.event?.zapraiserAmount()?.let { + zapRaiserAmount = it + } + + it.event?.zapSplitSetup()?.let { + val totalWeight = it.sumOf { if (it.isLnAddress) 0.0 else it.weight } + + it.forEach { + if (!it.isLnAddress) { + forwardZapTo.addItem(LocalCache.getOrCreateUser(it.lnAddressOrPubKeyHex), (it.weight / totalWeight).toFloat()) + } + } + } + + it.author?.let { + if (this.pTags?.contains(it) != true) { + this.pTags = listOf(it) + (this.pTags ?: emptyList()) + } + } + + forkedFromNote = it + } } fun sendPost(relayList: List? = null) { @@ -404,9 +437,16 @@ open class NewPostViewModel() : ViewModel() { val replyId = originalNote?.idHex + val replyToSet = + if (forkedFromNote != null) { + (listOfNotNull(forkedFromNote) + (tagger.eTags ?: emptyList())).ifEmpty { null } + } else { + tagger.eTags + } + account?.sendPost( message = tagger.message, - replyTo = tagger.eTags, + replyTo = replyToSet, mentions = tagger.pTags, tags = null, zapReceiver = zapReceiver, @@ -415,6 +455,7 @@ open class NewPostViewModel() : ViewModel() { replyingTo = replyId, root = rootId, directMentions = tagger.directMentions, + forkedFrom = forkedFromNote?.event as? Event, relayList = relayList, geohash = geoHash, nip94attachments = usedAttachments, @@ -504,7 +545,6 @@ open class NewPostViewModel() : ViewModel() { urlPreview = null isUploadingImage = false pTags = null - imetaTags.clear() wantsDirectMessage = false diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/ChannelFabColumn.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/ChannelFabColumn.kt index d2544a659..018d32834 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/ChannelFabColumn.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/ChannelFabColumn.kt @@ -65,7 +65,7 @@ fun ChannelFabColumn( if (wantsToSendNewMessage) { NewPostView( - { wantsToSendNewMessage = false }, + onClose = { wantsToSendNewMessage = false }, enableMessageInterface = true, accountViewModel = accountViewModel, nav = nav, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt index 31f5ea043..107f40c7d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt @@ -294,6 +294,7 @@ fun CreateClickableText( maxLines: Int = Int.MAX_VALUE, overrideColor: Color? = null, fontWeight: FontWeight? = null, + fontSize: TextUnit = TextUnit.Unspecified, route: String, nav: (String) -> Unit, ) { @@ -304,12 +305,14 @@ fun CreateClickableText( remember(clickablePart, suffix) { val clickablePartStyle = SpanStyle( + fontSize = fontSize, color = overrideColor ?: primaryColor, fontWeight = fontWeight, ) val nonClickablePartStyle = SpanStyle( + fontSize = fontSize, color = overrideColor ?: onBackgroundColor, fontWeight = fontWeight, ) @@ -562,6 +565,7 @@ fun CreateClickableTextWithEmoji( maxLines: Int = Int.MAX_VALUE, overrideColor: Color? = null, fontWeight: FontWeight = FontWeight.Normal, + fontSize: TextUnit = TextUnit.Unspecified, route: String, nav: (String) -> Unit, tags: ImmutableListOfLists?, @@ -570,11 +574,12 @@ fun CreateClickableTextWithEmoji( text = clickablePart, tags = tags, onRegularText = { - CreateClickableText(it, null, maxLines, overrideColor, fontWeight, route, nav) + CreateClickableText(it, null, maxLines, overrideColor, fontWeight, fontSize, route, nav) }, onEmojiText = { val clickablePartStyle = SpanStyle( + fontSize = fontSize, color = overrideColor ?: MaterialTheme.colorScheme.primary, fontWeight = fontWeight, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SplitItem.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SplitItem.kt index 389cb358a..6fedfb580 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SplitItem.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SplitItem.kt @@ -32,6 +32,15 @@ class SplitItem(val key: T) { class Split() { var items: List> by mutableStateOf(emptyList()) + fun addItem( + key: T, + percentage: Float, + ) { + val newItem = SplitItem(key) + newItem.percentage = percentage + this.items = items.plus(newItem) + } + fun addItem(key: T): Int { val wasEqualSplit = isEqualSplit() val newItem = SplitItem(key) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayReward.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayReward.kt index ae6e5409a..2c7520103 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayReward.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/elements/DisplayReward.kt @@ -184,6 +184,7 @@ class AddBountyAmountViewModel : ViewModel() { replyingTo = null, root = null, directMentions = setOf(), + forkedFrom = null, ) nextAmount = TextFieldValue("") diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index a895af644..935ec7838 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -80,6 +80,7 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -178,6 +179,7 @@ import com.vitorpamplona.amethyst.ui.theme.grayText import com.vitorpamplona.amethyst.ui.theme.imageModifier import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor +import com.vitorpamplona.amethyst.ui.theme.nip05 import com.vitorpamplona.amethyst.ui.theme.normalNoteModifier import com.vitorpamplona.amethyst.ui.theme.normalWithTopMarginNoteModifier import com.vitorpamplona.amethyst.ui.theme.placeholderText @@ -2512,28 +2514,32 @@ fun SecondUserInfoRow( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - val noteEvent = remember { note.event } ?: return - val noteAuthor = remember { note.author } ?: return + val noteEvent = note.event ?: return + val noteAuthor = note.author ?: return Row( verticalAlignment = CenterVertically, modifier = UserNameMaxRowHeight, ) { - ObserveDisplayNip05Status(noteAuthor, remember { Modifier.weight(1f) }, accountViewModel, nav) + if (noteEvent is BaseTextNoteEvent && noteEvent.isAFork()) { + ShowForkInformation(noteEvent, remember(noteEvent) { Modifier.weight(1f) }, accountViewModel, nav) + } else { + ObserveDisplayNip05Status(noteAuthor, remember(noteEvent) { Modifier.weight(1f) }, accountViewModel, nav) + } - val geo = remember { noteEvent.getGeoHash() } + val geo = remember(noteEvent) { noteEvent.getGeoHash() } if (geo != null) { Spacer(StdHorzSpacer) DisplayLocation(geo, nav) } - val baseReward = remember { noteEvent.getReward()?.let { Reward(it) } } + val baseReward = remember(noteEvent) { noteEvent.getReward()?.let { Reward(it) } } if (baseReward != null) { Spacer(StdHorzSpacer) DisplayReward(baseReward, note, accountViewModel, nav) } - val pow = remember { noteEvent.getPoWRank() } + val pow = remember(noteEvent) { noteEvent.getPoWRank() } if (pow > 20) { Spacer(StdHorzSpacer) DisplayPoW(pow) @@ -2541,6 +2547,76 @@ fun SecondUserInfoRow( } } +@Composable +private fun ShowForkInformation( + noteEvent: BaseTextNoteEvent, + modifier: Modifier, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val forkedAddress = remember(noteEvent) { noteEvent.forkFromAddress() } + val forkedEvent = remember(noteEvent) { noteEvent.forkFromVersion() } + if (forkedAddress != null) { + LoadAddressableNote(aTag = forkedAddress, accountViewModel = accountViewModel) { addressableNote -> + if (addressableNote != null) { + ForkInformationRowLightColor(addressableNote, modifier, accountViewModel, nav) + } + } + } else if (forkedEvent != null) { + LoadNote(forkedEvent, accountViewModel = accountViewModel) { event -> + if (event != null) { + ForkInformationRowLightColor(event, modifier, accountViewModel, nav) + } + } + } +} + +@Composable +fun ForkInformationRowLightColor( + originalVersion: Note, + modifier: Modifier = Modifier, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteState by originalVersion.live().metadata.observeAsState() + val note = noteState?.note ?: return + val author = note.author ?: return + val route = remember(note) { routeFor(note, accountViewModel.userProfile()) } + + if (route != null) { + Row(modifier) { + ClickableText( + text = + buildAnnotatedString { + append(stringResource(id = R.string.forked_from)) + append(" ") + }, + onClick = { nav(route) }, + style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.nip05, fontSize = Font14SP), + maxLines = 1, + overflow = TextOverflow.Visible, + ) + + val userState by author.live().metadata.observeAsState() + val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() } + val userTags = + remember(userState) { userState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() } + + if (userDisplayName != null) { + CreateClickableTextWithEmoji( + clickablePart = userDisplayName, + maxLines = 1, + route = route, + overrideColor = MaterialTheme.colorScheme.nip05, + fontSize = Font14SP, + nav = nav, + tags = userTags, + ) + } + } + } +} + @Composable fun LoadStatuses( user: User, @@ -3928,7 +4004,6 @@ private fun WikiNoteHeader( nav: (String) -> Unit, ) { val title = remember(noteEvent) { noteEvent.title() } - val forkedAddress = remember(noteEvent) { noteEvent.forkFromAddress() } val summary = remember(noteEvent) { noteEvent.summary()?.ifBlank { null } ?: noteEvent.content.take(200).ifBlank { null } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 098a56f4d..794a20752 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -127,6 +127,7 @@ import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderTextColorFilter import com.vitorpamplona.quartz.encoders.Nip30CustomEmoji +import com.vitorpamplona.quartz.events.BaseTextNoteEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.persistentListOf @@ -499,6 +500,7 @@ private fun BoostWithDialog( nav: (String) -> Unit, ) { var wantsToQuote by remember { mutableStateOf(null) } + var wantsToFork by remember { mutableStateOf(null) } if (wantsToQuote != null) { NewPostView( @@ -510,7 +512,34 @@ private fun BoostWithDialog( ) } - BoostReaction(baseNote, grayTint, accountViewModel) { wantsToQuote = baseNote } + if (wantsToFork != null) { + val replyTo = + remember(wantsToFork) { + val forkEvent = wantsToFork?.event + if (forkEvent is BaseTextNoteEvent) { + val hex = forkEvent.replyingTo() + wantsToFork?.replyTo?.filter { it.event?.id() == hex }?.firstOrNull() + } else { + null + } + } + + NewPostView( + onClose = { wantsToFork = null }, + baseReplyTo = replyTo, + fork = wantsToFork, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + BoostReaction( + baseNote, + grayTint, + accountViewModel, + onQuotePress = { wantsToQuote = baseNote }, + onForkPress = { wantsToFork = baseNote }, + ) } @Composable @@ -650,6 +679,7 @@ fun BoostReaction( iconSizeModifier: Modifier = Size20Modifier, iconSize: Dp = Size20dp, onQuotePress: () -> Unit, + onForkPress: () -> Unit, ) { var wantsToBoost by remember { mutableStateOf(false) } @@ -671,7 +701,13 @@ fun BoostReaction( wantsToBoost = false onQuotePress() }, - onRepost = { accountViewModel.boost(baseNote) }, + onRepost = { + accountViewModel.boost(baseNote) + }, + onFork = { + wantsToBoost = false + onForkPress() + }, ) } } @@ -1149,6 +1185,7 @@ private fun BoostTypeChoicePopup( onDismiss: () -> Unit, onQuote: () -> Unit, onRepost: () -> Unit, + onFork: () -> Unit, ) { val iconSizePx = with(LocalDensity.current) { -iconSize.toPx().toInt() } @@ -1189,6 +1226,18 @@ private fun BoostTypeChoicePopup( ) { Text(stringResource(R.string.quote), color = Color.White, textAlign = TextAlign.Center) } + + Button( + modifier = Modifier.padding(horizontal = 3.dp), + onClick = onFork, + shape = ButtonBorder, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + ), + ) { + Text(stringResource(R.string.fork), color = Color.White, textAlign = TextAlign.Center) + } } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index abb9572d6..511a8caa4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -83,7 +83,6 @@ import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.InlineCarrousel @@ -913,7 +912,7 @@ private fun RenderWikiHeaderForThread( forkedAddress?.let { LoadAddressableNote(aTag = it, accountViewModel = accountViewModel) { originalVersion -> if (originalVersion != null) { - ShowForkInformation(originalVersion, Modifier.fillMaxWidth(), accountViewModel, nav) + ForkInformationRow(originalVersion, Modifier.fillMaxWidth(), accountViewModel, nav) } } } @@ -934,8 +933,8 @@ private fun RenderWikiHeaderForThread( } @Composable -fun ShowForkInformation( - originalVersion: AddressableNote, +fun ForkInformationRow( + originalVersion: Note, modifier: Modifier = Modifier, accountViewModel: AccountViewModel, nav: (String) -> Unit, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt index 0ac705db3..24ff01a66 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt @@ -476,9 +476,12 @@ fun ReactionsColumn( accountViewModel = accountViewModel, iconSizeModifier = Size40Modifier, iconSize = Size40dp, - ) { - wantsToQuote = baseNote - } + onQuotePress = { + wantsToQuote = baseNote + }, + onForkPress = { + }, + ) LikeReaction( baseNote = baseNote, grayTint = MaterialTheme.colorScheme.onBackground, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d68c4f42e..f83505b19 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -47,6 +47,7 @@ Boost boosted Quote + Fork New Amount in Sats Add "replying to " @@ -777,6 +778,7 @@ Max Limit Restricted Writes Forked from + FORK Git Repository: %1$s Web: Clone: diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt index 39ba084ed..7d387e918 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.events import android.util.Log import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.Nip19Bech32 import com.vitorpamplona.quartz.encoders.Nip19Bech32.nip19regex @@ -42,6 +43,18 @@ open class BaseTextNoteEvent( ) : Event(id, pubKey, createdAt, kind, tags, content, sig) { fun mentions() = taggedUsers() + fun isAFork() = tags.any { it.size > 3 && (it[0] == "a" || it[0] == "e") && it[3] == "fork" } + + fun forkFromAddress() = + tags.firstOrNull { it.size > 3 && it[0] == "a" && it[3] == "fork" }?.let { + val aTagValue = it[1] + val relay = it.getOrNull(2) + + ATag.parse(aTagValue, relay) + } + + fun forkFromVersion() = tags.firstOrNull { it.size > 3 && it[0] == "e" && it[3] == "fork" }?.get(1) + open fun replyTos(): List { val oldStylePositional = tags.filter { it.size > 1 && it.size <= 3 && it[0] == "e" }.map { it[1] } val newStyleReply = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "reply" }?.get(1) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt index bd0efc504..79a3161b2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt @@ -57,6 +57,7 @@ class TextNoteEvent( directMentions: Set = emptySet(), geohash: String? = null, nip94attachments: List? = null, + forkedFrom: Event? = null, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (TextNoteEvent) -> Unit, @@ -69,6 +70,7 @@ class TextNoteEvent( root = root, replyingTo = replyingTo, directMentions = directMentions, + forkedFrom = forkedFrom?.id, ), ) } @@ -93,6 +95,7 @@ class TextNoteEvent( root = root, replyingTo = replyingTo, directMentions = directMentions, + forkedFrom = (forkedFrom as? AddressableEvent)?.address()?.toTag(), ), ) } @@ -136,6 +139,7 @@ class TextNoteEvent( root: String?, replyingTo: String?, directMentions: Set, + forkedFrom: String?, ) = sortedWith { o1, o2 -> when { o1 == o2 -> 0 @@ -150,6 +154,7 @@ class TextNoteEvent( when (it) { root -> arrayOf(tagName, it, "", "root") replyingTo -> arrayOf(tagName, it, "", "reply") + forkedFrom -> arrayOf(tagName, it, "", "fork") in directMentions -> arrayOf(tagName, it, "", "mention") else -> arrayOf(tagName, it) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/WikiNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/WikiNoteEvent.kt index 25bb51454..a08bb9992 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/WikiNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/WikiNoteEvent.kt @@ -41,16 +41,6 @@ class WikiNoteEvent( fun topics() = hashtags() - fun forkFromAddress() = - tags.firstOrNull { it.size > 3 && it[0] == "a" && it[3] == "fork" }?.let { - val aTagValue = it[1] - val relay = it.getOrNull(2) - - ATag.parse(aTagValue, relay) - } - - fun forkFromVersion() = tags.firstOrNull { it.size > 3 && it[0] == "e" && it[3] == "fork" }?.get(1) - fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1) fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1) From 85ecaf18cc4624fa75cab37a52b2f2e069acf319 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 26 Feb 2024 15:47:22 -0500 Subject: [PATCH 165/176] Sets split zaps for all quotes and forks --- .../amethyst/ui/actions/NewPostViewModel.kt | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index 944f01097..1c7284708 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -199,11 +199,6 @@ open class NewPostViewModel() : ViewModel() { pTags = null } - quote?.let { - message = TextFieldValue(message.text + "\nnostr:${it.toNEvent()}") - urlPreview = findUrlInMessage() - } - canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null canAddZapRaiser = accountViewModel.userProfile().info?.lnAddress() != null canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channelHex() == null @@ -217,6 +212,25 @@ open class NewPostViewModel() : ViewModel() { forwardZapTo = Split() forwardZapToEditting = TextFieldValue("") + quote?.let { + message = TextFieldValue(message.text + "\nnostr:${it.toNEvent()}") + urlPreview = findUrlInMessage() + + it.author?.let { quotedUser -> + if (quotedUser.pubkeyHex != accountViewModel.userProfile().pubkeyHex) { + if (forwardZapTo.items.none { it.key.pubkeyHex == quotedUser.pubkeyHex }) { + forwardZapTo.addItem(quotedUser) + } + if (forwardZapTo.items.none { it.key.pubkeyHex == accountViewModel.userProfile().pubkeyHex }) { + forwardZapTo.addItem(accountViewModel.userProfile()) + } + + val pos = forwardZapTo.items.indexOfFirst { it.key.pubkeyHex == quotedUser.pubkeyHex } + forwardZapTo.updatePercentage(pos, 0.9f) + } + } + } + fork?.let { message = TextFieldValue(it.event?.content() ?: "") urlPreview = findUrlInMessage() @@ -239,6 +253,19 @@ open class NewPostViewModel() : ViewModel() { } } + // Only adds if it is not already set up. + if (forwardZapTo.items.isEmpty()) { + it.author?.let { forkedAuthor -> + if (forkedAuthor.pubkeyHex != accountViewModel.userProfile().pubkeyHex) { + if (forwardZapTo.items.none { it.key.pubkeyHex == forkedAuthor.pubkeyHex }) forwardZapTo.addItem(forkedAuthor) + if (forwardZapTo.items.none { it.key.pubkeyHex == accountViewModel.userProfile().pubkeyHex }) forwardZapTo.addItem(accountViewModel.userProfile()) + + val pos = forwardZapTo.items.indexOfFirst { it.key.pubkeyHex == forkedAuthor.pubkeyHex } + forwardZapTo.updatePercentage(pos, 0.8f) + } + } + } + it.author?.let { if (this.pTags?.contains(it) != true) { this.pTags = listOf(it) + (this.pTags ?: emptyList()) @@ -247,6 +274,10 @@ open class NewPostViewModel() : ViewModel() { forkedFromNote = it } + + if (!forwardZapTo.items.isEmpty()) { + wantsForwardZapTo = true + } } fun sendPost(relayList: List? = null) { From bf67ff64b4aef5c7b197a5f714240143ee5a5015 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 26 Feb 2024 15:47:44 -0500 Subject: [PATCH 166/176] Moves the image preview on new posts to the top of additional options for the post. --- .../amethyst/ui/actions/NewPostView.kt | 92 +++++++++---------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index 9294f7081..ef685f603 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -381,6 +381,52 @@ fun NewPostView( } } + val myUrlPreview = postViewModel.urlPreview + if (myUrlPreview != null) { + Row(modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) { + if (RichTextParser.isValidURL(myUrlPreview)) { + if (RichTextParser.isImageUrl(myUrlPreview)) { + AsyncImage( + model = myUrlPreview, + contentDescription = myUrlPreview, + contentScale = ContentScale.FillWidth, + modifier = + Modifier + .padding(top = 4.dp) + .fillMaxWidth() + .clip(shape = QuoteBorder) + .border( + 1.dp, + MaterialTheme.colorScheme.subtleBorder, + QuoteBorder, + ), + ) + } else if (RichTextParser.isVideoUrl(myUrlPreview)) { + VideoView( + myUrlPreview, + roundedCorner = true, + accountViewModel = accountViewModel, + ) + } else { + LoadUrlPreview(myUrlPreview, myUrlPreview, accountViewModel) + } + } else if (RichTextParser.startsWithNIP19Scheme(myUrlPreview)) { + val bgColor = MaterialTheme.colorScheme.background + val backgroundColor = remember { mutableStateOf(bgColor) } + + BechLink( + myUrlPreview, + true, + backgroundColor, + accountViewModel, + nav, + ) + } else if (RichTextParser.isUrlWithoutScheme(myUrlPreview)) { + LoadUrlPreview("https://$myUrlPreview", myUrlPreview, accountViewModel) + } + } + } + if (postViewModel.wantsToMarkAsSensitive) { Row( verticalAlignment = Alignment.CenterVertically, @@ -468,52 +514,6 @@ fun NewPostView( ) } } - - val myUrlPreview = postViewModel.urlPreview - if (myUrlPreview != null) { - Row(modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) { - if (RichTextParser.isValidURL(myUrlPreview)) { - if (RichTextParser.isImageUrl(myUrlPreview)) { - AsyncImage( - model = myUrlPreview, - contentDescription = myUrlPreview, - contentScale = ContentScale.FillWidth, - modifier = - Modifier - .padding(top = 4.dp) - .fillMaxWidth() - .clip(shape = QuoteBorder) - .border( - 1.dp, - MaterialTheme.colorScheme.subtleBorder, - QuoteBorder, - ), - ) - } else if (RichTextParser.isVideoUrl(myUrlPreview)) { - VideoView( - myUrlPreview, - roundedCorner = true, - accountViewModel = accountViewModel, - ) - } else { - LoadUrlPreview(myUrlPreview, myUrlPreview, accountViewModel) - } - } else if (RichTextParser.startsWithNIP19Scheme(myUrlPreview)) { - val bgColor = MaterialTheme.colorScheme.background - val backgroundColor = remember { mutableStateOf(bgColor) } - - BechLink( - myUrlPreview, - true, - backgroundColor, - accountViewModel, - nav, - ) - } else if (RichTextParser.isUrlWithoutScheme(myUrlPreview)) { - LoadUrlPreview("https://$myUrlPreview", myUrlPreview, accountViewModel) - } - } - } } } From 8ebad524ff2fed3955e82e536f879689a4c47dcf Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 26 Feb 2024 18:51:23 -0500 Subject: [PATCH 167/176] Adds local cache to wait for OTS confirmations Presents OTS pending and confirmations on the screen Adds Menu item to OTS any post. --- .../amethyst/LocalPreferences.kt | 28 ++++++ .../vitorpamplona/amethyst/model/Account.kt | 34 ++++++++ .../amethyst/model/LocalCache.kt | 43 +++++++++ .../com/vitorpamplona/amethyst/model/Note.kt | 2 + .../service/NostrSingleEventDataSource.kt | 2 + .../amethyst/ui/note/NoteCompose.kt | 67 ++++++++++++++ .../amethyst/ui/note/NoteQuickActionMenu.kt | 8 +- .../amethyst/ui/note/UserProfilePicture.kt | 23 ++++- .../ui/screen/loggedIn/AccountViewModel.kt | 25 +++++- app/src/main/res/values/strings.xml | 4 + .../java/com/vitorpamplona/quartz/OtsTest.kt | 32 ++++++- .../vitorpamplona/quartz/events/OtsEvent.kt | 87 ++++++++++++++----- 12 files changed, 321 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index ec87e1d13..e606ac8d8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -43,6 +43,7 @@ import com.vitorpamplona.amethyst.service.HttpClientManager import com.vitorpamplona.amethyst.service.Nip96MediaServers import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.quartz.crypto.KeyPair +import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.Nip47WalletConnect import com.vitorpamplona.quartz.encoders.hexToByteArray import com.vitorpamplona.quartz.encoders.toHexKey @@ -113,6 +114,7 @@ private object PrefKeys { const val AUTOMATICALLY_SHOW_PROFILE_PICTURE = "automatically_show_profile_picture" const val SIGNER_PACKAGE_NAME = "signer_package_name" const val HAS_DONATED_IN_VERSION = "has_donated_in_version" + const val PENDING_ATTESTATIONS = "pending_attestations" const val ALL_ACCOUNT_INFO = "all_saved_accounts_info" const val SHARED_SETTINGS = "shared_settings" @@ -339,6 +341,11 @@ object LocalPreferences { } else { putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent!!) } + + putString( + PrefKeys.PENDING_ATTESTATIONS, + Event.mapper.writeValueAsString(account.pendingAttestations), + ) } .apply() } @@ -556,6 +563,26 @@ object LocalPreferences { null } + val pendingAttestations = + try { + getString(PrefKeys.PENDING_ATTESTATIONS, null)?.let { + println("Decoding Attestation List: " + it) + if (it != null) { + Event.mapper.readValue>(it) + } else { + null + } + } + } catch (e: Throwable) { + if (e is CancellationException) throw e + Log.w( + "LocalPreferences", + "Error Decoding Contact List ${getString(PrefKeys.LATEST_CONTACT_LIST, null)}", + e, + ) + null + } + val languagePreferences = try { getString(PrefKeys.LANGUAGE_PREFS, null)?.let { @@ -649,6 +676,7 @@ object LocalPreferences { filterSpamFromStrangers = filterSpam, lastReadPerRoute = lastReadPerRoute, hasDonatedInVersion = hasDonatedInVersion, + pendingAttestations = pendingAttestations ?: emptyMap(), ) // Loads from DB diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 1cc7b8423..ee83c10a5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -76,6 +76,7 @@ import com.vitorpamplona.quartz.events.LnZapRequestEvent import com.vitorpamplona.quartz.events.MetadataEvent import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.NIP24Factory +import com.vitorpamplona.quartz.events.OtsEvent import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.events.PollNoteEvent import com.vitorpamplona.quartz.events.Price @@ -185,6 +186,7 @@ class Account( var filterSpamFromStrangers: Boolean = true, var lastReadPerRoute: Map = mapOf(), var hasDonatedInVersion: Set = setOf(), + var pendingAttestations: Map = mapOf(), val scope: CoroutineScope = Amethyst.instance.applicationIOScope, ) { var transientHiddenUsers: ImmutableSet = persistentSetOf() @@ -893,6 +895,38 @@ class Account( } } + suspend fun updateAttestations() { + Log.d("Pending Attestations", "Updating ${pendingAttestations.size} pending attestations") + + pendingAttestations.toMap().forEach { pair -> + val newAttestation = OtsEvent.upgrade(pair.value, pair.key) + + if (pair.value != newAttestation) { + OtsEvent.create(pair.key, newAttestation, signer) { + LocalCache.justConsume(it, null) + Client.send(it) + + pendingAttestations = pendingAttestations - pair.key + } + } + } + } + + fun hasPendingAttestations(note: Note): Boolean { + val id = note.event?.id() ?: note.idHex + return pendingAttestations.get(id) != null + } + + fun timestamp(note: Note) { + if (!isWriteable()) return + + val id = note.event?.id() ?: note.idHex + + pendingAttestations = pendingAttestations + Pair(id, OtsEvent.stamp(id)) + + saveable.invalidateData() + } + fun follow(user: User) { if (!isWriteable()) return diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index e23032590..e21342a4f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -86,6 +86,7 @@ import com.vitorpamplona.quartz.events.LongTextNoteEvent import com.vitorpamplona.quartz.events.MetadataEvent import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.NNSEvent +import com.vitorpamplona.quartz.events.OtsEvent import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.events.PinListEvent import com.vitorpamplona.quartz.events.PollNoteEvent @@ -716,6 +717,28 @@ object LocalCache { } } + fun consume( + event: OtsEvent, + relay: Relay?, + ) { + val version = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) + + // Already processed this event. + if (version.event?.id() == event.id()) return + + if (version.event == null) { + // makes sure the OTS has a valid certificate + if (event.cacheVerify() == null) return // no valid OTS + + version.loadEvent(event, author, emptyList()) + + version.liveSet?.innerOts?.invalidateData() + } + + refreshObservers(version) + } + fun consume( event: BadgeDefinitionEvent, relay: Relay?, @@ -1626,6 +1649,25 @@ object LocalCache { .toImmutableList() } + suspend fun findEarliestOtsForNote(note: Note): Long? { + checkNotInMainThread() + + val validOts = + notes + .mapNotNull { + val noteEvent = it.value.event + if ((noteEvent is OtsEvent && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpired())) { + noteEvent.verifiedTime + } else { + null + } + } + + if (validOts.isEmpty()) return null + + return validOts.minBy { it } + } + fun cleanObservers() { notes.forEach { it.value.clearLive() } @@ -1960,6 +2002,7 @@ object LocalCache { is MetadataEvent -> consume(event) is MuteListEvent -> consume(event, relay) is NNSEvent -> comsume(event, relay) + is OtsEvent -> consume(event, relay) is PrivateDmEvent -> consume(event, relay) is PinListEvent -> consume(event, relay) is PeopleListEvent -> consume(event, relay) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 7602992b7..73526c322 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -947,6 +947,7 @@ class NoteLiveSet(u: Note) { val innerReports = NoteBundledRefresherLiveData(u) val innerRelays = NoteBundledRefresherLiveData(u) val innerZaps = NoteBundledRefresherLiveData(u) + val innerOts = NoteBundledRefresherLiveData(u) val metadata = innerMetadata.map { it } val reactions = innerReactions.map { it } @@ -1011,6 +1012,7 @@ class NoteLiveSet(u: Note) { innerReports.destroy() innerRelays.destroy() innerZaps.destroy() + innerOts.destroy() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt index 6d5f14e21..e29566892 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt @@ -31,6 +31,7 @@ import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent import com.vitorpamplona.quartz.events.GenericRepostEvent import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.events.LnZapEvent +import com.vitorpamplona.quartz.events.OtsEvent import com.vitorpamplona.quartz.events.PollNoteEvent import com.vitorpamplona.quartz.events.ReactionEvent import com.vitorpamplona.quartz.events.ReportEvent @@ -134,6 +135,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") { ReportEvent.KIND, LnZapEvent.KIND, PollNoteEvent.KIND, + OtsEvent.KIND, ), tags = mapOf("e" to it.map { it.idHex }), since = findMinimumEOSEs(it), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 935ec7838..b9bc45ef3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -113,6 +113,7 @@ import com.vitorpamplona.amethyst.ui.actions.NewRelayListView import com.vitorpamplona.amethyst.ui.components.ClickableUrl import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji +import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.LoadThumbAndThenVideoView import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status @@ -177,6 +178,7 @@ import com.vitorpamplona.amethyst.ui.theme.boostedNoteModifier import com.vitorpamplona.amethyst.ui.theme.channelNotePictureModifier import com.vitorpamplona.amethyst.ui.theme.grayText import com.vitorpamplona.amethyst.ui.theme.imageModifier +import com.vitorpamplona.amethyst.ui.theme.lessImportantLink import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor import com.vitorpamplona.amethyst.ui.theme.nip05 @@ -2544,9 +2546,43 @@ fun SecondUserInfoRow( Spacer(StdHorzSpacer) DisplayPoW(pow) } + + DisplayOts(note, accountViewModel) } } +@Composable +fun DisplayOts( + note: Note, + accountViewModel: AccountViewModel, +) { + LoadOts( + note, + accountViewModel, + whenConfirmed = { + val context = LocalContext.current + val timeStr by remember(note) { mutableStateOf(timeAgo(it, context = context)) } + + Text( + stringResource(id = R.string.existed_since, timeStr), + color = MaterialTheme.colorScheme.lessImportantLink, + fontSize = Font14SP, + fontWeight = FontWeight.Bold, + maxLines = 1, + ) + }, + whenPending = { + Text( + stringResource(id = R.string.timestamp_pending_short), + color = MaterialTheme.colorScheme.lessImportantLink, + fontSize = Font14SP, + fontWeight = FontWeight.Bold, + maxLines = 1, + ) + }, + ) +} + @Composable private fun ShowForkInformation( noteEvent: BaseTextNoteEvent, @@ -2638,6 +2674,37 @@ fun LoadStatuses( content(statuses) } +@Composable +fun LoadOts( + note: Note, + accountViewModel: AccountViewModel, + whenConfirmed: @Composable (Long) -> Unit, + whenPending: @Composable () -> Unit, +) { + var earliestDate: GenericLoadable by remember { mutableStateOf(GenericLoadable.Loading()) } + + val noteStatus by note.live().innerOts.observeAsState() + + LaunchedEffect(key1 = noteStatus) { + accountViewModel.findOtsEventsForNote(noteStatus?.note ?: note) { newOts -> + if (newOts == null) { + earliestDate = GenericLoadable.Empty() + } else { + earliestDate = GenericLoadable.Loaded(newOts) + } + } + } + + (earliestDate as? GenericLoadable.Loaded)?.let { + whenConfirmed(it.loaded) + } ?: run { + val account = accountViewModel.account.saveable.observeAsState() + if (account.value?.account?.hasPendingAttestations(note) == true) { + whenPending() + } + } +} + @Composable fun LoadCityName( geohash: GeoHash, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt index 3f4d2d513..d2bbb7f59 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -342,11 +342,9 @@ private fun RenderMainPopup( icon = ImageVector.vectorResource(id = R.drawable.relays), label = stringResource(R.string.broadcast), ) { - scope.launch(Dispatchers.IO) { - accountViewModel.broadcast(note) - // showSelectTextDialog = true - onDismiss() - } + accountViewModel.broadcast(note) + // showSelectTextDialog = true + onDismiss() } VerticalDivider(primaryLight) NoteQuickActionItem( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index a645395c9..b49ef2786 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -554,13 +554,28 @@ fun NoteDropDownMenu( DropdownMenuItem( text = { Text(stringResource(R.string.broadcast)) }, onClick = { - scope.launch(Dispatchers.IO) { - accountViewModel.broadcast(note) - onDismiss() - } + accountViewModel.broadcast(note) + onDismiss() }, ) Divider() + if (accountViewModel.account.hasPendingAttestations(note)) { + DropdownMenuItem( + text = { Text(stringResource(R.string.timestamp_pending)) }, + onClick = { + onDismiss() + }, + ) + } else { + DropdownMenuItem( + text = { Text(stringResource(R.string.timestamp_it)) }, + onClick = { + accountViewModel.timestamp(note) + onDismiss() + }, + ) + } + Divider() if (state.isPrivateBookmarkNote) { DropdownMenuItem( text = { Text(stringResource(R.string.remove_from_private_bookmarks)) }, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 5181cbc74..1f53559a5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -528,7 +528,22 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View } fun broadcast(note: Note) { - account.broadcast(note) + viewModelScope.launch(Dispatchers.IO) { account.broadcast(note) } + } + + fun timestamp(note: Note) { + viewModelScope.launch(Dispatchers.IO) { account.timestamp(note) } + } + + var lastTimeItTriedToUpdateAttestations: Long = 0 + + fun upgradeAttestations() { + // only tries to upgrade every hour + val now = TimeUtils.now() + if (now - lastTimeItTriedToUpdateAttestations > TimeUtils.ONE_HOUR) { + lastTimeItTriedToUpdateAttestations = now + viewModelScope.launch(Dispatchers.IO) { account.updateAttestations() } + } } fun delete(note: Note) { @@ -889,6 +904,13 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View viewModelScope.launch(Dispatchers.IO) { onResult(LocalCache.findStatusesForUser(myUser)) } } + fun findOtsEventsForNote( + note: Note, + onResult: (Long?) -> Unit, + ) { + viewModelScope.launch(Dispatchers.IO) { onResult(LocalCache.findEarliestOtsForNote(note)) } + } + private suspend fun checkGetOrCreateChannel(key: HexKey): Channel? { return LocalCache.checkGetOrCreateChannel(key) } @@ -1100,6 +1122,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View "Notification Dots Calculation refresh ${this@AccountViewModel} for ${account.userProfile().toBestDisplayName()}", ) invalidateInsertData(newNotes) + upgradeAttestations() } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f83505b19..b186de86e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -24,6 +24,9 @@ Copy Author ID Copy Note ID Broadcast + Timestamp it + Timestamp: Pending Confirmations + OTS: Pending Request Deletion Block / Report @@ -782,4 +785,5 @@ Git Repository: %1$s Web: Clone: + OTS: %1$s diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/OtsTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/OtsTest.kt index 3a57bb2c0..838c279ca 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/OtsTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/OtsTest.kt @@ -26,8 +26,8 @@ import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.OtsEvent import com.vitorpamplona.quartz.signers.NostrSignerInternal import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.fail import org.junit.Assert -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import java.util.concurrent.CountDownLatch @@ -60,9 +60,33 @@ class OtsTest { val ots = Event.Companion.fromJson(otsPendingEvent) as OtsEvent println(ots.info()) assertEquals(null, ots.verify()) + + val eventId = + ots.digestEvent() ?: run { + fail("Should not be null") + return + } + + val upgraded = OtsEvent.upgrade(ots.content, eventId) + + val signer = NostrSignerInternal(KeyPair()) + + var newOts: OtsEvent? = null + val countDownLatch = CountDownLatch(1) + + OtsEvent.create(eventId, upgraded, signer) { + newOts = it + countDownLatch.countDown() + } + + Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) + + println(newOts!!.toJson()) + println(newOts!!.info()) + + assertEquals(1708879025L, newOts!!.verify()) } - @Ignore("Need to figure out a way to wait for Bitcoin confirmations to test thiss") @Test fun createOTSEventAndVerify() { val signer = NostrSignerInternal(KeyPair()) @@ -70,7 +94,7 @@ class OtsTest { val countDownLatch = CountDownLatch(1) - OtsEvent.create(otsEvent2Digest, signer) { + OtsEvent.create(otsEvent2Digest, OtsEvent.stamp(otsEvent2Digest), signer) { ots = it countDownLatch.countDown() } @@ -80,6 +104,6 @@ class OtsTest { println(ots!!.toJson()) println(ots!!.info()) - assertEquals(1706322179L, ots!!.verify()) + assertEquals(null, ots!!.verify()) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/OtsEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/OtsEvent.kt index 60dfe74f2..f4f895322 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/OtsEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/OtsEvent.kt @@ -45,6 +45,9 @@ class OtsEvent( content: String, sig: HexKey, ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { + @Transient + var verifiedTime: Long? = null + fun digestEvent() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1) fun digest() = digestEvent()?.hexToByteArray() @@ -53,25 +56,19 @@ class OtsEvent( return Base64.getDecoder().decode(content) } - fun verify(): Long? { - try { - val digest = digest() ?: return null - - val detachedOts = DetachedTimestampFile.deserialize(otsByteArray()) - - val result = otsInstance.verify(detachedOts, digest) - if (result == null || result.isEmpty()) { - return null - } else { - return result.get(VerifyResult.Chains.BITCOIN)?.timestamp - } - } catch (e: Exception) { - if (e is CancellationException) throw e - Log.e("OpenTimeStamps", "Failed to verify", e) - return null + fun cacheVerify(): Long? { + return if (verifiedTime != null) { + verifiedTime + } else { + verifiedTime = verify() + verifiedTime } } + fun verify(): Long? { + return digestEvent()?.let { OtsEvent.verify(otsByteArray(), it) } + } + fun info(): String { val detachedOts = DetachedTimestampFile.deserialize(otsByteArray()) return otsInstance.info(detachedOts) @@ -83,27 +80,77 @@ class OtsEvent( var otsInstance = OpenTimestamps(BlockstreamExplorer(), CalendarPureJavaBuilder()) - fun stamp(eventId: HexKey): ByteArray { + fun stamp(eventId: HexKey): String { val hash = Hash(eventId.hexToByteArray(), OpSHA256._TAG) val file = DetachedTimestampFile.from(hash) val timestamp = otsInstance.stamp(file) val detachedToSerialize = DetachedTimestampFile(hash.getOp(), timestamp) - return detachedToSerialize.serialize() + return Base64.getEncoder().encodeToString(detachedToSerialize.serialize()) + } + + fun upgrade( + otsFile: String, + eventId: HexKey, + ): String { + val detachedOts = DetachedTimestampFile.deserialize(Base64.getDecoder().decode(otsFile)) + + return if (otsInstance.upgrade(detachedOts)) { + // if the change is now verifiable. + if (verify(detachedOts, eventId) != null) { + Base64.getEncoder().encodeToString(detachedOts.serialize()) + } else { + otsFile + } + } else { + otsFile + } + } + + fun verify( + otsFile: String, + eventId: HexKey, + ): Long? { + return verify(Base64.getDecoder().decode(otsFile), eventId) + } + + fun verify( + otsFile: ByteArray, + eventId: HexKey, + ): Long? { + return verify(DetachedTimestampFile.deserialize(otsFile), eventId) + } + + fun verify( + detachedOts: DetachedTimestampFile, + eventId: HexKey, + ): Long? { + try { + val result = otsInstance.verify(detachedOts, eventId.hexToByteArray()) + if (result == null || result.isEmpty()) { + return null + } else { + return result.get(VerifyResult.Chains.BITCOIN)?.timestamp + } + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e("OpenTimeStamps", "Failed to verify", e) + return null + } } fun create( eventId: HexKey, + otsFileBase64: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (OtsEvent) -> Unit, ) { - val otsFile = stamp(eventId) val tags = arrayOf( arrayOf("e", eventId), arrayOf("alt", ALT), ) - signer.sign(createdAt, KIND, tags, Base64.getEncoder().encodeToString(otsFile), onReady) + signer.sign(createdAt, KIND, tags, otsFileBase64, onReady) } } } From 7dccac41e937a3792bbf4886e2619bc115e56bd2 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Mon, 26 Feb 2024 23:53:45 +0000 Subject: [PATCH 168/176] New Crowdin translations by GitHub Action --- app/src/main/res/values-cs/strings.xml | 2 ++ app/src/main/res/values-de/strings.xml | 2 ++ app/src/main/res/values-pt-rBR/strings.xml | 2 ++ app/src/main/res/values-sv-rSE/strings.xml | 2 ++ 4 files changed, 8 insertions(+) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 93c3138b5..5cd064b5e 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -44,6 +44,7 @@ Zvýšení zvýšeno Citovat + Rozštěpení Nová částka v sats Přidat "odpovídá na " @@ -656,6 +657,7 @@ Maximální limit Omezené zápisy Rozštěpováno od + FORK Git repositář: %1$s Internet: Klon: diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d8f916137..3f5c2d99d 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -44,6 +44,7 @@ Boost boosted Zitat + Fork Neuer Betrag in Sats Hinzufügen "Antworten auf " @@ -661,6 +662,7 @@ anz der Bedingungen ist erforderlich Maximallimit Eingeschränkte Schriften Geforkt von + FORK Git Repository: %1$s Internet: Klonen: diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 25a3e7e47..d62aaa9e2 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -44,6 +44,7 @@ Impulsionar impulsionado Citar + Garfo Novo Valor em Sats Adicionar "respondendo para " @@ -656,6 +657,7 @@ Limite máximo Gravações Restritas Forkado de + FORK Repositório Git: %1$s Site: Clonar: diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 7cbd2d550..e099d4ffb 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -44,6 +44,7 @@ Boosta boostad Citera + Förgrening Nytt belopp i Sats Lägg till "Svarar till " @@ -655,6 +656,7 @@ Maximal gräns Begränsade skrivningar Forkad från + FORK Git Repository: %1$s Webbplats: Klona: From 11ff0736e4b7ec70b5c722ec69f9a39178e8b611 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 26 Feb 2024 20:51:59 -0500 Subject: [PATCH 169/176] Speeding up the loop through local cache. --- .../vitorpamplona/amethyst/model/Account.kt | 6 +- .../amethyst/model/LocalCache.kt | 112 +++++++++++------- .../com/vitorpamplona/amethyst/model/User.kt | 8 +- .../EventNotificationConsumer.kt | 6 +- .../amethyst/ui/components/MarkdownParser.kt | 8 +- .../amethyst/ui/dal/CommunityFeedFilter.kt | 2 +- .../amethyst/ui/dal/GeoHashFeedFilter.kt | 2 +- .../amethyst/ui/dal/HashtagFeedFilter.kt | 2 +- .../ui/dal/HomeConversationsFeedFilter.kt | 2 +- .../ui/dal/HomeNewThreadFeedFilter.kt | 2 +- .../amethyst/ui/dal/NotificationFeedFilter.kt | 2 +- .../dal/UserProfileConversationsFeedFilter.kt | 2 +- .../ui/dal/UserProfileFollowersFeedFilter.kt | 2 +- .../ui/dal/UserProfileNewThreadFeedFilter.kt | 2 +- .../amethyst/ui/dal/VideoFeedFilter.kt | 2 +- .../amethyst/ui/navigation/AppTopBar.kt | 16 +-- .../amethyst/ui/note/UserReactionsRow.kt | 2 +- .../amethyst/ui/screen/CardFeedViewModel.kt | 2 +- .../ui/screen/loggedIn/AccountViewModel.kt | 13 +- .../vitorpamplona/quartz/encoders/Lud06.kt | 11 +- .../com/vitorpamplona/quartz/events/Event.kt | 2 + .../quartz/events/EventInterface.kt | 2 + 22 files changed, 122 insertions(+), 86 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index ee83c10a5..c7421c35f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1202,7 +1202,7 @@ class Account( Client.send(signedEvent, relayList = relayList) LocalCache.consume(signedEvent, null) - return LocalCache.notes[signedEvent.id] + return LocalCache.getNoteIfExists(signedEvent.id) } fun consumeNip95( @@ -1212,7 +1212,7 @@ class Account( LocalCache.consume(data, null) LocalCache.consume(signedEvent, null) - return LocalCache.notes[signedEvent.id] + return LocalCache.getNoteIfExists(signedEvent.id) } fun sendNip95( @@ -1232,7 +1232,7 @@ class Account( Client.send(signedEvent, relayList = relayList) LocalCache.consume(signedEvent, null) - LocalCache.notes[signedEvent.id]?.let { onReady(it) } + LocalCache.getNoteIfExists(signedEvent.id)?.let { onReady(it) } } fun createHeader( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index e21342a4f..3b45fd55a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -102,6 +102,7 @@ import com.vitorpamplona.quartz.events.TextNoteEvent import com.vitorpamplona.quartz.events.VideoHorizontalEvent import com.vitorpamplona.quartz.events.VideoVerticalEvent import com.vitorpamplona.quartz.events.WikiNoteEvent +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toImmutableList @@ -118,18 +119,31 @@ import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter import java.util.concurrent.ConcurrentHashMap +import kotlin.time.measureTimedValue object LocalCache { val antiSpam = AntiSpamFilter() - val users = ConcurrentHashMap(5000) - val notes = ConcurrentHashMap(5000) + private val users = ConcurrentHashMap(5000) + private val notes = ConcurrentHashMap(5000) val channels = ConcurrentHashMap() val addressables = ConcurrentHashMap(100) val awaitingPaymentRequests = ConcurrentHashMap Unit>>(10) + var noteListCache: List = emptyList() + var userListCache: List = emptyList() + + fun updateListCache() { + val (value, elapsed) = + measureTimedValue { + noteListCache = ArrayList(notes.values) + userListCache = ArrayList(users.values) + } + Log.d("LocalCache", "UpdateListCache $elapsed") + } + fun checkGetOrCreateUser(key: String): User? { // checkNotInMainThread() @@ -910,7 +924,7 @@ object LocalCache { event .deleteEvents() - .mapNotNull { notes[it] } + .mapNotNull { getNoteIfExists(it) } .forEach { deleteNote -> // must be the same author if (deleteNote.author?.pubkeyHex == event.pubKey) { @@ -1547,11 +1561,14 @@ object LocalCache { val key = decodePublicKeyAsHexOrNull(username) - if (key != null && users[key] != null) { - return listOfNotNull(users[key]) + if (key != null) { + val user = getUserIfExists(key) + if (user != null) { + return listOfNotNull(user) + } } - return users.values.filter { + return userListCache.filter { (it.anyNameStartsWith(username)) || it.pubkeyHex.startsWith(username, true) || it.pubkeyNpub().startsWith(username, true) @@ -1569,11 +1586,14 @@ object LocalCache { null } - if (key != null && (notes[key] ?: addressables[key]) != null) { - return listOfNotNull(notes[key] ?: addressables[key]) + if (key != null) { + val note = getNoteIfExists(key) + if (note != null) { + return listOfNotNull(note) + } } - return notes.values.filter { + return noteListCache.filter { ( it.event !is GenericRepostEvent && it.event !is RepostEvent && @@ -1652,28 +1672,29 @@ object LocalCache { suspend fun findEarliestOtsForNote(note: Note): Long? { checkNotInMainThread() - val validOts = - notes - .mapNotNull { - val noteEvent = it.value.event - if ((noteEvent is OtsEvent && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpired())) { - noteEvent.verifiedTime - } else { - null + var minTime: Long? = null + val time = TimeUtils.now() + + noteListCache.forEach { item -> + val noteEvent = item.event + if ((noteEvent is OtsEvent && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time))) { + noteEvent.verifiedTime?.let { stampedTime -> + if (minTime == null || stampedTime < (minTime ?: Long.MAX_VALUE)) { + minTime = stampedTime } } + } + } - if (validOts.isEmpty()) return null - - return validOts.minBy { it } + return minTime } fun cleanObservers() { - notes.forEach { it.value.clearLive() } + noteListCache.forEach { it.clearLive() } addressables.forEach { it.value.clearLive() } - users.forEach { it.value.clearLive() } + userListCache.forEach { it.clearLive() } } fun pruneOldAndHiddenMessages(account: Account) { @@ -1699,8 +1720,8 @@ object LocalCache { } } - users.forEach { userPair -> - userPair.value.privateChatrooms.values.map { + userListCache.forEach { userPair -> + userPair.privateChatrooms.values.map { val toBeRemoved = it.pruneMessagesToTheLatestOnly() val childrenToBeRemoved = mutableListOf() @@ -1715,7 +1736,7 @@ object LocalCache { if (toBeRemoved.size > 1) { println( - "PRUNE: ${toBeRemoved.size} private messages with ${userPair.value.toBestDisplayName()} removed. ${it.roomMessages.size} kept", + "PRUNE: ${toBeRemoved.size} private messages with ${userPair.toBestDisplayName()} removed. ${it.roomMessages.size} kept", ) } } @@ -1724,9 +1745,9 @@ object LocalCache { fun prunePastVersionsOfReplaceables() { val toBeRemoved = - notes + noteListCache .filter { - val noteEvent = it.value.event + val noteEvent = it.event if (noteEvent is AddressableEvent) { noteEvent.createdAt() < (addressables[noteEvent.address().toTag()]?.event?.createdAt() ?: 0) @@ -1734,7 +1755,6 @@ object LocalCache { false } } - .values val childrenToBeRemoved = mutableListOf() @@ -1759,24 +1779,23 @@ object LocalCache { checkNotInMainThread() val toBeRemoved = - notes + noteListCache .filter { ( - (it.value.event is TextNoteEvent && !it.value.isNewThread()) || - it.value.event is ReactionEvent || - it.value.event is LnZapEvent || - it.value.event is LnZapRequestEvent || - it.value.event is ReportEvent || - it.value.event is GenericRepostEvent + (it.event is TextNoteEvent && !it.isNewThread()) || + it.event is ReactionEvent || + it.event is LnZapEvent || + it.event is LnZapRequestEvent || + it.event is ReportEvent || + it.event is GenericRepostEvent ) && - it.value.replyTo?.any { it.liveSet?.isInUse() == true } != true && - it.value.liveSet?.isInUse() != true && // don't delete if observing. - it.value.author?.pubkeyHex !in + it.replyTo?.any { it.liveSet?.isInUse() == true } != true && + it.liveSet?.isInUse() != true && // don't delete if observing. + it.author?.pubkeyHex !in accounts && // don't delete if it is the logged in account - it.value.event?.isTaggedUsers(accounts) != + it.event?.isTaggedUsers(accounts) != true // don't delete if it's a notification to the logged in user } - .values val childrenToBeRemoved = mutableListOf() @@ -1842,7 +1861,8 @@ object LocalCache { fun pruneExpiredEvents() { checkNotInMainThread() - val toBeRemoved = notes.filter { it.value.event?.isExpired() == true }.values + val now = TimeUtils.now() + val toBeRemoved = noteListCache.filter { it.event?.isExpirationBefore(now) == true } val childrenToBeRemoved = mutableListOf() @@ -1868,7 +1888,7 @@ object LocalCache { ?.hiddenUsers ?.map { userHex -> ( - notes.values.filter { it.event?.pubKey() == userHex } + + noteListCache.filter { it.event?.pubKey() == userHex } + addressables.values.filter { it.event?.pubKey() == userHex } ) .toSet() @@ -1890,7 +1910,7 @@ object LocalCache { checkNotInMainThread() var removingContactList = 0 - users.values.forEach { + userListCache.forEach { if ( it.pubkeyHex !in loggedIn && (it.liveSet == null || it.liveSet?.isInUse() == false) && @@ -2044,6 +2064,10 @@ class LocalCacheLiveData { private val bundler = BundledInsert(1000, Dispatchers.IO) fun invalidateData(newNote: Note) { - bundler.invalidateList(newNote) { bundledNewNotes -> _newEventBundles.emit(bundledNewNotes) } + bundler.invalidateList(newNote) { + bundledNewNotes -> + _newEventBundles.emit(bundledNewNotes) + LocalCache.updateListCache() + } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt index 7a60c6d04..30ded640a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -139,10 +139,10 @@ class User(val pubkeyHex: String) { // Update Followers of the past user list // Update Followers of the new contact list (oldContactListEvent)?.unverifiedFollowKeySet()?.forEach { - LocalCache.users[it]?.liveSet?.innerFollowers?.invalidateData() + LocalCache.getUserIfExists(it)?.liveSet?.innerFollowers?.invalidateData() } (latestContactList)?.unverifiedFollowKeySet()?.forEach { - LocalCache.users[it]?.liveSet?.innerFollowers?.invalidateData() + LocalCache.getUserIfExists(it)?.liveSet?.innerFollowers?.invalidateData() } liveSet?.innerRelays?.invalidateData() @@ -363,7 +363,7 @@ class User(val pubkeyHex: String) { } suspend fun transientFollowerCount(): Int { - return LocalCache.users.count { it.value.latestContactList?.isTaggedUser(pubkeyHex) ?: false } + return LocalCache.userListCache.count { it.latestContactList?.isTaggedUser(pubkeyHex) ?: false } } fun cachedFollowingKeySet(): Set { @@ -387,7 +387,7 @@ class User(val pubkeyHex: String) { } suspend fun cachedFollowerCount(): Int { - return LocalCache.users.count { it.value.latestContactList?.isTaggedUser(pubkeyHex) ?: false } + return LocalCache.userListCache.count { it.latestContactList?.isTaggedUser(pubkeyHex) ?: false } } fun hasSentMessagesTo(key: ChatroomKey?): Boolean { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt index 5dddfe075..274bb5c87 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt @@ -112,7 +112,7 @@ class EventNotificationConsumer(private val applicationContext: Context) { event.pubKey != acc.userProfile().pubkeyHex ) { // from the user - val chatNote = LocalCache.notes[event.id] ?: return + val chatNote = LocalCache.getNoteIfExists(event.id) ?: return val chatRoom = event.chatroomKey(acc.keyPair.pubKey.toHexKey()) val followingKeySet = acc.followingKeySet() @@ -145,7 +145,7 @@ class EventNotificationConsumer(private val applicationContext: Context) { event: PrivateDmEvent, acc: Account, ) { - val note = LocalCache.notes[event.id] ?: return + val note = LocalCache.getNoteIfExists(event.id) ?: return // old event being re-broadcast if (event.createdAt < TimeUtils.fiveMinutesAgo()) return @@ -184,7 +184,7 @@ class EventNotificationConsumer(private val applicationContext: Context) { event: LnZapEvent, acc: Account, ) { - val noteZapEvent = LocalCache.notes[event.id] ?: return + val noteZapEvent = LocalCache.getNoteIfExists(event.id) ?: return // old event being re-broadcast if (event.createdAt < TimeUtils.fiveMinutesAgo()) return diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/MarkdownParser.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/MarkdownParser.kt index a4714f773..4fe9d706b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/MarkdownParser.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/MarkdownParser.kt @@ -66,19 +66,19 @@ class MarkdownParser { private fun getDisplayNameFromNip19(nip19: Nip19Bech32.Return): Pair? { if (nip19.type == Nip19Bech32.Type.USER) { - LocalCache.users[nip19.hex]?.let { + LocalCache.getUserIfExists(nip19.hex)?.let { return Pair(it.toBestDisplayName(), it.pubkeyNpub()) } } else if (nip19.type == Nip19Bech32.Type.NOTE) { - LocalCache.notes[nip19.hex]?.let { + LocalCache.getNoteIfExists(nip19.hex)?.let { return Pair(it.idDisplayNote(), it.toNEvent()) } } else if (nip19.type == Nip19Bech32.Type.ADDRESS) { - LocalCache.addressables[nip19.hex]?.let { + LocalCache.getAddressableNoteIfExists(nip19.hex)?.let { return Pair(it.idDisplayNote(), it.toNEvent()) } } else if (nip19.type == Nip19Bech32.Type.EVENT) { - LocalCache.notes[nip19.hex]?.let { + LocalCache.getNoteIfExists(nip19.hex)?.let { return Pair(it.idDisplayNote(), it.toNEvent()) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/CommunityFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/CommunityFeedFilter.kt index 5a6cc7288..767d31f51 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/CommunityFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/CommunityFeedFilter.kt @@ -33,7 +33,7 @@ class CommunityFeedFilter(val note: AddressableNote, val account: Account) : } override fun feed(): List { - return sort(innerApplyFilter(LocalCache.notes.values)) + return sort(innerApplyFilter(LocalCache.noteListCache)) } override fun applyFilter(collection: Set): Set { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GeoHashFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GeoHashFeedFilter.kt index b902abbb6..65d249e5a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GeoHashFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GeoHashFeedFilter.kt @@ -36,7 +36,7 @@ class GeoHashFeedFilter(val tag: String, val account: Account) : AdditiveFeedFil } override fun feed(): List { - return sort(innerApplyFilter(LocalCache.notes.values)) + return sort(innerApplyFilter(LocalCache.noteListCache)) } override fun applyFilter(collection: Set): Set { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt index b51d4c2a4..4eaa3778f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt @@ -36,7 +36,7 @@ class HashtagFeedFilter(val tag: String, val account: Account) : AdditiveFeedFil } override fun feed(): List { - return sort(innerApplyFilter(LocalCache.notes.values)) + return sort(innerApplyFilter(LocalCache.noteListCache)) } override fun applyFilter(collection: Set): Set { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt index 178b970f7..15d977db7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt @@ -45,7 +45,7 @@ class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter { - return sort(innerApplyFilter(LocalCache.notes.values)) + return sort(innerApplyFilter(LocalCache.noteListCache)) } override fun applyFilter(collection: Set): Set { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt index 07cc506ed..ac87c3507 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt @@ -50,7 +50,7 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter() } override fun feed(): List { - val notes = innerApplyFilter(LocalCache.notes.values, true) + val notes = innerApplyFilter(LocalCache.noteListCache, true) val longFormNotes = innerApplyFilter(LocalCache.addressables.values, false) return sort(notes + longFormNotes) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt index d4acf44f0..a92a0d4a5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt @@ -52,7 +52,7 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter() } override fun feed(): List { - return sort(innerApplyFilter(LocalCache.notes.values)) + return sort(innerApplyFilter(LocalCache.noteListCache)) } override fun applyFilter(collection: Set): Set { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileConversationsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileConversationsFeedFilter.kt index 1a4171d1c..81953dd82 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileConversationsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileConversationsFeedFilter.kt @@ -36,7 +36,7 @@ class UserProfileConversationsFeedFilter(val user: User, val account: Account) : } override fun feed(): List { - return sort(innerApplyFilter(LocalCache.notes.values)) + return sort(innerApplyFilter(LocalCache.noteListCache)) } override fun applyFilter(collection: Set): Set { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileFollowersFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileFollowersFeedFilter.kt index 3c7a11e79..616be35e7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileFollowersFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileFollowersFeedFilter.kt @@ -30,7 +30,7 @@ class UserProfileFollowersFeedFilter(val user: User, val account: Account) : Fee } override fun feed(): List { - return LocalCache.users.values.filter { it.isFollowing(user) && !account.isHidden(it) } + return LocalCache.userListCache.filter { it.isFollowing(user) && !account.isHidden(it) } } override fun limit() = 400 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNewThreadFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNewThreadFeedFilter.kt index 04dac8a9d..9acff0d9b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNewThreadFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNewThreadFeedFilter.kt @@ -41,7 +41,7 @@ class UserProfileNewThreadFeedFilter(val user: User, val account: Account) : } override fun feed(): List { - val notes = innerApplyFilter(LocalCache.notes.values) + val notes = innerApplyFilter(LocalCache.noteListCache) val longFormNotes = innerApplyFilter(LocalCache.addressables.values) return sort(notes + longFormNotes) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt index 721cb736e..17f3dc24e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt @@ -43,7 +43,7 @@ class VideoFeedFilter(val account: Account) : AdditiveFeedFilter() { } override fun feed(): List { - val notes = innerApplyFilter(LocalCache.notes.values) + val notes = innerApplyFilter(LocalCache.noteListCache) return sort(notes) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt index 3e314bfbc..c5dfcf639 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt @@ -979,11 +979,11 @@ fun debugState(context: Context) { Log.d( "STATE DUMP", "Notes: " + - LocalCache.notes.filter { it.value.liveSet != null }.size + + LocalCache.noteListCache.filter { it.liveSet != null }.size + " / " + - LocalCache.notes.filter { it.value.event != null }.size + + LocalCache.noteListCache.filter { it.event != null }.size + " / " + - LocalCache.notes.size, + LocalCache.noteListCache.size, ) Log.d( "STATE DUMP", @@ -997,21 +997,21 @@ fun debugState(context: Context) { Log.d( "STATE DUMP", "Users: " + - LocalCache.users.filter { it.value.liveSet != null }.size + + LocalCache.userListCache.filter { it.liveSet != null }.size + " / " + - LocalCache.users.filter { it.value.info?.latestMetadata != null }.size + + LocalCache.userListCache.filter { it.info?.latestMetadata != null }.size + " / " + - LocalCache.users.size, + LocalCache.userListCache.size, ) Log.d( "STATE DUMP", "Memory used by Events: " + - LocalCache.notes.values.sumOf { it.event?.countMemory() ?: 0 } / (1024 * 1024) + + LocalCache.noteListCache.sumOf { it.event?.countMemory() ?: 0 } / (1024 * 1024) + " MB", ) - LocalCache.notes.values + LocalCache.noteListCache .groupBy { it.event?.kind() } .forEach { Log.d("STATE DUMP", "Kind ${it.key}: \t${it.value.size} elements ") } LocalCache.addressables.values diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt index 5358eab70..0d396edf5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserReactionsRow.kt @@ -232,7 +232,7 @@ class UserReactionsViewModel(val account: Account) : ViewModel() { val replies = mutableMapOf() val takenIntoAccount = mutableSetOf() - LocalCache.notes.values.forEach { + LocalCache.noteListCache.forEach { val noteEvent = it.event if (noteEvent != null && !takenIntoAccount.contains(noteEvent.id())) { if (noteEvent is ReactionEvent) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt index d80b7f326..f35f36ff6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt @@ -179,7 +179,7 @@ open class CardFeedViewModel(val localFilter: FeedFilter) : ViewModel() { val event = (zapEvent.event as LnZapEvent) val author = event.zappedAuthor().firstNotNullOfOrNull { - LocalCache.users[it] // don't create user if it doesn't exist + LocalCache.getUserIfExists(it) // don't create user if it doesn't exist } if (author != null) { val zapRequest = author.zaps.filter { it.value == zapEvent }.keys.firstOrNull() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 1f53559a5..52329ea95 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -96,6 +96,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull import java.util.Locale import kotlin.coroutines.resume @@ -897,18 +898,22 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View return LocalCache.addressables[key] } - fun findStatusesForUser( + suspend fun findStatusesForUser( myUser: User, onResult: (ImmutableList) -> Unit, ) { - viewModelScope.launch(Dispatchers.IO) { onResult(LocalCache.findStatusesForUser(myUser)) } + withContext(Dispatchers.IO) { + onResult(LocalCache.findStatusesForUser(myUser)) + } } - fun findOtsEventsForNote( + suspend fun findOtsEventsForNote( note: Note, onResult: (Long?) -> Unit, ) { - viewModelScope.launch(Dispatchers.IO) { onResult(LocalCache.findEarliestOtsForNote(note)) } + withContext(Dispatchers.IO) { + onResult(LocalCache.findEarliestOtsForNote(note)) + } } private suspend fun checkGetOrCreateChannel(key: HexKey): Channel? { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Lud06.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Lud06.kt index c2cbc21b9..d4d2bb178 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Lud06.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Lud06.kt @@ -33,11 +33,14 @@ class Lud06 { val url = toLnUrlp(str) val matcher = LNURLP_PATTERN.matcher(url) - matcher.find() - val domain = matcher.group(2) - val username = matcher.group(3) + if (matcher.find()) { + val domain = matcher.group(2) + val username = matcher.group(3) - "$username@$domain" + "$username@$domain" + } else { + null + } } catch (t: Throwable) { t.printStackTrace() Log.w("Lud06ToLud16", "Fail to convert LUD06 to LUD16", t) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt index dc8461040..aba51ab92 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt @@ -207,6 +207,8 @@ open class Event( override fun isExpired() = (expiration() ?: Long.MAX_VALUE) < TimeUtils.now() + override fun isExpirationBefore(time: Long) = (expiration() ?: Long.MAX_VALUE) < time + override fun getTagOfAddressableKind(kind: Int): ATag? { val kindStr = kind.toString() val aTag = diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt index a847eae0b..55f0b4b2b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt @@ -137,5 +137,7 @@ interface EventInterface { fun isExpired(): Boolean + fun isExpirationBefore(time: Long): Boolean + fun hasZapSplitSetup(): Boolean } From e2d86c172739fdaa3e3dea52ca5d2d9cbd3d52b7 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 26 Feb 2024 20:52:15 -0500 Subject: [PATCH 170/176] Adds a new report option as other --- .../main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt | 1 + app/src/main/res/values/strings.xml | 1 + .../src/main/java/com/vitorpamplona/quartz/events/ReportEvent.kt | 1 + 3 files changed, 3 insertions(+) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index b9bc45ef3..35929b150 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -2364,6 +2364,7 @@ private fun RenderReport( ReportEvent.ReportType.SPAM -> stringResource(R.string.spam) ReportEvent.ReportType.IMPERSONATION -> stringResource(R.string.impersonation) ReportEvent.ReportType.ILLEGAL -> stringResource(R.string.illegal_behavior) + ReportEvent.ReportType.OTHER -> stringResource(R.string.other) } } .toSet() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b186de86e..5451e48e9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,6 +17,7 @@ Spam Impersonation Illegal Behavior + Other Unknown Relay Icon Unknown Author diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ReportEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ReportEvent.kt index 1b827917b..0535c639d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ReportEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ReportEvent.kt @@ -126,5 +126,6 @@ class ReportEvent( IMPERSONATION, NUDITY, PROFANITY, + OTHER, } } From 87fabd54de22cdc724bed2a4481c7361f6da25a2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 27 Feb 2024 10:34:49 -0500 Subject: [PATCH 171/176] Completed OTS Support --- .../amethyst/model/LocalCache.kt | 6 +-- .../amethyst/ui/note/NoteCompose.kt | 39 ++++++++++++------ .../amethyst/ui/note/TimeAgoFormatter.kt | 40 +++++++++++++++++++ .../amethyst/ui/screen/ThreadFeedView.kt | 3 ++ app/src/main/res/values/strings.xml | 3 ++ 5 files changed, 76 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 3b45fd55a..442ec4da3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -741,10 +741,10 @@ object LocalCache { // Already processed this event. if (version.event?.id() == event.id()) return - if (version.event == null) { - // makes sure the OTS has a valid certificate - if (event.cacheVerify() == null) return // no valid OTS + // makes sure the OTS has a valid certificate + if (event.cacheVerify() == null) return // no valid OTS + if (version.event == null) { version.loadEvent(event, author, emptyList()) version.liveSet?.innerOts?.invalidateData() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 35929b150..7725b9ead 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -241,6 +241,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.io.File import java.net.URL +import java.text.SimpleDateFormat +import java.util.Date import java.util.Locale @OptIn(ExperimentalFoundationApi::class) @@ -2560,15 +2562,27 @@ fun DisplayOts( LoadOts( note, accountViewModel, - whenConfirmed = { + whenConfirmed = { unixtimestamp -> val context = LocalContext.current - val timeStr by remember(note) { mutableStateOf(timeAgo(it, context = context)) } + val timeStr by remember(unixtimestamp) { mutableStateOf(timeAgoNoDot(unixtimestamp, context = context)) } - Text( - stringResource(id = R.string.existed_since, timeStr), - color = MaterialTheme.colorScheme.lessImportantLink, - fontSize = Font14SP, - fontWeight = FontWeight.Bold, + ClickableText( + text = buildAnnotatedString { append(stringResource(id = R.string.existed_since, timeStr)) }, + onClick = { + val fullDateTime = + SimpleDateFormat.getDateTimeInstance().format(Date(unixtimestamp * 1000)) + + accountViewModel.toast( + context.getString(R.string.ots_info_title), + context.getString(R.string.ots_info_description, fullDateTime), + ) + }, + style = + LocalTextStyle.current.copy( + color = MaterialTheme.colorScheme.lessImportantLink, + fontSize = Font14SP, + fontWeight = FontWeight.Bold, + ), maxLines = 1, ) }, @@ -2688,11 +2702,12 @@ fun LoadOts( LaunchedEffect(key1 = noteStatus) { accountViewModel.findOtsEventsForNote(noteStatus?.note ?: note) { newOts -> - if (newOts == null) { - earliestDate = GenericLoadable.Empty() - } else { - earliestDate = GenericLoadable.Loaded(newOts) - } + earliestDate = + if (newOts == null) { + GenericLoadable.Empty() + } else { + GenericLoadable.Loaded(newOts) + } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/TimeAgoFormatter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/TimeAgoFormatter.kt index aa381ac24..d2040b21a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/TimeAgoFormatter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/TimeAgoFormatter.kt @@ -71,6 +71,46 @@ fun timeAgo( } } +fun timeAgoNoDot( + time: Long?, + context: Context, +): String { + if (time == null) return " " + if (time == 0L) return " ${context.getString(R.string.never)}" + + val timeDifference = TimeUtils.now() - time + + return if (timeDifference > TimeUtils.ONE_YEAR) { + // Dec 12, 2022 + + if (locale != Locale.getDefault()) { + locale = Locale.getDefault() + yearFormatter = SimpleDateFormat("MMM dd, yyyy", locale) + monthFormatter = SimpleDateFormat("MMM dd", locale) + } + + yearFormatter.format(time * 1000) + } else if (timeDifference > TimeUtils.ONE_MONTH) { + // Dec 12 + if (locale != Locale.getDefault()) { + locale = Locale.getDefault() + yearFormatter = SimpleDateFormat("MMM dd, yyyy", locale) + monthFormatter = SimpleDateFormat("MMM dd", locale) + } + + monthFormatter.format(time * 1000) + } else if (timeDifference > TimeUtils.ONE_DAY) { + // 2 days + (timeDifference / TimeUtils.ONE_DAY).toString() + context.getString(R.string.d) + } else if (timeDifference > TimeUtils.ONE_HOUR) { + (timeDifference / TimeUtils.ONE_HOUR).toString() + context.getString(R.string.h) + } else if (timeDifference > TimeUtils.ONE_MINUTE) { + (timeDifference / TimeUtils.ONE_MINUTE).toString() + context.getString(R.string.m) + } else { + context.getString(R.string.now) + } +} + fun timeAgoShort( mills: Long?, stringForNow: String, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index 511a8caa4..2d3153de5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -104,6 +104,7 @@ import com.vitorpamplona.amethyst.ui.note.BlankNote import com.vitorpamplona.amethyst.ui.note.CreateImageHeader import com.vitorpamplona.amethyst.ui.note.DisplayHighlight import com.vitorpamplona.amethyst.ui.note.DisplayLocation +import com.vitorpamplona.amethyst.ui.note.DisplayOts import com.vitorpamplona.amethyst.ui.note.DisplayPeopleList import com.vitorpamplona.amethyst.ui.note.DisplayRelaySet import com.vitorpamplona.amethyst.ui.note.FileHeaderDisplay @@ -449,6 +450,8 @@ fun NoteMaster( if (pow > 20) { DisplayPoW(pow) } + + DisplayOts(note, accountViewModel) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5451e48e9..d36dbb21a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -787,4 +787,7 @@ Web: Clone: OTS: %1$s + + Timestamp Proof + There\'s proof this post was signed sometime before %1$s. The proof was stamped in the Bitcoin blockchain at that date and time. From 9639514a8247750e1ea45eca783589091ccd55a5 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Tue, 27 Feb 2024 15:37:25 +0000 Subject: [PATCH 172/176] New Crowdin translations by GitHub Action --- app/src/main/res/values-fr/strings.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index ea3522c88..6dd5a6e5d 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -16,6 +16,7 @@ Spam Falsification d\'identité Comportement illégal + Autre inconnu Icône de relais Auteur inconnu @@ -23,6 +24,9 @@ Copier la clé publique de l\'utilisateur Copier l\'ID de la note Diffuser + Horodater + Horodatage : Confirmations en attente + OTS : En attente Demande de suppression Bloquer / Signaler @@ -44,6 +48,7 @@ Boost boosté Citation + Fork Nouveau montant en Sats Ajouter "répondre à " @@ -658,7 +663,9 @@ Limite maximale Écritures limitées Forké depuis + FORK Dépôt Git : %1$s Web : Clone : + OTS: %1$s From f78a252c8fcdc24b2f8696018b1d91fdea598ca4 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 27 Feb 2024 15:39:15 -0500 Subject: [PATCH 173/176] - Adds nembed - Redesigns NIP-19 returns to allow nembeds and give more nuance to the UI. --- .../amethyst/model/LocalCache.kt | 21 +- .../NostrSearchEventOrUserDataSource.kt | 168 ++++++++----- .../vitorpamplona/amethyst/ui/MainActivity.kt | 27 ++- .../amethyst/ui/actions/NewMessageTagger.kt | 78 +++--- .../amethyst/ui/components/ClickableRoute.kt | 129 ++++++---- .../amethyst/ui/components/MarkdownParser.kt | 71 ++++-- .../amethyst/ui/components/RichTextViewer.kt | 57 ++--- .../ui/navigation/AccountSwitchBottomSheet.kt | 22 +- .../amethyst/ui/note/UpdateZapAmountDialog.kt | 10 +- .../amethyst/ui/qrcode/QrCodeScanner.kt | 18 +- .../ui/screen/AccountStateViewModel.kt | 16 +- .../ui/screen/loggedIn/AccountViewModel.kt | 47 +++- .../amethyst/NewMessageTaggerKeyParseTest.kt | 41 ++-- .../vitorpamplona/quartz/NIP19EmbedTests.kt | 68 +----- .../quartz/encoders/Bech32Util.kt | 12 - .../quartz/encoders/Nip19Bech32.kt | 229 +++++++++++++----- .../com/vitorpamplona/quartz/encoders/Tlv.kt | 2 + .../quartz/events/BaseTextNoteEvent.kt | 18 +- .../quartz/events/ContactListEvent.kt | 39 +-- .../ots/StreamSerializationContext.java | 8 +- .../quartz/signers/NostrSignerExternal.kt | 15 +- .../quartz/encoders/NIP19ParserTest.kt | 71 +++--- .../quartz/encoders/Nip19Bech32Test.kt | 60 +++-- 23 files changed, 710 insertions(+), 517 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 442ec4da3..83638986e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -27,12 +27,10 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.ui.components.BundledInsert import com.vitorpamplona.quartz.encoders.ATag -import com.vitorpamplona.quartz.encoders.Hex import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexValidator -import com.vitorpamplona.quartz.encoders.Nip19Bech32 +import com.vitorpamplona.quartz.encoders.decodeEventIdAsHexOrNull import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull -import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.events.AddressableEvent import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent import com.vitorpamplona.quartz.events.AppDefinitionEvent @@ -1578,13 +1576,7 @@ object LocalCache { fun findNotesStartingWith(text: String): List { checkNotInMainThread() - val key = - try { - Nip19Bech32.uriToRoute(text)?.hex ?: Hex.decode(text).toHexKey() - } catch (e: Exception) { - if (e is CancellationException) throw e - null - } + val key = decodeEventIdAsHexOrNull(text) if (key != null) { val note = getNoteIfExists(key) @@ -1631,14 +1623,7 @@ object LocalCache { fun findChannelsStartingWith(text: String): List { checkNotInMainThread() - val key = - try { - Nip19Bech32.uriToRoute(text)?.hex ?: Hex.decode(text).toHexKey() - } catch (e: Exception) { - if (e is CancellationException) throw e - null - } - + val key = decodeEventIdAsHexOrNull(text) if (key != null && channels[key] != null) { return listOfNotNull(channels[key]) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt index 0f5800906..e7c2d66da 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt @@ -24,9 +24,12 @@ import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter +import com.vitorpamplona.quartz.crypto.KeyPair +import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.Hex import com.vitorpamplona.quartz.encoders.HexValidator import com.vitorpamplona.quartz.encoders.Nip19Bech32 +import com.vitorpamplona.quartz.encoders.bechToBytes import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.events.AudioHeaderEvent import com.vitorpamplona.quartz.events.AudioTrackEvent @@ -66,83 +69,118 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") { null } - Nip19Bech32.uriToRoute(mySearchString)?.hex ?: isAStraightHex + when (val parsed = Nip19Bech32.uriToRoute(mySearchString)?.entity) { + is Nip19Bech32.NSec -> KeyPair(privKey = parsed.hex.bechToBytes()).pubKey.toHexKey() + is Nip19Bech32.NPub -> parsed.hex + is Nip19Bech32.NProfile -> parsed.hex + is Nip19Bech32.Note -> parsed.hex + is Nip19Bech32.NEvent -> parsed.hex + is Nip19Bech32.NEmbed -> parsed.event.id + is Nip19Bech32.NRelay -> null + is Nip19Bech32.NAddress -> parsed.atag + else -> isAStraightHex + } } catch (e: Exception) { if (e is CancellationException) throw e null } - // downloads all the reactions to a given event. - return listOfNotNull( + val directReferenceFilters = hexToWatch?.let { - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - JsonFilter( - ids = listOfNotNull(hexToWatch), + if (it.contains(":")) { + // naddr + listOfNotNull( + ATag.parse(it, null)?.let { aTag -> + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + JsonFilter( + kinds = listOf(MetadataEvent.KIND, aTag.kind), + authors = listOfNotNull(aTag.pubKeyHex), + // just to be sure + limit = 5, + ), + ) + }, + ) + } else { + // event ids + listOf( + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + JsonFilter( + ids = listOfNotNull(hexToWatch), + ), ), - ) - }, - hexToWatch?.let { + // authors + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + JsonFilter( + kinds = listOf(MetadataEvent.KIND), + authors = listOfNotNull(hexToWatch), + // just to be sure + limit = 5, + ), + ), + ) + } + } ?: emptyList() + + // downloads all the reactions to a given event. + return directReferenceFilters + + listOfNotNull( TypedFilter( - types = COMMON_FEED_TYPES, + types = setOf(FeedType.SEARCH), filter = JsonFilter( kinds = listOf(MetadataEvent.KIND), - authors = listOfNotNull(hexToWatch), + search = mySearchString, + limit = 100, ), - ) - }, - TypedFilter( - types = setOf(FeedType.SEARCH), - filter = - JsonFilter( - kinds = listOf(MetadataEvent.KIND), - search = mySearchString, - limit = 100, - ), - ), - TypedFilter( - types = setOf(FeedType.SEARCH), - filter = - JsonFilter( - kinds = - listOf( - TextNoteEvent.KIND, - LongTextNoteEvent.KIND, - BadgeDefinitionEvent.KIND, - PeopleListEvent.KIND, - BookmarkListEvent.KIND, - AudioHeaderEvent.KIND, - AudioTrackEvent.KIND, - PinListEvent.KIND, - PollNoteEvent.KIND, - ChannelCreateEvent.KIND, - ), - search = mySearchString, - limit = 100, - ), - ), - TypedFilter( - types = setOf(FeedType.SEARCH), - filter = - JsonFilter( - kinds = - listOf( - ChannelMetadataEvent.KIND, - ClassifiedsEvent.KIND, - CommunityDefinitionEvent.KIND, - EmojiPackEvent.KIND, - HighlightEvent.KIND, - LiveActivitiesEvent.KIND, - PollNoteEvent.KIND, - NNSEvent.KIND, - ), - search = mySearchString, - limit = 100, - ), - ), - ) + ), + TypedFilter( + types = setOf(FeedType.SEARCH), + filter = + JsonFilter( + kinds = + listOf( + TextNoteEvent.KIND, + LongTextNoteEvent.KIND, + BadgeDefinitionEvent.KIND, + PeopleListEvent.KIND, + BookmarkListEvent.KIND, + AudioHeaderEvent.KIND, + AudioTrackEvent.KIND, + PinListEvent.KIND, + PollNoteEvent.KIND, + ChannelCreateEvent.KIND, + ), + search = mySearchString, + limit = 100, + ), + ), + TypedFilter( + types = setOf(FeedType.SEARCH), + filter = + JsonFilter( + kinds = + listOf( + ChannelMetadataEvent.KIND, + ClassifiedsEvent.KIND, + CommunityDefinitionEvent.KIND, + EmojiPackEvent.KIND, + HighlightEvent.KIND, + LiveActivitiesEvent.KIND, + PollNoteEvent.KIND, + NNSEvent.KIND, + ), + search = mySearchString, + limit = 100, + ), + ), + ) } val searchChannel = requestNewChannel() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index d5bd039aa..6497e217e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -45,6 +45,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.google.accompanist.adaptive.calculateDisplayFeatures import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.ServiceManager +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.HttpClientManager import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils @@ -311,11 +312,12 @@ fun uriToRoute(uri: String?): String? { if (uri?.startsWith("nostr:Hashtag?id=") == true) { Route.Hashtag.route.replace("{id}", uri.removePrefix("nostr:Hashtag?id=")) } else { - val nip19 = Nip19Bech32.uriToRoute(uri) - when (nip19?.type) { - Nip19Bech32.Type.USER -> "User/${nip19.hex}" - Nip19Bech32.Type.NOTE -> "Note/${nip19.hex}" - Nip19Bech32.Type.EVENT -> { + val nip19 = Nip19Bech32.uriToRoute(uri)?.entity + when (nip19) { + is Nip19Bech32.NPub -> "User/${nip19.hex}" + is Nip19Bech32.NProfile -> "User/${nip19.hex}" + is Nip19Bech32.Note -> "Note/${nip19.hex}" + is Nip19Bech32.NEvent -> { if (nip19.kind == PrivateDmEvent.KIND) { nip19.author?.let { "RoomByAuthor/$it" } } else if ( @@ -328,14 +330,21 @@ fun uriToRoute(uri: String?): String? { "Event/${nip19.hex}" } } - Nip19Bech32.Type.ADDRESS -> + is Nip19Bech32.NAddress -> { if (nip19.kind == CommunityDefinitionEvent.KIND) { - "Community/${nip19.hex}" + "Community/${nip19.atag}" } else if (nip19.kind == LiveActivitiesEvent.KIND) { - "Channel/${nip19.hex}" + "Channel/${nip19.atag}" } else { - "Event/${nip19.hex}" + "Event/${nip19.atag}" } + } + is Nip19Bech32.NEmbed -> { + if (LocalCache.getNoteIfExists(nip19.event.id) == null) { + LocalCache.verifyAndConsume(nip19.event, null) + } + "Event/${nip19.event.id}" + } else -> null } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt index c05e661b5..f7a3b5366 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt @@ -68,17 +68,23 @@ class NewMessageTagger( paragraph.split(' ').forEach { word: String -> val results = parseDirtyWordForKey(word) - if (results?.key?.type == Nip19Bech32.Type.USER) { - addUserToMentions(dao.getOrCreateUser(results.key.hex)) - } else if (results?.key?.type == Nip19Bech32.Type.NOTE) { - addNoteToReplyTos(dao.getOrCreateNote(results.key.hex)) - } else if (results?.key?.type == Nip19Bech32.Type.EVENT) { - addNoteToReplyTos(dao.getOrCreateNote(results.key.hex)) - } else if (results?.key?.type == Nip19Bech32.Type.ADDRESS) { - val note = dao.checkGetOrCreateAddressableNote(results.key.hex) - if (note != null) { - addNoteToReplyTos(note) + when (val entity = results?.key?.entity) { + is Nip19Bech32.NPub -> addUserToMentions(dao.getOrCreateUser(entity.hex)) + is Nip19Bech32.NProfile -> addUserToMentions(dao.getOrCreateUser(entity.hex)) + + is Nip19Bech32.Note -> addNoteToReplyTos(dao.getOrCreateNote(entity.hex)) + is Nip19Bech32.NEvent -> addNoteToReplyTos(dao.getOrCreateNote(entity.hex)) + is Nip19Bech32.NEmbed -> addNoteToReplyTos(dao.getOrCreateNote(entity.event.id)) + + is Nip19Bech32.NAddress -> { + val note = dao.checkGetOrCreateAddressableNote(entity.atag) + if (note != null) { + addNoteToReplyTos(note) + } } + + is Nip19Bech32.NSec -> {} + is Nip19Bech32.NRelay -> {} } } } @@ -92,27 +98,45 @@ class NewMessageTagger( .split(' ') .map { word: String -> val results = parseDirtyWordForKey(word) - if (results?.key?.type == Nip19Bech32.Type.USER) { - val user = dao.getOrCreateUser(results.key.hex) + when (val entity = results?.key?.entity) { + is Nip19Bech32.NPub -> { + getNostrAddress(dao.getOrCreateUser(entity.hex).pubkeyNpub(), results.restOfWord) + } + is Nip19Bech32.NProfile -> { + getNostrAddress(dao.getOrCreateUser(entity.hex).pubkeyNpub(), results.restOfWord) + } - getNostrAddress(user.pubkeyNpub(), results.restOfWord) - } else if (results?.key?.type == Nip19Bech32.Type.NOTE) { - val note = dao.getOrCreateNote(results.key.hex) + is Nip19Bech32.Note -> { + getNostrAddress(dao.getOrCreateNote(entity.hex).toNEvent(), results.restOfWord) + } + is Nip19Bech32.NEvent -> { + getNostrAddress(dao.getOrCreateNote(entity.hex).toNEvent(), results.restOfWord) + } - getNostrAddress(note.toNEvent(), results.restOfWord) - } else if (results?.key?.type == Nip19Bech32.Type.EVENT) { - val note = dao.getOrCreateNote(results.key.hex) + is Nip19Bech32.NAddress -> { + val note = dao.checkGetOrCreateAddressableNote(entity.atag) + if (note != null) { + getNostrAddress(note.idNote(), results.restOfWord) + } else { + word + } + } - getNostrAddress(note.toNEvent(), results.restOfWord) - } else if (results?.key?.type == Nip19Bech32.Type.ADDRESS) { - val note = dao.checkGetOrCreateAddressableNote(results.key.hex) - if (note != null) { - getNostrAddress(note.idNote(), results.restOfWord) - } else { + is Nip19Bech32.NEmbed -> { + word + } + + is Nip19Bech32.NSec -> { + word + } + + is Nip19Bech32.NRelay -> { + word + } + + else -> { word } - } else { - word } } .joinToString(" ") @@ -135,7 +159,7 @@ class NewMessageTagger( } } - @Immutable data class DirtyKeyInfo(val key: Nip19Bech32.Return, val restOfWord: String) + @Immutable data class DirtyKeyInfo(val key: Nip19Bech32.ParseReturn, val restOfWord: String) fun parseDirtyWordForKey(mightBeAKey: String): DirtyKeyInfo? { var key = mightBeAKey diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt index 107f40c7d..f5743f460 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt @@ -61,9 +61,11 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.note.LoadChannel import com.vitorpamplona.amethyst.ui.note.toShortenHex import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.Nip19Bech32 import com.vitorpamplona.quartz.encoders.Nip30CustomEmoji import com.vitorpamplona.quartz.events.ChannelCreateEvent +import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.ImmutableListOfLists import com.vitorpamplona.quartz.events.PrivateDmEvent import com.vitorpamplona.quartz.events.toImmutableListOfLists @@ -72,45 +74,63 @@ import kotlinx.collections.immutable.ImmutableMap @Composable fun ClickableRoute( - nip19: Nip19Bech32.Return, + word: String, + nip19: Nip19Bech32.ParseReturn, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - when (nip19.type) { - Nip19Bech32.Type.USER -> { - DisplayUser(nip19, accountViewModel, nav) + when (val entity = nip19.entity) { + is Nip19Bech32.NPub -> DisplayUser(entity.hex, nip19.additionalChars, accountViewModel, nav) + is Nip19Bech32.NProfile -> DisplayUser(entity.hex, nip19.additionalChars, accountViewModel, nav) + is Nip19Bech32.Note -> DisplayEvent(entity.hex, null, nip19.additionalChars, accountViewModel, nav) + is Nip19Bech32.NEvent -> DisplayEvent(entity.hex, entity.kind, nip19.additionalChars, accountViewModel, nav) + is Nip19Bech32.NEmbed -> LoadAndDisplayEvent(entity.event, nip19.additionalChars, accountViewModel, nav) + is Nip19Bech32.NAddress -> DisplayAddress(entity, nip19.additionalChars, accountViewModel, nav) + is Nip19Bech32.NRelay -> { + Text(word) } - Nip19Bech32.Type.ADDRESS -> { - DisplayAddress(nip19, accountViewModel, nav) - } - Nip19Bech32.Type.NOTE -> { - DisplayNote(nip19, accountViewModel, nav) - } - Nip19Bech32.Type.EVENT -> { - DisplayEvent(nip19, accountViewModel, nav) + is Nip19Bech32.NSec -> { + Text(word) } else -> { - Text( - remember { "@${nip19.hex}${nip19.additionalChars}" }, - ) + Text(word) } } } @Composable -private fun DisplayEvent( - nip19: Nip19Bech32.Return, +fun LoadOrCreateNote( + event: Event, + accountViewModel: AccountViewModel, + content: @Composable (Note?) -> Unit, +) { + var note by + remember(event.id) { mutableStateOf(accountViewModel.getNoteIfExists(event.id)) } + + if (note == null) { + LaunchedEffect(key1 = event.id) { + accountViewModel.checkGetOrCreateNote(event) { note = it } + } + } + + content(note) +} + +@Composable +private fun LoadAndDisplayEvent( + event: Event, + additionalChars: String, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - LoadNote(nip19.hex, accountViewModel) { + LoadOrCreateNote(event, accountViewModel) { if (it != null) { - DisplayNoteLink(it, nip19, accountViewModel, nav) + DisplayNoteLink(it, event.id, event.kind, additionalChars, accountViewModel, nav) } else { CreateClickableText( - clickablePart = remember(nip19) { "@${nip19.hex.toShortenHex()}" }, - suffix = nip19.additionalChars, - route = remember(nip19) { "Event/${nip19.hex}" }, + clickablePart = remember(event.id) { "@${event.toNIP19()}" }, + suffix = additionalChars, + route = remember(event.id) { "Event/${event.id}" }, nav = nav, ) } @@ -118,19 +138,21 @@ private fun DisplayEvent( } @Composable -private fun DisplayNote( - nip19: Nip19Bech32.Return, +private fun DisplayEvent( + hex: HexKey, + kind: Int?, + additionalChars: String, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - LoadNote(nip19.hex, accountViewModel = accountViewModel) { + LoadNote(hex, accountViewModel) { if (it != null) { - DisplayNoteLink(it, nip19, accountViewModel, nav) + DisplayNoteLink(it, hex, kind, additionalChars, accountViewModel, nav) } else { CreateClickableText( - clickablePart = remember(nip19) { "@${nip19.hex.toShortenHex()}" }, - suffix = nip19.additionalChars, - route = remember(nip19) { "Event/${nip19.hex}" }, + clickablePart = remember(hex) { "@${hex.toShortenHex()}" }, + suffix = additionalChars, + route = remember(hex) { "Event/$hex" }, nav = nav, ) } @@ -140,7 +162,9 @@ private fun DisplayNote( @Composable private fun DisplayNoteLink( it: Note, - nip19: Nip19Bech32.Return, + hex: HexKey, + kind: Int?, + addedCharts: String, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { @@ -150,21 +174,20 @@ private fun DisplayNoteLink( val channelHex = remember(noteState) { note.channelHex() } val noteIdDisplayNote = remember(noteState) { "@${note.idDisplayNote()}" } - val addedCharts = nip19.additionalChars - if (note.event is ChannelCreateEvent || nip19.kind == ChannelCreateEvent.KIND) { + if (note.event is ChannelCreateEvent || kind == ChannelCreateEvent.KIND) { CreateClickableText( clickablePart = noteIdDisplayNote, suffix = addedCharts, - route = remember(noteState) { "Channel/${nip19.hex}" }, + route = remember(noteState) { "Channel/$hex" }, nav = nav, ) - } else if (note.event is PrivateDmEvent || nip19.kind == PrivateDmEvent.KIND) { + } else if (note.event is PrivateDmEvent || kind == PrivateDmEvent.KIND) { CreateClickableText( clickablePart = noteIdDisplayNote, suffix = addedCharts, route = - remember(noteState) { (note.author?.pubkeyHex ?: nip19.hex).let { "RoomByAuthor/$it" } }, + remember(noteState) { (note.author?.pubkeyHex ?: hex).let { "RoomByAuthor/$it" } }, nav = nav, ) } else if (channelHex != null) { @@ -186,7 +209,7 @@ private fun DisplayNoteLink( CreateClickableText( clickablePart = noteIdDisplayNote, suffix = addedCharts, - route = remember(noteState) { "Event/${nip19.hex}" }, + route = remember(noteState) { "Event/$hex" }, nav = nav, ) } @@ -194,27 +217,28 @@ private fun DisplayNoteLink( @Composable private fun DisplayAddress( - nip19: Nip19Bech32.Return, + nip19: Nip19Bech32.NAddress, + additionalChars: String, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - var noteBase by remember(nip19) { mutableStateOf(accountViewModel.getNoteIfExists(nip19.hex)) } + var noteBase by remember(nip19) { mutableStateOf(accountViewModel.getNoteIfExists(nip19.atag)) } if (noteBase == null) { - LaunchedEffect(key1 = nip19.hex) { - accountViewModel.checkGetOrCreateAddressableNote(nip19.hex) { noteBase = it } + LaunchedEffect(key1 = nip19.atag) { + accountViewModel.checkGetOrCreateAddressableNote(nip19.atag) { noteBase = it } } } noteBase?.let { val noteState by it.live().metadata.observeAsState() - val route = remember(noteState) { "Note/${nip19.hex}" } + val route = remember(noteState) { "Note/${nip19.atag}" } val displayName = remember(noteState) { "@${noteState?.note?.idDisplayNote()}" } CreateClickableText( clickablePart = displayName, - suffix = nip19.additionalChars, + suffix = additionalChars, route = route, nav = nav, ) @@ -222,35 +246,36 @@ private fun DisplayAddress( if (noteBase == null) { Text( - remember { "@${nip19.hex}${nip19.additionalChars}" }, + remember { "@${nip19.atag}$additionalChars" }, ) } } @Composable private fun DisplayUser( - nip19: Nip19Bech32.Return, + userHex: HexKey, + additionalChars: String, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { var userBase by - remember(nip19) { + remember(userHex) { mutableStateOf( - accountViewModel.getUserIfExists(nip19.hex), + accountViewModel.getUserIfExists(userHex), ) } if (userBase == null) { - LaunchedEffect(key1 = nip19.hex) { - accountViewModel.checkGetOrCreateUser(nip19.hex) { userBase = it } + LaunchedEffect(key1 = userHex) { + accountViewModel.checkGetOrCreateUser(userHex) { userBase = it } } } - userBase?.let { RenderUserAsClickableText(it, nip19, nav) } + userBase?.let { RenderUserAsClickableText(it, additionalChars, nav) } if (userBase == null) { Text( - remember { "@${nip19.hex}${nip19.additionalChars}" }, + remember { "@${userHex}$additionalChars" }, ) } } @@ -258,7 +283,7 @@ private fun DisplayUser( @Composable private fun RenderUserAsClickableText( baseUser: User, - nip19: Nip19Bech32.Return, + additionalChars: String, nav: (String) -> Unit, ) { val userState by baseUser.live().metadata.observeAsState() @@ -281,7 +306,7 @@ private fun RenderUserAsClickableText( tags = userTags, ) - nip19.additionalChars.ifBlank { null }?.let { + additionalChars.ifBlank { null }?.let { Text(text = it, maxLines = 1) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/MarkdownParser.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/MarkdownParser.kt index 4fe9d706b..8ed1e6e49 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/MarkdownParser.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/MarkdownParser.kt @@ -25,6 +25,7 @@ import android.util.Patterns import com.vitorpamplona.amethyst.commons.RichTextParser import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.checkNotInMainThread +import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.Nip19Bech32 import com.vitorpamplona.quartz.events.ImmutableListOfLists import kotlinx.coroutines.CancellationException @@ -64,58 +65,80 @@ class MarkdownParser { return null } - private fun getDisplayNameFromNip19(nip19: Nip19Bech32.Return): Pair? { - if (nip19.type == Nip19Bech32.Type.USER) { - LocalCache.getUserIfExists(nip19.hex)?.let { - return Pair(it.toBestDisplayName(), it.pubkeyNpub()) + private suspend fun getDisplayNameFromNip19(nip19: Nip19Bech32.Entity): Pair? { + return when (nip19) { + is Nip19Bech32.NSec -> null + is Nip19Bech32.NPub -> { + LocalCache.getUserIfExists(nip19.hex)?.let { + return Pair(it.toBestDisplayName(), it.pubkeyNpub()) + } } - } else if (nip19.type == Nip19Bech32.Type.NOTE) { - LocalCache.getNoteIfExists(nip19.hex)?.let { - return Pair(it.idDisplayNote(), it.toNEvent()) + is Nip19Bech32.NProfile -> { + LocalCache.getUserIfExists(nip19.hex)?.let { + return Pair(it.toBestDisplayName(), it.pubkeyNpub()) + } } - } else if (nip19.type == Nip19Bech32.Type.ADDRESS) { - LocalCache.getAddressableNoteIfExists(nip19.hex)?.let { - return Pair(it.idDisplayNote(), it.toNEvent()) + is Nip19Bech32.Note -> { + LocalCache.getNoteIfExists(nip19.hex)?.let { + return Pair(it.idDisplayNote(), it.toNEvent()) + } } - } else if (nip19.type == Nip19Bech32.Type.EVENT) { - LocalCache.getNoteIfExists(nip19.hex)?.let { - return Pair(it.idDisplayNote(), it.toNEvent()) + is Nip19Bech32.NEvent -> { + LocalCache.getNoteIfExists(nip19.hex)?.let { + return Pair(it.idDisplayNote(), it.toNEvent()) + } } - } + is Nip19Bech32.NEmbed -> { + if (LocalCache.getNoteIfExists(nip19.event.id) == null) { + LocalCache.verifyAndConsume(nip19.event, null) + } - return null + LocalCache.getNoteIfExists(nip19.event.id)?.let { + return Pair(it.idDisplayNote(), it.toNEvent()) + } + } + is Nip19Bech32.NRelay -> null + is Nip19Bech32.NAddress -> { + LocalCache.getAddressableNoteIfExists(nip19.atag)?.let { + return Pair(it.idDisplayNote(), it.toNEvent()) + } + } + else -> null + } } fun returnNIP19References( content: String, tags: ImmutableListOfLists?, - ): List { + ): List { checkNotInMainThread() - val listOfReferences = mutableListOf() + val listOfReferences = mutableListOf() content.split('\n').forEach { paragraph -> paragraph.split(' ').forEach { word: String -> if (RichTextParser.startsWithNIP19Scheme(word)) { val parsedNip19 = Nip19Bech32.uriToRoute(word) - parsedNip19?.let { listOfReferences.add(it) } + parsedNip19?.let { listOfReferences.add(it.entity) } } } } tags?.lists?.forEach { if (it[0] == "p" && it.size > 1) { - listOfReferences.add(Nip19Bech32.Return(Nip19Bech32.Type.USER, it[1], null, null, null, "")) + listOfReferences.add(Nip19Bech32.NProfile(it[1], listOfNotNull(it.getOrNull(2)))) } else if (it[0] == "e" && it.size > 1) { - listOfReferences.add(Nip19Bech32.Return(Nip19Bech32.Type.NOTE, it[1], null, null, null, "")) + listOfReferences.add(Nip19Bech32.NEvent(it[1], listOfNotNull(it.getOrNull(2)), null, null)) } else if (it[0] == "a" && it.size > 1) { - listOfReferences.add(Nip19Bech32.Return(Nip19Bech32.Type.ADDRESS, it[1], null, null, null, "")) + ATag.parseAtag(it[1], it.getOrNull(2))?.let { atag -> + listOfReferences.add(Nip19Bech32.NAddress(it[1], listOfNotNull(atag.relay), atag.pubKeyHex, atag.kind)) + } } } return listOfReferences } - fun returnMarkdownWithSpecialContent( + suspend fun returnMarkdownWithSpecialContent( content: String, tags: ImmutableListOfLists?, ): String { @@ -135,8 +158,8 @@ class MarkdownParser { } else if (RichTextParser.startsWithNIP19Scheme(word)) { val parsedNip19 = Nip19Bech32.uriToRoute(word) returnContent += - if (parsedNip19 !== null) { - val pair = getDisplayNameFromNip19(parsedNip19) + if (parsedNip19?.entity !== null) { + val pair = getDisplayNameFromNip19(parsedNip19.entity) if (pair != null) { val (displayName, nip19) = pair "[$displayName](nostr:$nip19) " diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 33fb34fef..e6d8a1fef 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -106,6 +106,7 @@ import com.vitorpamplona.amethyst.ui.theme.innerPostModifier import com.vitorpamplona.amethyst.ui.theme.markdownStyle import com.vitorpamplona.amethyst.ui.uriToRoute import com.vitorpamplona.quartz.crypto.KeyPair +import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.Nip19Bech32 import com.vitorpamplona.quartz.events.EmptyTagList import com.vitorpamplona.quartz.events.ImmutableListOfLists @@ -507,9 +508,7 @@ private fun RefreshableContent( var markdownWithSpecialContent by remember(content) { mutableStateOf(content) } ObserverAllNIP19References(content, tags, accountViewModel) { - accountViewModel.returnMarkdownWithSpecialContent(content, tags) { - newMarkdownWithSpecialContent, - -> + accountViewModel.returnMarkdownWithSpecialContent(content, tags) { newMarkdownWithSpecialContent -> if (markdownWithSpecialContent != newMarkdownWithSpecialContent) { markdownWithSpecialContent = newMarkdownWithSpecialContent } @@ -526,7 +525,7 @@ fun ObserverAllNIP19References( accountViewModel: AccountViewModel, onRefresh: () -> Unit, ) { - var nip19References by remember(content) { mutableStateOf>(emptyList()) } + var nip19References by remember(content) { mutableStateOf>(emptyList()) } LaunchedEffect(key1 = content) { accountViewModel.returnNIP19References(content, tags) { @@ -540,33 +539,37 @@ fun ObserverAllNIP19References( @Composable fun ObserveNIP19( - it: Nip19Bech32.Return, + entity: Nip19Bech32.Entity, accountViewModel: AccountViewModel, onRefresh: () -> Unit, ) { - if (it.type == Nip19Bech32.Type.NOTE || it.type == Nip19Bech32.Type.EVENT || it.type == Nip19Bech32.Type.ADDRESS) { - ObserveNIP19Event(it, accountViewModel, onRefresh) - } else if (it.type == Nip19Bech32.Type.USER) { - ObserveNIP19User(it, accountViewModel, onRefresh) + when (val parsed = entity) { + is Nip19Bech32.NPub -> ObserveNIP19User(parsed.hex, accountViewModel, onRefresh) + is Nip19Bech32.NProfile -> ObserveNIP19User(parsed.hex, accountViewModel, onRefresh) + + is Nip19Bech32.Note -> ObserveNIP19Event(parsed.hex, accountViewModel, onRefresh) + is Nip19Bech32.NEvent -> ObserveNIP19Event(parsed.hex, accountViewModel, onRefresh) + is Nip19Bech32.NEmbed -> ObserveNIP19Event(parsed.event.id, accountViewModel, onRefresh) + + is Nip19Bech32.NAddress -> ObserveNIP19Event(parsed.atag, accountViewModel, onRefresh) + + is Nip19Bech32.NSec -> {} + is Nip19Bech32.NRelay -> {} } } @Composable private fun ObserveNIP19Event( - it: Nip19Bech32.Return, + hex: HexKey, accountViewModel: AccountViewModel, onRefresh: () -> Unit, ) { - var baseNote by remember(it) { mutableStateOf(accountViewModel.getNoteIfExists(it.hex)) } + var baseNote by remember(hex) { mutableStateOf(accountViewModel.getNoteIfExists(hex)) } if (baseNote == null) { - LaunchedEffect(key1 = it.hex) { - if ( - it.type == Nip19Bech32.Type.NOTE || it.type == Nip19Bech32.Type.EVENT || it.type == Nip19Bech32.Type.ADDRESS - ) { - accountViewModel.checkGetOrCreateNote(it.hex) { note -> - launch(Dispatchers.Main) { baseNote = note } - } + LaunchedEffect(key1 = hex) { + accountViewModel.checkGetOrCreateNote(hex) { note -> + launch(Dispatchers.Main) { baseNote = note } } } } @@ -590,18 +593,16 @@ fun ObserveNote( @Composable private fun ObserveNIP19User( - it: Nip19Bech32.Return, + hex: HexKey, accountViewModel: AccountViewModel, onRefresh: () -> Unit, ) { - var baseUser by remember(it) { mutableStateOf(accountViewModel.getUserIfExists(it.hex)) } + var baseUser by remember(hex) { mutableStateOf(accountViewModel.getUserIfExists(hex)) } if (baseUser == null) { - LaunchedEffect(key1 = it.hex) { - if (it.type == Nip19Bech32.Type.USER) { - accountViewModel.checkGetOrCreateUser(it.hex)?.let { user -> - launch(Dispatchers.Main) { baseUser = user } - } + LaunchedEffect(key1 = hex) { + accountViewModel.checkGetOrCreateUser(hex)?.let { user -> + launch(Dispatchers.Main) { baseUser = user } } } } @@ -634,7 +635,9 @@ fun BechLink( var loadedLink by remember { mutableStateOf(null) } if (loadedLink == null) { - LaunchedEffect(key1 = word) { accountViewModel.parseNIP19(word) { loadedLink = it } } + LaunchedEffect(key1 = word) { + accountViewModel.parseNIP19(word) { loadedLink = it } + } } if (canPreview && loadedLink?.baseNote != null) { @@ -648,7 +651,7 @@ fun BechLink( ) } } else if (loadedLink?.nip19 != null) { - ClickableRoute(loadedLink?.nip19!!, accountViewModel, nav) + ClickableRoute(word, loadedLink?.nip19!!, accountViewModel, nav) } else { val text = remember(word) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt index e5ad82775..e7f17c92a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt @@ -77,10 +77,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginOrSignupScreen import com.vitorpamplona.amethyst.ui.theme.AccountPictureModifier import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size55dp -import com.vitorpamplona.quartz.encoders.decodePublicKey -import com.vitorpamplona.quartz.encoders.toHexKey +import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull import com.vitorpamplona.quartz.events.toImmutableListOfLists -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -149,12 +147,9 @@ fun DisplayAccount( ) { var baseUser by remember { mutableStateOf( - LocalCache.getUserIfExists( - decodePublicKey( - acc.npub, - ) - .toHexKey(), - ), + decodePublicKeyAsHexOrNull(acc.npub)?.let { + LocalCache.getUserIfExists(it) + }, ) } @@ -162,13 +157,8 @@ fun DisplayAccount( LaunchedEffect(key1 = acc.npub) { launch(Dispatchers.IO) { baseUser = - try { - LocalCache.getOrCreateUser( - decodePublicKey(acc.npub).toHexKey(), - ) - } catch (e: Exception) { - if (e is CancellationException) throw e - null + decodePublicKeyAsHexOrNull(acc.npub)?.let { + LocalCache.getOrCreateUser(it) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt index eef2fff55..e81d9bee4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt @@ -99,6 +99,7 @@ import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.encoders.Nip47WalletConnect +import com.vitorpamplona.quartz.encoders.decodePrivateKeyAsHexOrNull import com.vitorpamplona.quartz.encoders.decodePublicKey import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.events.LnZapEvent @@ -165,14 +166,7 @@ class UpdateZapAmountViewModel(val account: Account) : ViewModel() { addedWSS } - val unverifiedPrivKey = walletConnectSecret.text.ifBlank { null } - val privKeyHex = - try { - unverifiedPrivKey?.let { decodePublicKey(it).toHexKey() } - } catch (e: Exception) { - if (e is CancellationException) throw e - null - } + val privKeyHex = walletConnectSecret.text.ifBlank { null }?.let { decodePrivateKeyAsHexOrNull(it) } if (pubkeyHex != null) { account?.changeZapPaymentRequest( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeScanner.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeScanner.kt index fe92d7a2d..f79e09099 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeScanner.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeScanner.kt @@ -29,28 +29,14 @@ import com.google.zxing.client.android.Intents import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanOptions import com.vitorpamplona.amethyst.R -import com.vitorpamplona.quartz.encoders.Nip19Bech32 +import com.vitorpamplona.amethyst.ui.uriToRoute import kotlinx.coroutines.CancellationException @Composable fun NIP19QrCodeScanner(onScan: (String?) -> Unit) { SimpleQrCodeScanner { try { - val nip19 = Nip19Bech32.uriToRoute(it) - val startingPage = - when (nip19?.type) { - Nip19Bech32.Type.USER -> "User/${nip19.hex}" - Nip19Bech32.Type.NOTE -> "Note/${nip19.hex}" - Nip19Bech32.Type.EVENT -> "Event/${nip19.hex}" - Nip19Bech32.Type.ADDRESS -> "Note/${nip19.hex}" - else -> null - } - - if (startingPage != null) { - onScan(startingPage) - } else { - onScan(null) - } + onScan(uriToRoute(it)) } catch (e: Throwable) { if (e is CancellationException) throw e Log.e("NIP19 Scanner", "Error parsing $it", e) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index aae445d40..a68d895d7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -85,8 +85,20 @@ class AccountStateViewModel() : ViewModel() { loginWithExternalSigner: Boolean = false, packageName: String = "", ) = withContext(Dispatchers.IO) { - val parsed = Nip19Bech32.uriToRoute(key) - val pubKeyParsed = parsed?.hex?.hexToByteArray() + val parsed = Nip19Bech32.uriToRoute(key)?.entity + val pubKeyParsed = + when (parsed) { + is Nip19Bech32.NSec -> null + is Nip19Bech32.NPub -> parsed.hex.hexToByteArray() + is Nip19Bech32.NProfile -> parsed.hex.hexToByteArray() + is Nip19Bech32.Note -> null + is Nip19Bech32.NEvent -> null + is Nip19Bech32.NEmbed -> null + is Nip19Bech32.NRelay -> null + is Nip19Bech32.NAddress -> null + else -> null + } + val proxy = HttpClientManager.initProxy(useProxy, "127.0.0.1", proxyPort) if (loginWithExternalSigner && pubKeyParsed == null) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 52329ea95..6b6969f78 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -868,6 +868,22 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View viewModelScope.launch(Dispatchers.IO) { onResult(checkGetOrCreateNote(key)) } } + fun checkGetOrCreateNote( + event: Event, + onResult: (Note?) -> Unit, + ) { + viewModelScope.launch(Dispatchers.IO) { + var note = checkGetOrCreateNote(event.id) + + if (note == null) { + LocalCache.verifyAndConsume(event, null) + note = checkGetOrCreateNote(event.id) + } + + onResult(note) + } + } + fun getNoteIfExists(hex: HexKey): Note? { return LocalCache.getNoteIfExists(hex) } @@ -970,7 +986,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View fun returnNIP19References( content: String, tags: ImmutableListOfLists?, - onNewReferences: (List) -> Unit, + onNewReferences: (List) -> Unit, ) { viewModelScope.launch(Dispatchers.IO) { onNewReferences(MarkdownParser().returnNIP19References(content, tags)) @@ -987,17 +1003,32 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View } } - fun parseNIP19( + suspend fun parseNIP19( str: String, onNote: (LoadedBechLink) -> Unit, ) { - viewModelScope.launch(Dispatchers.IO) { + withContext(Dispatchers.IO) { Nip19Bech32.uriToRoute(str)?.let { var returningNote: Note? = null - if ( - it.type == Nip19Bech32.Type.NOTE || it.type == Nip19Bech32.Type.EVENT || it.type == Nip19Bech32.Type.ADDRESS - ) { - LocalCache.checkGetOrCreateNote(it.hex)?.let { note -> returningNote = note } + + when (val parsed = it.entity) { + is Nip19Bech32.NSec -> {} + is Nip19Bech32.NPub -> {} + is Nip19Bech32.NProfile -> {} + is Nip19Bech32.Note -> LocalCache.checkGetOrCreateNote(parsed.hex)?.let { note -> returningNote = note } + is Nip19Bech32.NEvent -> LocalCache.checkGetOrCreateNote(parsed.hex)?.let { note -> returningNote = note } + is Nip19Bech32.NEmbed -> { + if (LocalCache.getNoteIfExists(parsed.event.id) == null) { + LocalCache.verifyAndConsume(parsed.event, null) + } + + LocalCache.checkGetOrCreateNote(parsed.event.id)?.let { note -> + returningNote = note + } + } + is Nip19Bech32.NRelay -> {} + is Nip19Bech32.NAddress -> LocalCache.checkGetOrCreateNote(parsed.atag)?.let { note -> returningNote = note } + else -> {} } onNote(LoadedBechLink(returningNote, it)) @@ -1298,7 +1329,7 @@ class HasNotificationDot(bottomNavigationItems: ImmutableList) { } } -@Immutable data class LoadedBechLink(val baseNote: Note?, val nip19: Nip19Bech32.Return) +@Immutable data class LoadedBechLink(val baseNote: Note?, val nip19: Nip19Bech32.ParseReturn) public fun allOrNothingSigningOperations( remainingTos: List, diff --git a/app/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt b/app/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt index a0216ffa5..43645c380 100644 --- a/app/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt +++ b/app/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.ui.actions.Dao import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.quartz.encoders.Nip19Bech32 import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue import org.junit.Test /** @@ -54,10 +55,10 @@ class NewMessageTaggerKeyParseTest { val result = NewMessageTagger(message = "", dao = dao) .parseDirtyWordForKey("note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn") - assertEquals(Nip19Bech32.Type.NOTE, result?.key?.type) + assertTrue(result?.key?.entity is Nip19Bech32.Note) assertEquals( "1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", - result?.key?.hex, + (result?.key?.entity as? Nip19Bech32.Note)?.hex, ) assertEquals("", result?.restOfWord) } @@ -67,10 +68,10 @@ class NewMessageTaggerKeyParseTest { val result = NewMessageTagger(message = "", dao = dao) .parseDirtyWordForKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z") - assertEquals(Nip19Bech32.Type.USER, result?.key?.type) + assertTrue(result?.key?.entity is Nip19Bech32.NPub) assertEquals( "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", - result?.key?.hex, + (result?.key?.entity as? Nip19Bech32.NPub)?.hex, ) assertEquals("", result?.restOfWord) } @@ -80,10 +81,10 @@ class NewMessageTaggerKeyParseTest { val result = NewMessageTagger(message = "", dao = dao) .parseDirtyWordForKey("note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn,") - assertEquals(Nip19Bech32.Type.NOTE, result?.key?.type) + assertTrue(result?.key?.entity is Nip19Bech32.Note) assertEquals( "1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", - result?.key?.hex, + (result?.key?.entity as? Nip19Bech32.Note)?.hex, ) assertEquals(",", result?.restOfWord) } @@ -93,10 +94,10 @@ class NewMessageTaggerKeyParseTest { val result = NewMessageTagger(message = "", dao = dao) .parseDirtyWordForKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z,") - assertEquals(Nip19Bech32.Type.USER, result?.key?.type) + assertTrue(result?.key?.entity is Nip19Bech32.NPub) assertEquals( "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", - result?.key?.hex, + (result?.key?.entity as? Nip19Bech32.NPub)?.hex, ) assertEquals(",", result?.restOfWord) } @@ -106,10 +107,10 @@ class NewMessageTaggerKeyParseTest { val result = NewMessageTagger(message = "", dao = dao) .parseDirtyWordForKey("@note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn,") - assertEquals(Nip19Bech32.Type.NOTE, result?.key?.type) + assertTrue(result?.key?.entity is Nip19Bech32.Note) assertEquals( "1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", - result?.key?.hex, + (result?.key?.entity as? Nip19Bech32.Note)?.hex, ) assertEquals(",", result?.restOfWord) } @@ -119,10 +120,10 @@ class NewMessageTaggerKeyParseTest { val result = NewMessageTagger(message = "", dao = dao) .parseDirtyWordForKey("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z,") - assertEquals(Nip19Bech32.Type.USER, result?.key?.type) + assertTrue(result?.key?.entity is Nip19Bech32.NPub) assertEquals( "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", - result?.key?.hex, + (result?.key?.entity as? Nip19Bech32.NPub)?.hex, ) assertEquals(",", result?.restOfWord) } @@ -134,10 +135,10 @@ class NewMessageTaggerKeyParseTest { .parseDirtyWordForKey( "nostr:note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn,", ) - assertEquals(Nip19Bech32.Type.NOTE, result?.key?.type) + assertTrue(result?.key?.entity is Nip19Bech32.Note) assertEquals( "1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", - result?.key?.hex, + (result?.key?.entity as? Nip19Bech32.Note)?.hex, ) assertEquals(",", result?.restOfWord) } @@ -149,10 +150,10 @@ class NewMessageTaggerKeyParseTest { .parseDirtyWordForKey( "nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z,", ) - assertEquals(Nip19Bech32.Type.USER, result?.key?.type) + assertTrue(result?.key?.entity is Nip19Bech32.NPub) assertEquals( "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", - result?.key?.hex, + (result?.key?.entity as? Nip19Bech32.NPub)?.hex, ) assertEquals(",", result?.restOfWord) } @@ -164,10 +165,10 @@ class NewMessageTaggerKeyParseTest { .parseDirtyWordForKey( "Nostr:note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn,", ) - assertEquals(Nip19Bech32.Type.NOTE, result?.key?.type) + assertTrue(result?.key?.entity is Nip19Bech32.Note) assertEquals( "1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", - result?.key?.hex, + (result?.key?.entity as? Nip19Bech32.Note)?.hex, ) assertEquals(",", result?.restOfWord) } @@ -179,10 +180,10 @@ class NewMessageTaggerKeyParseTest { .parseDirtyWordForKey( "nOstr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z,", ) - assertEquals(Nip19Bech32.Type.USER, result?.key?.type) + assertTrue(result?.key?.entity is Nip19Bech32.NPub) assertEquals( "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", - result?.key?.hex, + (result?.key?.entity as? Nip19Bech32.NPub)?.hex, ) assertEquals(",", result?.restOfWord) } diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP19EmbedTests.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP19EmbedTests.kt index 973b2fe5e..4e9506987 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP19EmbedTests.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP19EmbedTests.kt @@ -22,9 +22,8 @@ package com.vitorpamplona.quartz import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.crypto.KeyPair -import com.vitorpamplona.quartz.encoders.Bech32 import com.vitorpamplona.quartz.encoders.Hex -import com.vitorpamplona.quartz.encoders.bechToBytes +import com.vitorpamplona.quartz.encoders.Nip19Bech32 import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.FhirResourceEvent import com.vitorpamplona.quartz.events.TextNoteEvent @@ -35,17 +34,13 @@ import junit.framework.TestCase.assertTrue import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith -import java.io.ByteArrayOutputStream import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit -import java.util.zip.GZIPInputStream -import java.util.zip.GZIPOutputStream -import kotlin.time.measureTimedValue @RunWith(AndroidJUnit4::class) class NIP19EmbedTests { @Test - fun testEmbedEvent() { + fun testEmbedKind1Event() { val signer = NostrSignerInternal( KeyPair(Hex.decode("e8e7197ccc53c9ed4cf9b1c8dce085475fa1ffdd71f2c14e44fe23d0bdf77598")), @@ -64,12 +59,11 @@ class NIP19EmbedTests { assertNotNull(textNote) - val bech32 = Bech32.encodeBytes(hrp = "n", textNote!!.toJson().toByteArray(Charsets.UTF_8), Bech32.Encoding.Bech32) + val bech32 = Nip19Bech32.createNEmbed(textNote!!) println(bech32) - val decoded = String(bech32.bechToBytes()) - val decodedNote = Event.fromJson(decoded) + val decodedNote = (Nip19Bech32.uriToRoute(bech32)?.entity as Nip19Bech32.NEmbed).event assertTrue(decodedNote.hasValidSignature()) @@ -96,52 +90,19 @@ class NIP19EmbedTests { assertNotNull(eyeglassesPrescriptionEvent) - val bech32 = Bech32.encodeBytes(hrp = "nembed", eyeglassesPrescriptionEvent!!.toJson().toByteArray(Charsets.UTF_8), Bech32.Encoding.Bech32) + val bech32 = Nip19Bech32.createNEmbed(eyeglassesPrescriptionEvent!!) println(eyeglassesPrescriptionEvent!!.toJson()) println(bech32) - val decoded = String(bech32.bechToBytes()) - val decodedNote = Event.fromJson(decoded) - - assertTrue(decodedNote.hasValidSignature()) - - assertEquals(eyeglassesPrescriptionEvent!!.toJson(), decodedNote.toJson()) - } - - @Test - fun testVisionPrescriptionEmbedEventCompression() { - val signer = - NostrSignerInternal( - KeyPair(Hex.decode("e8e7197ccc53c9ed4cf9b1c8dce085475fa1ffdd71f2c14e44fe23d0bdf77598")), - ) - - var eyeglassesPrescriptionEvent: Event? = null - - val countDownLatch = CountDownLatch(1) - - FhirResourceEvent.create(fhirPayload = visionPrescriptionFhir, signer = signer) { - eyeglassesPrescriptionEvent = it - countDownLatch.countDown() - } - - Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) - - assertNotNull(eyeglassesPrescriptionEvent) - - val bech32 = Bech32.encodeBytes(hrp = "nembed", gzip(eyeglassesPrescriptionEvent!!.toJson()), Bech32.Encoding.Bech32) - - println(eyeglassesPrescriptionEvent!!.toJson()) - println(bech32) - - val decoded = ungzip(bech32.bechToBytes()) - val decodedNote = Event.fromJson(decoded) + val decodedNote = (Nip19Bech32.uriToRoute(bech32)?.entity as Nip19Bech32.NEmbed).event assertTrue(decodedNote.hasValidSignature()) assertEquals(eyeglassesPrescriptionEvent!!.toJson(), decodedNote.toJson()) } + /* @Test fun testCompressionSizes() { val signer = @@ -167,24 +128,15 @@ class NIP19EmbedTests { Bech32.encodeBytes(hrp = "nembed", json.toByteArray(), Bech32.Encoding.Bech32) Bech32.encodeBytes(hrp = "nembed", gzip(json), Bech32.Encoding.Bech32) - val (bech32json, elapsedjson) = measureTimedValue { Bech32.encodeBytes(hrp = "nembed", json.toByteArray(), Bech32.Encoding.Bech32) } - val (bech32gzip, elapsedgzip) = measureTimedValue { Bech32.encodeBytes(hrp = "nembed", gzip(json), Bech32.Encoding.Bech32) } + val (bech32json, elapsedjson) = measureTimedValue { json.toByteArray().toNEmbed() } + val (bech32gzip, elapsedgzip) = measureTimedValue { gzip(json).toNEmbed() } println("Raw JSON ${json.length} chars") println("Bech32 JSON ${bech32json.length} chars in $elapsedjson") println("Bech32 GZIP ${bech32gzip.length} chars in $elapsedgzip") assertTrue(true) - } + }*/ val visionPrescriptionFhir = "{\"resourceType\":\"VisionPrescription\",\"status\":\"active\",\"created\":\"2014-06-15\",\"patient\":{\"reference\":\"Patient/example\"},\"dateWritten\":\"2014-06-15\",\"prescriber\":{\"reference\":\"Practitioner/example\"},\"lensSpecification\":[{\"eye\":\"right\",\"sphere\":-2,\"prism\":[{\"amount\":0.5,\"base\":\"down\"}],\"add\":2},{\"eye\":\"left\",\"sphere\":-1,\"cylinder\":-0.5,\"axis\":180,\"prism\":[{\"amount\":0.5,\"base\":\"up\"}],\"add\":2}]}" } - -fun gzip(content: String): ByteArray { - val bos = ByteArrayOutputStream() - GZIPOutputStream(bos).bufferedWriter(Charsets.UTF_8).use { it.write(content) } - val array = bos.toByteArray() - return array -} - -fun ungzip(content: ByteArray): String = GZIPInputStream(content.inputStream()).bufferedReader(Charsets.UTF_8).use { it.readText() } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Bech32Util.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Bech32Util.kt index a327bd131..f3ce3903c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Bech32Util.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Bech32Util.kt @@ -271,18 +271,6 @@ object Bech32 { } } -fun ByteArray.toNsec() = Bech32.encodeBytes(hrp = "nsec", this, Bech32.Encoding.Bech32) - -fun ByteArray.toNpub() = Bech32.encodeBytes(hrp = "npub", this, Bech32.Encoding.Bech32) - -fun ByteArray.toNote() = Bech32.encodeBytes(hrp = "note", this, Bech32.Encoding.Bech32) - -fun ByteArray.toNEvent() = Bech32.encodeBytes(hrp = "nevent", this, Bech32.Encoding.Bech32) - -fun ByteArray.toNAddress() = Bech32.encodeBytes(hrp = "naddr", this, Bech32.Encoding.Bech32) - -fun ByteArray.toLnUrl() = Bech32.encodeBytes(hrp = "lnurl", this, Bech32.Encoding.Bech32) - fun String.bechToBytes(hrp: String? = null): ByteArray { val decodedForm = Bech32.decodeBytes(this) hrp?.also { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19Bech32.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19Bech32.kt index 225553069..0516f1916 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19Bech32.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Nip19Bech32.kt @@ -23,7 +23,12 @@ package com.vitorpamplona.quartz.encoders import android.util.Log import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.crypto.KeyPair +import com.vitorpamplona.quartz.events.Event +import kotlinx.coroutines.CancellationException +import java.io.ByteArrayOutputStream import java.util.regex.Pattern +import java.util.zip.GZIPInputStream +import java.util.zip.GZIPOutputStream object Nip19Bech32 { enum class Type { @@ -43,21 +48,40 @@ object Nip19Bech32 { val nip19regex = Pattern.compile( - "(nostr:)?@?(nsec1|npub1|nevent1|naddr1|note1|nprofile1|nrelay1)([qpzry9x8gf2tvdw0s3jn54khce6mua7l]+)([\\S]*)", + "(nostr:)?@?(nsec1|npub1|nevent1|naddr1|note1|nprofile1|nrelay1|nembed1)([qpzry9x8gf2tvdw0s3jn54khce6mua7l]+)([\\S]*)", Pattern.CASE_INSENSITIVE, ) @Immutable - data class Return( - val type: Type, - val hex: String, - val relay: String? = null, - val author: String? = null, - val kind: Int? = null, - val additionalChars: String = "", - ) + data class ParseReturn(val entity: Entity, val additionalChars: String = "") - fun uriToRoute(uri: String?): Return? { + interface Entity + + @Immutable + data class NSec(val hex: String) : Entity + + @Immutable + data class NPub(val hex: String) : Entity + + @Immutable + data class Note(val hex: String) : Entity + + @Immutable + data class NProfile(val hex: String, val relay: List) : Entity + + @Immutable + data class NEvent(val hex: String, val relay: List, val author: String?, val kind: Int?) : Entity + + @Immutable + data class NAddress(val atag: String, val relay: List, val author: String, val kind: Int) : Entity + + @Immutable + data class NRelay(val relay: List) : Entity + + @Immutable + data class NEmbed(val event: Event) : Entity + + fun uriToRoute(uri: String?): ParseReturn? { if (uri == null) return null try { @@ -66,12 +90,11 @@ object Nip19Bech32 { return null } - val uriScheme = matcher.group(1) // nostr: val type = matcher.group(2) // npub1 val key = matcher.group(3) // bech32 val additionalChars = matcher.group(4) // additional chars - return parseComponents(uriScheme, type, key, additionalChars) + return parseComponents(type!!, key, additionalChars) } catch (e: Throwable) { Log.e("NIP19 Parser", "Issue trying to Decode NIP19 $uri: ${e.message}", e) } @@ -80,28 +103,24 @@ object Nip19Bech32 { } fun parseComponents( - uriScheme: String?, type: String, key: String?, additionalChars: String?, - ): Return? { + ): ParseReturn? { return try { val bytes = (type + key).bechToBytes() - val parsed = - when (type.lowercase()) { - "npub1" -> npub(bytes) - "note1" -> note(bytes) - "nprofile1" -> nprofile(bytes) - "nevent1" -> nevent(bytes) - "nrelay1" -> nrelay(bytes) - "naddr1" -> naddr(bytes) - else -> null - } - if (parsed?.hex?.isBlank() == true) { - null - } else { - parsed?.copy(additionalChars = additionalChars ?: "") + when (type.lowercase()) { + "npub1" -> npub(bytes) + "note1" -> note(bytes) + "nprofile1" -> nprofile(bytes) + "nevent1" -> nevent(bytes) + "nrelay1" -> nrelay(bytes) + "naddr1" -> naddr(bytes) + "nembed1" -> nembed(bytes) + else -> null + }?.let { + ParseReturn(it, additionalChars ?: "") } } catch (e: Throwable) { Log.w("NIP19 Parser", "Issue trying to Decode NIP19 $key: ${e.message}", e) @@ -109,52 +128,71 @@ object Nip19Bech32 { } } - private fun npub(bytes: ByteArray): Return { - return Return(Type.USER, bytes.toHexKey()) + private fun nembed(bytes: ByteArray): NEmbed? { + if (bytes.isEmpty()) return null + return NEmbed(Event.fromJson(ungzip(bytes))) } - private fun note(bytes: ByteArray): Return { - return Return(Type.NOTE, bytes.toHexKey()) + private fun npub(bytes: ByteArray): NPub? { + if (bytes.isEmpty()) return null + return NPub(bytes.toHexKey()) } - private fun nprofile(bytes: ByteArray): Return? { + private fun note(bytes: ByteArray): Note? { + if (bytes.isEmpty()) return null + return Note(bytes.toHexKey()) + } + + private fun nprofile(bytes: ByteArray): NProfile? { + if (bytes.isEmpty()) return null + val tlv = Tlv.parse(bytes) val hex = tlv.firstAsHex(TlvTypes.SPECIAL) ?: return null - val relay = tlv.firstAsString(TlvTypes.RELAY) + val relay = tlv.asStringList(TlvTypes.RELAY) ?: emptyList() - return Return(Type.USER, hex, relay) + if (hex.isBlank()) return null + + return NProfile(hex, relay) } - private fun nevent(bytes: ByteArray): Return? { + private fun nevent(bytes: ByteArray): NEvent? { + if (bytes.isEmpty()) return null + val tlv = Tlv.parse(bytes) val hex = tlv.firstAsHex(TlvTypes.SPECIAL) ?: return null - val relay = tlv.firstAsString(TlvTypes.RELAY) + val relay = tlv.asStringList(TlvTypes.RELAY) ?: emptyList() val author = tlv.firstAsHex(TlvTypes.AUTHOR) val kind = tlv.firstAsInt(TlvTypes.KIND.id) - return Return(Type.EVENT, hex, relay, author, kind) + if (hex.isBlank()) return null + + return NEvent(hex, relay, author, kind) } - private fun nrelay(bytes: ByteArray): Return? { - val relayUrl = Tlv.parse(bytes).firstAsString(TlvTypes.SPECIAL.id) ?: return null + private fun nrelay(bytes: ByteArray): NRelay? { + if (bytes.isEmpty()) return null - return Return(Type.RELAY, relayUrl) + val relayUrl = Tlv.parse(bytes).asStringList(TlvTypes.SPECIAL.id) ?: return null + + return NRelay(relayUrl) } - private fun naddr(bytes: ByteArray): Return? { + private fun naddr(bytes: ByteArray): NAddress? { + if (bytes.isEmpty()) return null + val tlv = Tlv.parse(bytes) val d = tlv.firstAsString(TlvTypes.SPECIAL.id) ?: "" - val relay = tlv.firstAsString(TlvTypes.RELAY.id) + val relay = tlv.asStringList(TlvTypes.RELAY.id) ?: emptyList() val author = tlv.firstAsHex(TlvTypes.AUTHOR.id) ?: return null val kind = tlv.firstAsInt(TlvTypes.KIND.id) ?: return null - return Return(Type.ADDRESS, "$kind:$author:$d", relay, author, kind) + return NAddress("$kind:$author:$d", relay, author, kind) } - public fun createNEvent( + fun createNEvent( idHex: String, author: String?, kind: Int?, @@ -170,34 +208,97 @@ object Nip19Bech32 { .build() .toNEvent() } + + fun createNEmbed(event: Event): String { + return gzip(event.toJson()).toNEmbed() + } + + fun gzip(content: String): ByteArray { + val bos = ByteArrayOutputStream() + GZIPOutputStream(bos).bufferedWriter(Charsets.UTF_8).use { it.write(content) } + val array = bos.toByteArray() + return array + } + + fun ungzip(content: ByteArray): String = GZIPInputStream(content.inputStream()).bufferedReader(Charsets.UTF_8).use { it.readText() } } -fun decodePublicKey(key: String): ByteArray { - val parsed = Nip19Bech32.uriToRoute(key) - val pubKeyParsed = parsed?.hex?.hexToByteArray() +fun ByteArray.toNsec() = Bech32.encodeBytes(hrp = "nsec", this, Bech32.Encoding.Bech32) - return if (key.startsWith("nsec")) { - KeyPair(privKey = key.bechToBytes()).pubKey - } else if (pubKeyParsed != null) { - pubKeyParsed - } else { - Hex.decode(key) +fun ByteArray.toNpub() = Bech32.encodeBytes(hrp = "npub", this, Bech32.Encoding.Bech32) + +fun ByteArray.toNote() = Bech32.encodeBytes(hrp = "note", this, Bech32.Encoding.Bech32) + +fun ByteArray.toNEvent() = Bech32.encodeBytes(hrp = "nevent", this, Bech32.Encoding.Bech32) + +fun ByteArray.toNAddress() = Bech32.encodeBytes(hrp = "naddr", this, Bech32.Encoding.Bech32) + +fun ByteArray.toLnUrl() = Bech32.encodeBytes(hrp = "lnurl", this, Bech32.Encoding.Bech32) + +fun ByteArray.toNEmbed() = Bech32.encodeBytes(hrp = "nembed", this, Bech32.Encoding.Bech32) + +fun decodePublicKey(key: String): ByteArray { + return when (val parsed = Nip19Bech32.uriToRoute(key)?.entity) { + is Nip19Bech32.NSec -> KeyPair(privKey = key.bechToBytes()).pubKey + is Nip19Bech32.NPub -> parsed.hex.hexToByteArray() + is Nip19Bech32.NProfile -> parsed.hex.hexToByteArray() + else -> Hex.decode(key) // crashes on purpose + } +} + +fun decodePrivateKeyAsHexOrNull(key: String): HexKey? { + return try { + when (val parsed = Nip19Bech32.uriToRoute(key)?.entity) { + is Nip19Bech32.NSec -> parsed.hex + is Nip19Bech32.NPub -> null + is Nip19Bech32.NProfile -> null + is Nip19Bech32.Note -> null + is Nip19Bech32.NEvent -> null + is Nip19Bech32.NEmbed -> null + is Nip19Bech32.NRelay -> null + is Nip19Bech32.NAddress -> null + else -> Hex.decode(key).toHexKey() + } + } catch (e: Exception) { + if (e is CancellationException) throw e + null } } fun decodePublicKeyAsHexOrNull(key: String): HexKey? { return try { - val parsed = Nip19Bech32.uriToRoute(key) - val pubKeyParsed = parsed?.hex - - if (key.startsWith("nsec")) { - KeyPair(privKey = key.bechToBytes()).pubKey.toHexKey() - } else if (pubKeyParsed != null) { - pubKeyParsed - } else { - Hex.decode(key).toHexKey() + when (val parsed = Nip19Bech32.uriToRoute(key)?.entity) { + is Nip19Bech32.NSec -> KeyPair(privKey = key.bechToBytes()).pubKey.toHexKey() + is Nip19Bech32.NPub -> parsed.hex + is Nip19Bech32.NProfile -> parsed.hex + is Nip19Bech32.Note -> null + is Nip19Bech32.NEvent -> null + is Nip19Bech32.NEmbed -> null + is Nip19Bech32.NRelay -> null + is Nip19Bech32.NAddress -> null + else -> Hex.decode(key).toHexKey() } } catch (e: Exception) { + if (e is CancellationException) throw e + null + } +} + +fun decodeEventIdAsHexOrNull(key: String): HexKey? { + return try { + when (val parsed = Nip19Bech32.uriToRoute(key)?.entity) { + is Nip19Bech32.NSec -> null + is Nip19Bech32.NPub -> null + is Nip19Bech32.NProfile -> null + is Nip19Bech32.Note -> parsed.hex + is Nip19Bech32.NEvent -> parsed.hex + is Nip19Bech32.NAddress -> parsed.atag + is Nip19Bech32.NEmbed -> null + is Nip19Bech32.NRelay -> null + else -> Hex.decode(key).toHexKey() + } + } catch (e: Exception) { + if (e is CancellationException) throw e null } } @@ -237,3 +338,5 @@ fun Tlv.firstAsInt(type: Nip19Bech32.TlvTypes) = firstAsInt(type.id) fun Tlv.firstAsHex(type: Nip19Bech32.TlvTypes) = firstAsHex(type.id) fun Tlv.firstAsString(type: Nip19Bech32.TlvTypes) = firstAsString(type.id) + +fun Tlv.asStringList(type: Nip19Bech32.TlvTypes) = asStringList(type.id) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Tlv.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Tlv.kt index 3d452da55..81254a688 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Tlv.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Tlv.kt @@ -94,6 +94,8 @@ class Tlv(val data: Map>) { fun firstAsString(type: Byte) = data[type]?.firstOrNull()?.toString(Charsets.UTF_8) + fun asStringList(type: Byte) = data[type]?.map { it.toString(Charsets.UTF_8) } + companion object { fun parse(data: ByteArray): Tlv { val result = mutableMapOf>() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt index 7d387e918..26a522e49 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt @@ -100,16 +100,18 @@ open class BaseTextNoteEvent( val matcher2 = nip19regex.matcher(content) while (matcher2.find()) { - val uriScheme = matcher2.group(1) // nostr: val type = matcher2.group(2) // npub1 val key = matcher2.group(3) // bech32 val additionalChars = matcher2.group(4) // additional chars try { - val parsed = Nip19Bech32.parseComponents(uriScheme, type, key, additionalChars) + val parsed = Nip19Bech32.parseComponents(type, key, additionalChars)?.entity if (parsed != null) { - if (parsed.type == Nip19Bech32.Type.USER) { + if (parsed is Nip19Bech32.NProfile) { + returningList.add(parsed.hex) + } + if (parsed is Nip19Bech32.NPub) { returningList.add(parsed.hex) } } @@ -145,16 +147,18 @@ open class BaseTextNoteEvent( val matcher2 = nip19regex.matcher(content) while (matcher2.find()) { - val uriScheme = matcher2.group(1) // nostr: val type = matcher2.group(2) // npub1 val key = matcher2.group(3) // bech32 val additionalChars = matcher2.group(4) // additional chars - val parsed = Nip19Bech32.parseComponents(uriScheme, type, key, additionalChars) + val parsed = Nip19Bech32.parseComponents(type, key, additionalChars)?.entity if (parsed != null) { - if (parsed.type == Nip19Bech32.Type.EVENT || parsed.type == Nip19Bech32.Type.ADDRESS || parsed.type == Nip19Bech32.Type.NOTE) { - citations.add(parsed.hex) + when (parsed) { + is Nip19Bech32.NEvent -> citations.add(parsed.hex) + is Nip19Bech32.NAddress -> citations.add(parsed.atag) + is Nip19Bech32.Note -> citations.add(parsed.hex) + is Nip19Bech32.NEmbed -> citations.add(parsed.event.id) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ContactListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ContactListEvent.kt index 496fe4fd9..e57df43d8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ContactListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ContactListEvent.kt @@ -48,17 +48,18 @@ class ContactListEvent( @delegate:Transient val verifiedFollowKeySet: Set by lazy { - tags - .filter { it.size > 1 && it[0] == "p" } - .mapNotNull { - try { + tags.mapNotNullTo(HashSet()) { + try { + if (it.size > 1 && it[0] == "p") { decodePublicKey(it[1]).toHexKey() - } catch (e: Exception) { - Log.w("ContactListEvent", "Can't parse tags as a follows: ${it[1]}", e) + } else { null } + } catch (e: Exception) { + Log.w("ContactListEvent", "Can't parse tags as a follows: ${it[1]}", e) + null } - .toSet() + } } @delegate:Transient @@ -77,27 +78,29 @@ class ContactListEvent( @delegate:Transient val verifiedFollowKeySetAndMe: Set by lazy { verifiedFollowKeySet + pubKey } - fun unverifiedFollowKeySet() = tags.filter { it[0] == "p" }.mapNotNull { it.getOrNull(1) } + fun unverifiedFollowKeySet() = tags.filter { it.size > 1 && it[0] == "p" }.mapNotNull { it.getOrNull(1) } - fun unverifiedFollowTagSet() = tags.filter { it[0] == "t" }.mapNotNull { it.getOrNull(1) } + fun unverifiedFollowTagSet() = tags.filter { it.size > 1 && it[0] == "t" }.mapNotNull { it.getOrNull(1) } - fun unverifiedFollowGeohashSet() = tags.filter { it[0] == "g" }.mapNotNull { it.getOrNull(1) } + fun unverifiedFollowGeohashSet() = tags.filter { it.size > 1 && it[0] == "g" }.mapNotNull { it.getOrNull(1) } - fun unverifiedFollowAddressSet() = tags.filter { it[0] == "a" }.mapNotNull { it.getOrNull(1) } + fun unverifiedFollowAddressSet() = tags.filter { it.size > 1 && it[0] == "a" }.mapNotNull { it.getOrNull(1) } fun follows() = - tags - .filter { it[0] == "p" } - .mapNotNull { - try { + tags.mapNotNull { + try { + if (it.size > 1 && it[0] == "p") { Contact(decodePublicKey(it[1]).toHexKey(), it.getOrNull(2)) - } catch (e: Exception) { - Log.w("ContactListEvent", "Can't parse tags as a follows: ${it[1]}", e) + } else { null } + } catch (e: Exception) { + Log.w("ContactListEvent", "Can't parse tags as a follows: ${it[1]}", e) + null } + } - fun followsTags() = tags.filter { it[0] == "t" }.mapNotNull { it.getOrNull(2) } + fun followsTags() = tags.filter { it.size > 1 && it[0] == "t" }.mapNotNull { it.getOrNull(1) } fun relays(): Map? = try { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/StreamSerializationContext.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/StreamSerializationContext.java index dcdc34b44..87368bda8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/ots/StreamSerializationContext.java +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/StreamSerializationContext.java @@ -28,7 +28,7 @@ public class StreamSerializationContext { } public void writeBool(boolean value) { - if (value == true) { + if (value) { this.writeByte((byte) 0xff); } else { this.writeByte((byte) 0x00); @@ -58,7 +58,7 @@ public class StreamSerializationContext { } public void writeByte(byte value) { - this.buffer.add(new Byte(value)); + this.buffer.add(Byte.valueOf(value)); } public void writeByte(Byte value) { @@ -66,8 +66,8 @@ public class StreamSerializationContext { } public void writeBytes(byte[] value) { - for (int i = 0; i < value.length; i++) { - this.writeByte(value[i]); + for (byte b : value) { + this.writeByte(b); } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerExternal.kt b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerExternal.kt index 73a55cb8c..4f34af999 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerExternal.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerExternal.kt @@ -150,15 +150,12 @@ class NostrSignerExternal( event: LnZapRequestEvent, onReady: (LnZapPrivateEvent) -> Unit, ) { - return launcher.decryptZapEvent(event) { - val event = - try { - Event.fromJson(it) - } catch (e: Exception) { - Log.e("NostrExternalSigner", "Unable to parse returned decrypted Zap: $it") - null - } - (event as? LnZapPrivateEvent)?.let { onReady(event) } + return launcher.decryptZapEvent(event) { jsonEvent -> + try { + (Event.fromJson(jsonEvent) as? LnZapPrivateEvent)?.let { onReady(it) } + } catch (e: Exception) { + Log.e("NostrExternalSigner", "Unable to parse returned decrypted Zap: $jsonEvent") + } } } } diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/encoders/NIP19ParserTest.kt b/quartz/src/test/java/com/vitorpamplona/quartz/encoders/NIP19ParserTest.kt index e40e4f9c3..ee3fb557a 100644 --- a/quartz/src/test/java/com/vitorpamplona/quartz/encoders/NIP19ParserTest.kt +++ b/quartz/src/test/java/com/vitorpamplona/quartz/encoders/NIP19ParserTest.kt @@ -21,6 +21,8 @@ package com.vitorpamplona.quartz.encoders import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue import org.junit.Test class NIP19ParserTest { @@ -32,7 +34,7 @@ class NIP19ParserTest { ) assertEquals( "30023:460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c:", - result?.hex, + (result?.entity as? Nip19Bech32.NAddress)?.atag, ) } @@ -44,7 +46,7 @@ class NIP19ParserTest { ) assertEquals( "30023:d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c:guide-wireguard", - result?.hex, + (result?.entity as? Nip19Bech32.NAddress)?.atag, ) } @@ -54,12 +56,12 @@ class NIP19ParserTest { Nip19Bech32.uriToRoute( "naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38", ) - assertEquals(Nip19Bech32.Type.ADDRESS, result?.type) + assertTrue(result?.entity is Nip19Bech32.NAddress) assertEquals( "30023:d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193:89de7920", - result?.hex, + (result?.entity as? Nip19Bech32.NAddress)?.atag, ) - assertEquals("wss://relay.damus.io", result?.relay) + assertEquals("wss://relay.damus.io", (result?.entity as? Nip19Bech32.NAddress)?.relay?.get(0)) } @Test @@ -132,13 +134,14 @@ class NIP19ParserTest { val result = Nip19Bech32.uriToRoute( "naddr1qq2hs7p30p6kcunxxamkgcnyd33xxve3veshyq3qyujphdcz69z6jafxpnldae3xtymdekfeatkt3r4qusr3w5krqspqxpqqqpaxjlg805f", - ) - assertEquals(Nip19Bech32.Type.ADDRESS, result?.type) + )?.entity as? Nip19Bech32.NAddress + + assertNotNull(result) assertEquals( "31337:27241bb702d145a975260cfedee6265936dcd939eaecb88ea0e4071752c30402:xx1xulrf7wdbdlbc31far", - result?.hex, + result?.atag, ) - assertEquals(null, result?.relay) + assertEquals(true, result?.relay?.isEmpty()) assertEquals("27241bb702d145a975260cfedee6265936dcd939eaecb88ea0e4071752c30402", result?.author) assertEquals(31337, result?.kind) } @@ -148,13 +151,14 @@ class NIP19ParserTest { val result = Nip19Bech32.uriToRoute( "naddr1qpqrvvfnvccrzdryxgunzvtxvgukge34xfjnqdpcv9sk2desxgmrscesvserzd3h8ycrywphvg6nsvf58ycnqef3v5mnsvt98pjnqdfs8ypzq3huhccxt6h34eupz3jeynjgjgek8lel2f4adaea0svyk94a3njdqvzqqqr4gudhrkyk", - ) - assertEquals(Nip19Bech32.Type.ADDRESS, result?.type) + )?.entity as? Nip19Bech32.NAddress + + assertNotNull(result) assertEquals( "30023:46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d:613f014d2911fb9df52e048aae70268c0d216790287b5814910e1e781e8e0509", - result?.hex, + result?.atag, ) - assertEquals(null, result?.relay) + assertEquals(true, result?.relay?.isEmpty()) assertEquals("46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d", result?.author) assertEquals(30023, result?.kind) } @@ -164,13 +168,14 @@ class NIP19ParserTest { val result = Nip19Bech32.uriToRoute( "naddr1qq9rzd3h8y6nqwf5xyuqygzxljlrqe027xh8sy2xtyjwfzfrxcll8afxh4hh847psjckhkxwf5psgqqqw4rsty50fx", - ) - assertEquals(Nip19Bech32.Type.ADDRESS, result?.type) + )?.entity as? Nip19Bech32.NAddress + + assertNotNull(result) assertEquals( "30023:46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d:1679509418", - result?.hex, + result?.atag, ) - assertEquals(null, result?.relay) + assertEquals(true, result?.relay?.isEmpty()) assertEquals("46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d", result?.author) assertEquals(30023, result?.kind) } @@ -178,10 +183,11 @@ class NIP19ParserTest { @Test fun nEventParserTest() { val result = - Nip19Bech32.uriToRoute("nostr:nevent1qqs0tsw8hjacs4fppgdg7f5yhgwwfkyua4xcs3re9wwkpkk2qeu6mhql22rcy") - assertEquals(Nip19Bech32.Type.EVENT, result?.type) + Nip19Bech32.uriToRoute("nostr:nevent1qqs0tsw8hjacs4fppgdg7f5yhgwwfkyua4xcs3re9wwkpkk2qeu6mhql22rcy")?.entity as? Nip19Bech32.NEvent + + assertNotNull(result) assertEquals("f5c1c7bcbb8855210a1a8f2684ba1ce4d89ced4d8844792b9d60daca0679addc", result?.hex) - assertEquals(null, result?.relay) + assertEquals(true, result?.relay?.isEmpty()) assertEquals(null, result?.author) assertEquals(null, result?.kind) } @@ -191,10 +197,11 @@ class NIP19ParserTest { val result = Nip19Bech32.uriToRoute( "nostr:nevent1qqstvrl6wftd8ht4g0vrp6m30tjs6pdxcvk977g769dcvlptkzu4ftqppamhxue69uhkummnw3ezumt0d5pzp78lz8r60568sd2a8dx3wnj6gume02gxaf92vx4fk67qv5kpagt6qvzqqqqqqygqr86c", - ) - assertEquals(Nip19Bech32.Type.EVENT, result?.type) + )?.entity as? Nip19Bech32.NEvent + + assertNotNull(result) assertEquals("b60ffa7256d3dd7543d830eb717ae50d05a6c32c5f791ed15b867c2bb0b954ac", result?.hex) - assertEquals("wss://nostr.mom", result?.relay) + assertEquals("wss://nostr.mom", result?.relay?.get(0)) assertEquals("f8ff11c7a7d3478355d3b4d174e5a473797a906ea4aa61aa9b6bc0652c1ea17a", result?.author) assertEquals(1, result?.kind) } @@ -204,11 +211,11 @@ class NIP19ParserTest { val result = Nip19Bech32.uriToRoute( "nostr:nevent1qqsplpuwsgrrmq85rfup6w3w777rxmcmadu590emfx6z4msj2844euqpz3mhxue69uhhyetvv9ujuerpd46hxtnfdupzq3svyhng9ld8sv44950j957j9vchdktj7cxumsep9mvvjthc2pjuqvzqqqqqqye3a70w", - ) + )?.entity as? Nip19Bech32.NEvent - assertEquals(Nip19Bech32.Type.EVENT, result?.type) + assertNotNull(result) assertEquals("1f878e82063d80f41a781d3a2ef7bc336f1beb7942bf3b49b42aee1251eb5cf0", result?.hex) - assertEquals("wss://relay.damus.io", result?.relay) + assertEquals("wss://relay.damus.io", result?.relay?.get(0)) assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.author) assertEquals(1, result?.kind) } @@ -218,13 +225,13 @@ class NIP19ParserTest { val result = Nip19Bech32.uriToRoute( "nostr:nevent1qqsg6gechd3dhzx38n4z8a2lylzgsmmgeamhmtzz72m9ummsnf0xjfspsdmhxue69uhkummn9ekx7mpvwaehxw309ahx7um5wghx77r5wghxgetk93mhxue69uhhyetvv9ujumn0wd68ytnzvuk8wumn8ghj7mn0wd68ytn9d9h82mny0fmkzmn6d9njuumsv93k2trhwden5te0wfjkccte9ehx7um5wghxyctwvsk8wumn8ghj7un9d3shjtnyv9kh2uewd9hs3kqsdn", - ) + )?.entity as? Nip19Bech32.NEvent - assertEquals(Nip19Bech32.Type.EVENT, result?.type) + assertNotNull(result) assertEquals("8d2338bb62db88d13cea23f55f27c4886f68cf777dac42f2b65e6f709a5e6926", result?.hex) assertEquals( "wss://nos.lol,wss://nostr.oxtr.dev,wss://relay.nostr.bg,wss://nostr.einundzwanzig.space,wss://relay.nostr.band,wss://relay.damus.io", - result?.relay, + result?.relay?.joinToString(","), ) } @@ -233,11 +240,11 @@ class NIP19ParserTest { val result = Nip19Bech32.uriToRoute( "nostr:nevent1qqsyxq8v0730nz38dupnjzp5jegkyz4gu2ptwcps4v32hjnrap0q0espz3mhxue69uhhyetvv9ujuerpd46hxtnfdupzq3svyhng9ld8sv44950j957j9vchdktj7cxumsep9mvvjthc2pjuqvzqqqqqqyn3t9gj", - ) + )?.entity as? Nip19Bech32.NEvent - assertEquals(Nip19Bech32.Type.EVENT, result?.type) + assertNotNull(result) assertEquals("4300ec7fa2f98a276f033908349651620aa8e282b76030ab22abca63e85e07e6", result?.hex) - assertEquals("wss://relay.damus.io", result?.relay) + assertEquals("wss://relay.damus.io", result?.relay?.get(0)) assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.author) assertEquals(1, result?.kind) } diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/encoders/Nip19Bech32Test.kt b/quartz/src/test/java/com/vitorpamplona/quartz/encoders/Nip19Bech32Test.kt index f12cbdc4b..7d1cce5eb 100644 --- a/quartz/src/test/java/com/vitorpamplona/quartz/encoders/Nip19Bech32Test.kt +++ b/quartz/src/test/java/com/vitorpamplona/quartz/encoders/Nip19Bech32Test.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.encoders import org.junit.Assert -import org.junit.Ignore +import org.junit.Assert.assertNotNull import org.junit.Test class Nip19Bech32Test { @@ -44,58 +44,74 @@ class Nip19Bech32Test { val actual = Nip19Bech32.uriToRoute("nostr:npub1hv7k2s755n697sptva8vkh9jz40lzfzklnwj6ekewfmxp5crwdjs27007y") - Assert.assertEquals(Nip19Bech32.Type.USER, actual?.type) + Assert.assertTrue(actual?.entity is Nip19Bech32.NPub) Assert.assertEquals( "bb3d6543d4a4f45f402b674ecb5cb2155ff12456fcdd2d66d9727660d3037365", - actual?.hex, + (actual?.entity as? Nip19Bech32.NPub)?.hex, ) } @Test() fun uri_to_route_note() { - val actual = - Nip19Bech32.uriToRoute("nostr:note1stqea6wmwezg9x6yyr6qkukw95ewtdukyaztycws65l8wppjmtpscawevv") + val result = + Nip19Bech32.uriToRoute("nostr:note1stqea6wmwezg9x6yyr6qkukw95ewtdukyaztycws65l8wppjmtpscawevv")?.entity as? Nip19Bech32.Note - Assert.assertEquals(Nip19Bech32.Type.NOTE, actual?.type) + assertNotNull(result) Assert.assertEquals( "82c19ee9db7644829b4420f40b72ce2d32e5b7962744b261d0d53e770432dac3", - actual?.hex, + result?.hex, ) } - @Ignore("Test not implemented yet") @Test() fun uri_to_route_nprofile() { val actual = Nip19Bech32.uriToRoute("nostr:nprofile") - Assert.assertEquals(Nip19Bech32.Type.USER, actual?.type) - Assert.assertEquals("*", actual?.hex) + Assert.assertEquals(null, actual) } - @Ignore("Test not implemented yet") @Test() - fun uri_to_route_nevent() { + fun uri_to_route_incomplete_nevent() { val actual = Nip19Bech32.uriToRoute("nostr:nevent") - Assert.assertEquals(Nip19Bech32.Type.USER, actual?.type) - Assert.assertEquals("*", actual?.hex) + Assert.assertEquals(null, actual) } - @Ignore("Test not implemented yet") @Test() - fun uri_to_route_nrelay() { + fun uri_to_route_incomplete_nrelay() { val actual = Nip19Bech32.uriToRoute("nostr:nrelay") - Assert.assertEquals(Nip19Bech32.Type.RELAY, actual?.type) - Assert.assertEquals("*", actual?.hex) + Assert.assertEquals(null, actual) } - @Ignore("Test not implemented yet") @Test() - fun uri_to_route_naddr() { + fun uri_to_route_incomplete_naddr() { val actual = Nip19Bech32.uriToRoute("nostr:naddr") - Assert.assertEquals(Nip19Bech32.Type.ADDRESS, actual?.type) - Assert.assertEquals("*", actual?.hex) + Assert.assertEquals(null, actual) + } + + @Test() + fun uri_to_route_complete_nprofile() { + val actual = Nip19Bech32.uriToRoute("nostr:nprofile1qy2hwumn8ghj7un9d3shjtnyv9kh2uewd9hj7qgwwaehxw309ahx7uewd3hkctcpr9mhxue69uhhyetvv9ujuumwdae8gtnnda3kjctv9uqzq9thu3vem5gvsc6f3l3uyz7c92h6lq56t9wws0zulzkrgc6nrvym5jfztf") + + Assert.assertTrue(actual?.entity is Nip19Bech32.NProfile) + Assert.assertEquals("1577e4599dd10c863498fe3c20bd82aafaf829a595ce83c5cf8ac3463531b09b", (actual?.entity as? Nip19Bech32.NProfile)?.hex) + } + + @Test() + fun uri_to_route_complete_nevent() { + val actual = Nip19Bech32.uriToRoute("nostr:nevent1qy2hwumn8ghj7un9d3shjtnyv9kh2uewd9hj7qgwwaehxw309ahx7uewd3hkctcpr9mhxue69uhhyetvv9ujuumwdae8gtnnda3kjctv9uq36amnwvaz7tmjv4kxz7fwvd5xjcmpvahhqmr9vfejucm0d5hsz9mhwden5te0wfjkccte9ec8y6tdv9kzumn9wshsz8thwden5te0dehhxarj9ekh2arfdeuhwctvd3jhgtnrdakj7qg3waehxw309ucngvpwvcmh5tnfduhszythwden5te0dehhxarj9emkjmn99uq3jamnwvaz7tmhv4kxxmmdv5hxummnw3ezuamfdejj7qpqvsup5xk3e2quedxjvn2gjppc0lqny5dmnr2ypc9tftwmdxta0yjqrd6n50") + + Assert.assertTrue(actual?.entity is Nip19Bech32.NEvent) + Assert.assertEquals("64381a1ad1ca81ccb4d264d48904387fc13251bb98d440e0ab4addb6997d7924", (actual?.entity as? Nip19Bech32.NEvent)?.hex) + } + + @Test() + fun uri_to_route_complete_naddr() { + val actual = Nip19Bech32.uriToRoute("nostr:naddr1qqyxzmt9w358jum5qyt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueqzypd7v3r24z33cydnk3fmlrd0exe5dlej3506zxs05q4puerp765mzqcyqqq8scsq6mk7u") + + Assert.assertTrue(actual?.entity is Nip19Bech32.NAddress) + Assert.assertEquals("30818:5be6446aa8a31c11b3b453bf8dafc9b346ff328d1fa11a0fa02a1e6461f6a9b1:amethyst", (actual?.entity as? Nip19Bech32.NAddress)?.atag) } } From 328ed226c6085e56fc3c27b547812303105e0325 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 27 Feb 2024 18:50:20 -0500 Subject: [PATCH 174/176] Basic support for FHIR payloads. Support for nembed --- .../amethyst/model/LocalCache.kt | 22 ++ .../amethyst/ui/note/MiniFhir.kt | 123 +++++++++ .../amethyst/ui/note/NoteCompose.kt | 237 ++++++++++++++++++ .../amethyst/commons/RichTextParser.kt | 4 +- .../vitorpamplona/quartz/NIP19EmbedTests.kt | 35 ++- .../quartz/events/EventFactory.kt | 1 + .../quartz/events/FhirResourceEvent.kt | 51 ++++ 7 files changed, 470 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/MiniFhir.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/FhirResourceEvent.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 83638986e..f366531a5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -63,6 +63,7 @@ import com.vitorpamplona.quartz.events.DeletionEvent import com.vitorpamplona.quartz.events.EmojiPackEvent import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent import com.vitorpamplona.quartz.events.Event +import com.vitorpamplona.quartz.events.FhirResourceEvent import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileServersEvent import com.vitorpamplona.quartz.events.FileStorageEvent @@ -1363,6 +1364,26 @@ object LocalCache { refreshObservers(note) } + fun consume( + event: FhirResourceEvent, + relay: Relay?, + ) { + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) + + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event != null) return + + note.loadEvent(event, author, emptyList()) + + refreshObservers(note) + } + fun consume( event: HighlightEvent, relay: Relay?, @@ -1981,6 +2002,7 @@ object LocalCache { is EmojiPackEvent -> consume(event, relay) is EmojiPackSelectionEvent -> consume(event, relay) is SealedGossipEvent -> consume(event, relay) + is FhirResourceEvent -> consume(event, relay) is FileHeaderEvent -> consume(event, relay) is FileServersEvent -> consume(event, relay) is FileStorageEvent -> consume(event, relay) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MiniFhir.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MiniFhir.kt new file mode 100644 index 000000000..b782a1c19 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MiniFhir.kt @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2024 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.ui.note + +import com.fasterxml.jackson.annotation.JsonSubTypes +import com.fasterxml.jackson.annotation.JsonTypeInfo + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "resourceType", +) +@JsonSubTypes( + JsonSubTypes.Type(value = Practitioner::class, name = "Practitioner"), + JsonSubTypes.Type(value = Patient::class, name = "Patient"), + JsonSubTypes.Type(value = Bundle::class, name = "Bundle"), + JsonSubTypes.Type(value = VisionPrescription::class, name = "VisionPrescription"), +) +open class Resource( + var resourceType: String? = null, + var id: String = "", +) + +class Practitioner( + resourceType: String? = null, + id: String = "", + var active: Boolean? = null, + var name: ArrayList = arrayListOf(), + var gender: String? = null, +) : Resource(resourceType, id) + +class Patient( + resourceType: String? = null, + id: String = "", + var active: Boolean? = null, + var name: ArrayList = arrayListOf(), + var gender: String? = null, +) : Resource(resourceType, id) + +class HumanName( + var use: String? = null, + var family: String? = null, + var given: ArrayList = arrayListOf(), +) { + fun assembleName(): String { + return given.joinToString(" ") + " " + family + } +} + +class Bundle( + resourceType: String? = null, + id: String = "", + var type: String? = null, + var created: String? = null, + var entry: List = arrayListOf(), +) : Resource(resourceType, id) + +class VisionPrescription( + resourceType: String? = null, + id: String = "", + var status: String? = null, + var created: String? = null, + var patient: Reference? = Reference(), + var encounter: Reference? = Reference(), + var dateWritten: String? = null, + var prescriber: Reference? = Reference(), + var lensSpecification: List = arrayListOf(), +) : Resource(resourceType, id) + +class LensSpecification( + var eye: String? = null, + var sphere: Double? = null, + var cylinder: Double? = null, + var axis: Double? = null, + var add: Double? = null, + var prism: List = arrayListOf(), + // contact lenses + var power: Double? = null, + var backCurve: Double? = null, + var diameter: Double? = null, + var color: String? = null, + var brand: String? = null, + var note: String? = null, +) + +class Prism( + var amount: Double? = null, + var base: String? = null, +) + +class Reference( + var reference: String? = null, +) + +fun findReferenceInDb( + it: String, + db: Map, +): Resource? { + val parts = it.split("/") + return if (parts.size == 2) { + db.get(parts[1].removePrefix("#")) + } else { + db.get(it.removePrefix("#")) + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 7725b9ead..fbab305e1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -34,6 +34,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -53,11 +54,13 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState @@ -66,6 +69,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.Center import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -81,9 +85,11 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.graphics.drawable.toBitmap @@ -94,6 +100,8 @@ import androidx.lifecycle.map import coil.compose.AsyncImage import coil.compose.AsyncImagePainter import coil.request.SuccessResult +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fonfon.kgeohash.GeoHash import com.fonfon.kgeohash.toGeoHash import com.vitorpamplona.amethyst.R @@ -125,6 +133,7 @@ import com.vitorpamplona.amethyst.ui.components.VideoView import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog import com.vitorpamplona.amethyst.ui.components.measureSpaceWidth +import com.vitorpamplona.amethyst.ui.components.mockAccountViewModel import com.vitorpamplona.amethyst.ui.elements.AddButton import com.vitorpamplona.amethyst.ui.elements.DisplayFollowingCommunityInPost import com.vitorpamplona.amethyst.ui.elements.DisplayFollowingHashtagsInPost @@ -206,6 +215,8 @@ import com.vitorpamplona.quartz.events.EmojiPackEvent import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent import com.vitorpamplona.quartz.events.EmojiUrl import com.vitorpamplona.quartz.events.EmptyTagList +import com.vitorpamplona.quartz.events.Event +import com.vitorpamplona.quartz.events.FhirResourceEvent import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent import com.vitorpamplona.quartz.events.GenericRepostEvent @@ -239,8 +250,11 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.File import java.net.URL +import java.text.DecimalFormat +import java.text.NumberFormat import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -1176,6 +1190,9 @@ private fun RenderNoteRow( is BadgeAwardEvent -> { RenderBadgeAward(baseNote, backgroundColor, accountViewModel, nav) } + is FhirResourceEvent -> { + RenderFhirResource(baseNote, accountViewModel, nav) + } is PeopleListEvent -> { DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav) } @@ -4346,3 +4363,223 @@ fun CreateImageHeader( } } } + +@Preview +@Composable +fun RenderEyeGlassesPrescriptionPreview() { + val accountViewModel = mockAccountViewModel() + val nav: (String) -> Unit = {} + + val prescriptionEvent = Event.fromJson("{\"id\":\"0c15d2bc6f7dcc42fa4426d35d30d09840c9afa5b46d100415006e41d6471416\",\"pubkey\":\"bcd4715cc34f98dce7b52fddaf1d826e5ce0263479b7e110a5bd3c3789486ca8\",\"created_at\":1709074097,\"kind\":82,\"tags\":[],\"content\":\"{\\\"resourceType\\\":\\\"Bundle\\\",\\\"id\\\":\\\"bundle-vision-test\\\",\\\"type\\\":\\\"document\\\",\\\"entry\\\":[{\\\"resourceType\\\":\\\"Practitioner\\\",\\\"id\\\":\\\"2\\\",\\\"active\\\":true,\\\"name\\\":[{\\\"use\\\":\\\"official\\\",\\\"family\\\":\\\"Careful\\\",\\\"given\\\":[\\\"Adam\\\"]}],\\\"gender\\\":\\\"male\\\"},{\\\"resourceType\\\":\\\"Patient\\\",\\\"id\\\":\\\"1\\\",\\\"active\\\":true,\\\"name\\\":[{\\\"use\\\":\\\"official\\\",\\\"family\\\":\\\"Duck\\\",\\\"given\\\":[\\\"Donald\\\"]}],\\\"gender\\\":\\\"male\\\"},{\\\"resourceType\\\":\\\"VisionPrescription\\\",\\\"status\\\":\\\"active\\\",\\\"created\\\":\\\"2014-06-15\\\",\\\"patient\\\":{\\\"reference\\\":\\\"#1\\\"},\\\"dateWritten\\\":\\\"2014-06-15\\\",\\\"prescriber\\\":{\\\"reference\\\":\\\"#2\\\"},\\\"lensSpecification\\\":[{\\\"eye\\\":\\\"right\\\",\\\"sphere\\\":-2,\\\"prism\\\":[{\\\"amount\\\":0.5,\\\"base\\\":\\\"down\\\"}],\\\"add\\\":2},{\\\"eye\\\":\\\"left\\\",\\\"sphere\\\":-1,\\\"cylinder\\\":-0.5,\\\"axis\\\":180,\\\"prism\\\":[{\\\"amount\\\":0.5,\\\"base\\\":\\\"up\\\"}],\\\"add\\\":2}]}]}\",\"sig\":\"dc58f6109111ca06920c0c711aeaf8e2ee84975afa60d939828d4e01e2edea738f735fb5b1fcadf6d5496e36ac429abf7020a55fd1e4ed215738afc8d07cb950\"}") as FhirResourceEvent + + RenderFhirResource(prescriptionEvent, accountViewModel, nav) +} + +@Composable +fun RenderFhirResource( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val event = baseNote.event as? FhirResourceEvent ?: return + + RenderFhirResource(event, accountViewModel, nav) +} + +@Composable +fun RenderFhirResource( + event: FhirResourceEvent, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + var baseResource: Resource? by remember(event) { + mutableStateOf(null) + } + + LaunchedEffect(key1 = event) { + withContext(Dispatchers.IO) { + val mapper = + jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + + try { + baseResource = mapper.readValue(event.content, Resource::class.java) + } catch (e: Exception) { + Log.e("RenderEyeGlassesPrescription", "Parser error", e) + } + } + } + + baseResource?.let { resource -> + when (resource) { + is Bundle -> { + val db = resource.entry.associate { it.id to it } + val vision = resource.entry.filterIsInstance(VisionPrescription::class.java) + + vision.firstOrNull()?.let { + RenderEyeGlassesPrescription(it, db, accountViewModel, nav) + } + } + is VisionPrescription -> { + val db = mapOf(resource.id to resource) + RenderEyeGlassesPrescription(resource, db, accountViewModel, nav) + } + else -> { + } + } + } +} + +@Composable +fun RenderEyeGlassesPrescription( + visionPrescription: VisionPrescription, + db: Map, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(Size10dp), + ) { + val rightEye = visionPrescription.lensSpecification.firstOrNull { it.eye == "right" } + val leftEye = visionPrescription.lensSpecification.firstOrNull { it.eye == "left" } + + Text( + "Eyeglasses Prescription", + modifier = Modifier.padding(4.dp).fillMaxWidth(), + textAlign = TextAlign.Center, + ) + + Spacer(StdVertSpacer) + + visionPrescription.patient?.reference?.let { + val patient = findReferenceInDb(it, db) as? Patient + + patient?.name?.firstOrNull()?.assembleName()?.let { + Text( + text = "Patient: $it", + modifier = Modifier.padding(4.dp).fillMaxWidth(), + ) + } + } + visionPrescription.status?.let { + Text( + text = "Status: ${it.capitalize()}", + modifier = Modifier.padding(4.dp).fillMaxWidth(), + ) + } + + Spacer(DoubleVertSpacer) + + RenderEyeGlassesPrescriptionHeaderRow() + HorizontalDivider(thickness = DividerThickness) + + rightEye?.let { + RenderEyeGlassesPrescriptionRow(data = it) + HorizontalDivider(thickness = DividerThickness) + } + + leftEye?.let { + RenderEyeGlassesPrescriptionRow(data = it) + HorizontalDivider(thickness = DividerThickness) + } + + Spacer(DoubleVertSpacer) + + visionPrescription.prescriber?.reference?.let { + val practitioner = findReferenceInDb(it, db) as? Practitioner + + practitioner?.name?.firstOrNull()?.assembleName()?.let { + Text( + text = "Signed by: $it", + modifier = Modifier.padding(4.dp).fillMaxWidth(), + textAlign = TextAlign.Right, + ) + } + } + } +} + +@Composable +fun RenderEyeGlassesPrescriptionHeaderRow() { + Row( + modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = "Eye", + modifier = Modifier.padding(4.dp).weight(1f), + ) + VerticalDivider(thickness = DividerThickness) + Text( + text = "Sph", + textAlign = TextAlign.Right, + modifier = Modifier.padding(4.dp).weight(1f), + ) + Text( + text = "Cyl", + textAlign = TextAlign.Right, + modifier = Modifier.padding(4.dp).weight(1f), + ) + Text( + text = "Axis", + textAlign = TextAlign.Right, + modifier = Modifier.padding(4.dp).weight(1f), + ) + VerticalDivider(thickness = DividerThickness) + Text( + text = "Add", + textAlign = TextAlign.Right, + modifier = Modifier.padding(4.dp).weight(1f), + ) + } +} + +@Composable +fun RenderEyeGlassesPrescriptionRow(data: LensSpecification) { + Row( + modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + val numberFormat = DecimalFormat("##.00") + val integerFormat = DecimalFormat("###") + + Text( + text = data.eye?.capitalize() ?: "Unknown", + modifier = Modifier.padding(4.dp).weight(1f), + ) + VerticalDivider(thickness = DividerThickness) + Text( + text = formatOrBlank(data.sphere, numberFormat), + textAlign = TextAlign.Right, + modifier = Modifier.padding(4.dp).weight(1f), + ) + Text( + text = formatOrBlank(data.cylinder, numberFormat), + textAlign = TextAlign.Right, + modifier = Modifier.padding(4.dp).weight(1f), + ) + Text( + text = formatOrBlank(data.axis, integerFormat), + textAlign = TextAlign.Right, + modifier = Modifier.padding(4.dp).weight(1f), + ) + VerticalDivider(thickness = DividerThickness) + Text( + text = formatOrBlank(data.add, numberFormat), + textAlign = TextAlign.Right, + modifier = Modifier.padding(4.dp).weight(1f), + ) + } +} + +fun formatOrBlank( + amount: Double?, + numberFormat: NumberFormat, +): String { + if (amount == null) return "" + if (Math.abs(amount) < 0.01) return "" + return numberFormat.format(amount) +} diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/RichTextParser.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/RichTextParser.kt index 3532ba941..bb205711b 100644 --- a/commons/src/main/java/com/vitorpamplona/amethyst/commons/RichTextParser.kt +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/RichTextParser.kt @@ -319,8 +319,8 @@ class RichTextParser() { Pattern.compile("#([^\\s!@#\$%^&*()=+./,\\[{\\]};:'\"?><]+)(.*)", Pattern.CASE_INSENSITIVE) val acceptedNIP19schemes = - listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1") + - listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1").map { + listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1", "nembed") + + listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1", "nembed").map { it.uppercase() } diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP19EmbedTests.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP19EmbedTests.kt index 4e9506987..326fe5bb1 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP19EmbedTests.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP19EmbedTests.kt @@ -102,6 +102,38 @@ class NIP19EmbedTests { assertEquals(eyeglassesPrescriptionEvent!!.toJson(), decodedNote.toJson()) } + @Test + fun testVisionPrescriptionBundleEmbedEvent() { + val signer = + NostrSignerInternal( + KeyPair(Hex.decode("e8e7197ccc53c9ed4cf9b1c8dce085475fa1ffdd71f2c14e44fe23d0bdf77598")), + ) + + var eyeglassesPrescriptionEvent: Event? = null + + val countDownLatch = CountDownLatch(1) + + FhirResourceEvent.create(fhirPayload = visionPrescriptionBundle, signer = signer) { + eyeglassesPrescriptionEvent = it + countDownLatch.countDown() + } + + Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) + + assertNotNull(eyeglassesPrescriptionEvent) + + val bech32 = Nip19Bech32.createNEmbed(eyeglassesPrescriptionEvent!!) + + println(eyeglassesPrescriptionEvent!!.toJson()) + println(bech32) + + val decodedNote = (Nip19Bech32.uriToRoute(bech32)?.entity as Nip19Bech32.NEmbed).event + + assertTrue(decodedNote.hasValidSignature()) + + assertEquals(eyeglassesPrescriptionEvent!!.toJson(), decodedNote.toJson()) + } + /* @Test fun testCompressionSizes() { @@ -138,5 +170,6 @@ class NIP19EmbedTests { assertTrue(true) }*/ - val visionPrescriptionFhir = "{\"resourceType\":\"VisionPrescription\",\"status\":\"active\",\"created\":\"2014-06-15\",\"patient\":{\"reference\":\"Patient/example\"},\"dateWritten\":\"2014-06-15\",\"prescriber\":{\"reference\":\"Practitioner/example\"},\"lensSpecification\":[{\"eye\":\"right\",\"sphere\":-2,\"prism\":[{\"amount\":0.5,\"base\":\"down\"}],\"add\":2},{\"eye\":\"left\",\"sphere\":-1,\"cylinder\":-0.5,\"axis\":180,\"prism\":[{\"amount\":0.5,\"base\":\"up\"}],\"add\":2}]}" + val visionPrescriptionFhir = "{\"resourceType\":\"VisionPrescription\",\"status\":\"active\",\"created\":\"2014-06-15\",\"patient\":{\"reference\":\"Patient/Donald Duck\"},\"dateWritten\":\"2014-06-15\",\"prescriber\":{\"reference\":\"Practitioner/Adam Careful\"},\"lensSpecification\":[{\"eye\":\"right\",\"sphere\":-2,\"prism\":[{\"amount\":0.5,\"base\":\"down\"}],\"add\":2},{\"eye\":\"left\",\"sphere\":-1,\"cylinder\":-0.5,\"axis\":180,\"prism\":[{\"amount\":0.5,\"base\":\"up\"}],\"add\":2}]}" + val visionPrescriptionBundle = "{\"resourceType\":\"Bundle\",\"id\":\"bundle-vision-test\",\"type\":\"document\",\"entry\":[{\"resourceType\":\"Practitioner\",\"id\":\"2\",\"active\":true,\"name\":[{\"use\":\"official\",\"family\":\"Careful\",\"given\":[\"Adam\"]}],\"gender\":\"male\"},{\"resourceType\":\"Patient\",\"id\":\"1\",\"active\":true,\"name\":[{\"use\":\"official\",\"family\":\"Duck\",\"given\":[\"Donald\"]}],\"gender\":\"male\"},{\"resourceType\":\"VisionPrescription\",\"status\":\"active\",\"created\":\"2014-06-15\",\"patient\":{\"reference\":\"#1\"},\"dateWritten\":\"2014-06-15\",\"prescriber\":{\"reference\":\"#2\"},\"lensSpecification\":[{\"eye\":\"right\",\"sphere\":-2,\"prism\":[{\"amount\":0.5,\"base\":\"down\"}],\"add\":2},{\"eye\":\"left\",\"sphere\":-1,\"cylinder\":-0.5,\"axis\":180,\"prism\":[{\"amount\":0.5,\"base\":\"up\"}],\"add\":2}]}]}" } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt index ae0b8e962..951901e4e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt @@ -87,6 +87,7 @@ class EventFactory { FileStorageEvent.KIND -> FileStorageEvent(id, pubKey, createdAt, tags, content, sig) FileStorageHeaderEvent.KIND -> FileStorageHeaderEvent(id, pubKey, createdAt, tags, content, sig) + FhirResourceEvent.KIND -> FhirResourceEvent(id, pubKey, createdAt, tags, content, sig) GenericRepostEvent.KIND -> GenericRepostEvent(id, pubKey, createdAt, tags, content, sig) GiftWrapEvent.KIND -> GiftWrapEvent(id, pubKey, createdAt, tags, content, sig) GitIssueEvent.KIND -> GitIssueEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/FhirResourceEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/FhirResourceEvent.kt new file mode 100644 index 000000000..923fb47a7 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/FhirResourceEvent.kt @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2024 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.events + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class FhirResourceEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { + companion object { + const val KIND = 82 + + fun create( + fhirPayload: String, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (FhirResourceEvent) -> Unit, + ) { + val tags = mutableListOf>() + + signer.sign(createdAt, KIND, tags.toTypedArray(), fhirPayload, onReady) + } + } +} From 85059d775b6b8d3cd3ca299951f8c22595c9c694 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Tue, 27 Feb 2024 23:52:02 +0000 Subject: [PATCH 175/176] New Crowdin translations by GitHub Action --- app/src/main/res/values-es-rES/strings.xml | 38 +++++++++++++++++----- app/src/main/res/values-es-rMX/strings.xml | 26 +++++++++++++-- app/src/main/res/values-es-rUS/strings.xml | 26 +++++++++++++-- app/src/main/res/values-fr/strings.xml | 2 ++ 4 files changed, 80 insertions(+), 12 deletions(-) diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index c9e4bba80..62a2a8e82 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -2,7 +2,7 @@ Apunta al código QR Mostrar el QR - Imagen de Perfil + Imagen de perfil Tu imagen de perfil Escanear el QR Mostrar de todos modos @@ -16,6 +16,7 @@ Spam Suplantación de identidad Comportamiento ilegal + Otro Desconocido Icono del transmisor Autor desconocido @@ -23,6 +24,9 @@ Copiar PubKey del usuario Copiar ID de la nota Transmisión + Poner marca de tiempo + Marca de tiempo: confirmaciones pendientes + OTS: pendiente Solicitar eliminación Bloquear / Reportar @@ -30,20 +34,21 @@ Reportar suplantación de identidad Reportar contenido explícito Reportar comportamiento ilegal - Inicie sesión con una clave privada para poder responder - Inicie sesión con una clave privada para poder publicar posts - Inicie sesión con una clave privada para dar me gusta a los posts - Falta la configuración de Zaps por defecto. Mantén pulsado unos segundos para cambiarla - Inicie sesión con una clave privada para poder enviar Zaps + Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder responder + Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder impulsar publicaciones. + Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para indicar que te gustan las publicaciones. + Falta la configuración de zaps por defecto. Mantén pulsado unos segundos para cambiarla + Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder enviar zaps. Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder seguir a otros usuarios. Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder dejar de seguir a otros usuarios. Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder ocultar una palabra o frase. Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder mostrar una palabra o frase. Zaps - Total vistas + Total de visualizaciones Impulsar Impulsada Citar + Bifurcar Nueva cantidad en sats Añadir "respondiendo a " @@ -355,7 +360,7 @@ Te notifica cuando alguien te zapea %1$s sats De %1$s - para %1$s + por %1$s Notificar: Unirse a la conversación ID de usuario o grupo @@ -648,4 +653,21 @@ Mostrar npub como código QR Dirección inválida Amethyst recibió un URI para abrir pero era inválido: %1$s + ¡Zapea a los desarrolladores! + Tu donación nos ayuda a marcar la diferencia. ¡Cada sat cuenta! + Donar ahora + fue posible gracias a: + Esta versión fue posible gracias a: + Versión %1$s + ¡Gracias! + Límite máximo + Escrituras restringidas + Bifurcación de + BIFURCACIÓN + Repositorio Git: %1$s + Web: + Clon: + OTS: %1$s + Prueba de marca de tiempo + Hay prueba de que esta publicación se firmó en algún momento antes de %1$s. La prueba se marcó en la cadena de bloques de Bitcoin en esa fecha y hora. diff --git a/app/src/main/res/values-es-rMX/strings.xml b/app/src/main/res/values-es-rMX/strings.xml index 9e284eff0..2852fb8e8 100644 --- a/app/src/main/res/values-es-rMX/strings.xml +++ b/app/src/main/res/values-es-rMX/strings.xml @@ -16,6 +16,7 @@ Spam Suplantación de identidad Comportamiento ilegal + Otro Desconocido Ícono de relé Autor desconocido @@ -23,6 +24,9 @@ Copiar llave pública del usuario Copiar ID de la nota Transmisión + Poner marca de tiempo + Marca de tiempo: confirmaciones pendientes + OTS: pendiente Solicitar eliminación Bloquear / Reportar @@ -40,10 +44,11 @@ Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder ocultar una palabra o frase. Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder mostrar una palabra o frase. Zaps - Ver recuento + Total de visualizaciones Impulsar Impulsada Cita + Bifurcar Nueva cantidad en sats Agregar "respondiendo a " @@ -355,7 +360,7 @@ Te notifica cuando alguien te zapea %1$s sats De %1$s - para %1$s + por %1$s Notificar: Unirse a la conversación ID de usuario o grupo @@ -648,4 +653,21 @@ Mostrar npub como código QR Dirección inválida Amethyst recibió un URI para abrir pero era inválido: %1$s + ¡Zapea a los desarrolladores! + Tu donación nos ayuda a marcar la diferencia. ¡Cada sat cuenta! + Donar ahora + fue posible gracias a: + Esta versión fue posible gracias a: + Versión %1$s + ¡Gracias! + Límite máximo + Escrituras restringidas + Bifurcación de + BIFURCACIÓN + Repositorio Git: %1$s + Web: + Clon: + OTS: %1$s + Prueba de marca de tiempo + Hay prueba de que esta publicación se firmó en algún momento antes de %1$s. La prueba se marcó en la cadena de bloques de Bitcoin en esa fecha y hora. diff --git a/app/src/main/res/values-es-rUS/strings.xml b/app/src/main/res/values-es-rUS/strings.xml index b0c447063..09a021900 100644 --- a/app/src/main/res/values-es-rUS/strings.xml +++ b/app/src/main/res/values-es-rUS/strings.xml @@ -16,6 +16,7 @@ Spam Suplantación de identidad Comportamiento ilegal + Otro Desconocido Ícono de relé Autor desconocido @@ -23,6 +24,9 @@ Copiar ID del autor Copiar ID de la nota Transmisión + Poner marca de tiempo + Marca de tiempo: confirmaciones pendientes + OTS: pendiente Solicitar eliminación Bloquear / Reportar @@ -40,10 +44,11 @@ Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder ocultar una palabra o frase. Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder mostrar una palabra o frase. Zaps - Ver recuento + Total vistas Impulsar Impulsada Cita + Bifurcar Nueva cantidad en sats Agregar "respondiendo a " @@ -355,7 +360,7 @@ Te notifica cuando alguien te zapea %1$s sats De %1$s - para %1$s + por %1$s Notificar: Unirse a la conversación ID de usuario o grupo @@ -648,4 +653,21 @@ Mostrar npub como código QR Dirección inválida Amethyst recibió un URI para abrir pero era inválido: %1$s + ¡Zapea a los desarrolladores! + Tu donación nos ayuda a marcar la diferencia. ¡Cada sat cuenta! + Donar ahora + fue posible gracias a: + Esta versión fue posible gracias a: + Versión %1$s + ¡Gracias! + Límite máximo + Escrituras restringidas + Bifurcación de + BIFURCACIÓN + Repositorio Git: %1$s + Web: + Clon: + OTS: %1$s + Prueba de marca de tiempo + Hay prueba de que esta publicación se firmó en algún momento antes de %1$s. La prueba se marcó en la cadena de bloques de Bitcoin en esa fecha y hora. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 6dd5a6e5d..266c0422d 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -668,4 +668,6 @@ Web : Clone : OTS: %1$s + Preuve d\'horodatage + Il y a une preuve que ce message a été signé avant %1$s. La preuve a été estampillée dans la blockchain Bitcoin à cette date et à cette heure. From 0d92384d8ca9f0b246a8415b147f8865910d90b8 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 27 Feb 2024 19:16:06 -0500 Subject: [PATCH 176/176] Fixes OTS test cases --- .../java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt b/app/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt index 7dd5626fe..f09f8964b 100644 --- a/app/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt +++ b/app/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt @@ -77,7 +77,9 @@ class OkHttpOtsTest { val countDownLatch = CountDownLatch(1) - OtsEvent.create(otsEvent2Digest, signer) { + val otsFile = OtsEvent.stamp(otsEvent2Digest) + + OtsEvent.create(otsEvent2Digest, otsFile, signer) { ots = it countDownLatch.countDown() } @@ -87,7 +89,7 @@ class OkHttpOtsTest { println(ots!!.toJson()) println(ots!!.info()) - // Should not be valid + // Should not be valid because we need to wait for confirmations assertEquals(null, ots!!.verify()) } }