diff --git a/.gitignore b/.gitignore
index e5303fd75d..f527be1420 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,7 @@
/.idea/studiobot.xml
/.idea/other.xml
/.idea/runConfigurations.xml
+/.idea/ChatHistory_schema_v2.xml
.DS_Store
/build
/captures
diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml
new file mode 100644
index 0000000000..91f95584dd
--- /dev/null
+++ b/.idea/deviceManager.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/kotlinNotebook.xml b/.idea/kotlinNotebook.xml
new file mode 100644
index 0000000000..665e38dae3
--- /dev/null
+++ b/.idea/kotlinNotebook.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index bb4493707f..bdfaa5e068 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,5 +1,11 @@
+
+
+
+
+
+
diff --git a/README.md b/README.md
index 3ed3b45d97..27e3714122 100644
--- a/README.md
+++ b/README.md
@@ -175,7 +175,7 @@ Lastly, the user's account information (private key/pub key) is stored in the An
## Setup
Make sure to have the following pre-requisites installed:
-1. Java 17+
+1. Java 21+
2. Android Studio
3. Android 8.0+ Phone or Emulation setup
diff --git a/amethyst/build.gradle b/amethyst/build.gradle
index 38147f0ca7..ab20337e63 100644
--- a/amethyst/build.gradle
+++ b/amethyst/build.gradle
@@ -9,20 +9,20 @@ plugins {
}
android {
- namespace 'com.vitorpamplona.amethyst'
- compileSdk libs.versions.android.compileSdk.get().toInteger()
+ namespace = 'com.vitorpamplona.amethyst'
+ compileSdk = libs.versions.android.compileSdk.get().toInteger()
defaultConfig {
- applicationId "com.vitorpamplona.amethyst"
- minSdk libs.versions.android.minSdk.get().toInteger()
- targetSdk libs.versions.android.targetSdk.get().toInteger()
- versionCode 418
- versionName "0.94.3"
+ applicationId = "com.vitorpamplona.amethyst"
+ minSdk = libs.versions.android.minSdk.get().toInteger()
+ targetSdk = libs.versions.android.targetSdk.get().toInteger()
+ versionCode = 418
+ versionName = "0.94.3"
buildConfigField "String", "RELEASE_NOTES_ID", "\"fd42b23b9ef792059b1c1a89555443abbb11578f4b3c8430b452559eec7325f3\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
- useSupportLibrary true
+ useSupportLibrary = true
}
resourceConfigurations += [
'ar',
@@ -107,7 +107,7 @@ android {
buildTypes {
release {
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), 'proguard-rules.pro'
- minifyEnabled true
+ minifyEnabled = true
}
debug {
applicationIdSuffix '.debug'
@@ -119,8 +119,8 @@ android {
applicationIdSuffix '.benchmark'
versionNameSuffix '-BENCHMARK'
resValue "string", "app_name", "@string/app_name_benchmark"
- profileable true
- signingConfig signingConfigs.debug
+ profileable = true
+ signingConfig = signingConfigs.debug
}
}
@@ -139,10 +139,10 @@ android {
splits {
abi {
- enable true
+ enable = true
reset()
include "x86", "x86_64", "arm64-v8a", "armeabi-v7a"
- universalApk true
+ universalApk = true
}
}
@@ -152,8 +152,8 @@ android {
}
buildFeatures {
- compose true
- buildConfig true
+ compose = true
+ buildConfig = true
}
packagingOptions {
@@ -171,6 +171,17 @@ android {
}
}
+// TODO: until google merges and unifiedpush updates https://github.com/tink-crypto/tink-java-apps/pull/5
+configurations.all {
+ def tink = "com.google.crypto.tink:tink-android:1.17.0"
+ resolutionStrategy {
+ force(tink)
+ dependencySubstitution {
+ substitute module('com.google.crypto.tink:tink') using module(tink)
+ }
+ }
+}
+
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_17
@@ -222,9 +233,11 @@ dependencies {
// Websockets API
implementation libs.okhttp
+ implementation libs.okhttpCoroutines
// Encrypted Key Storage
implementation libs.androidx.security.crypto.ktx
+ implementation libs.androidx.datastore.preferences
// view videos
implementation libs.androidx.media3.exoplayer
diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/CashuBTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/CashuBTest.kt
index ff697d6c82..200c896d79 100644
--- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/CashuBTest.kt
+++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/CashuBTest.kt
@@ -21,8 +21,8 @@
package com.vitorpamplona.amethyst
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.vitorpamplona.amethyst.service.CashuProcessor
-import com.vitorpamplona.amethyst.service.CashuToken
+import com.vitorpamplona.amethyst.service.cashu.CashuParser
+import com.vitorpamplona.amethyst.service.cashu.CashuToken
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.runBlocking
@@ -38,7 +38,7 @@ class CashuBTest {
@Test()
fun parseCashuA() {
runBlocking {
- val parsed = (CashuProcessor().parse(cashuTokenA) as GenericLoadable.Loaded>).loaded[0]
+ val parsed = (CashuParser().parse(cashuTokenA) as GenericLoadable.Loaded>).loaded[0]
assertEquals(cashuTokenA, parsed.token)
assertEquals("https://8333.space:3338", parsed.mint)
@@ -59,7 +59,7 @@ class CashuBTest {
@Test()
fun parseCashuB() =
runBlocking {
- val parsed = (CashuProcessor().parse(cashuTokenB1) as GenericLoadable.Loaded>).loaded
+ val parsed = (CashuParser().parse(cashuTokenB1) as GenericLoadable.Loaded>).loaded
assertEquals(cashuTokenB1, parsed[0].token)
assertEquals("http://localhost:3338", parsed[0].mint)
@@ -84,7 +84,7 @@ class CashuBTest {
@Test()
fun parseCashuB2() =
runBlocking {
- val parsed = (CashuProcessor().parse(cashuTokenB2) as GenericLoadable.Loaded>).loaded
+ val parsed = (CashuParser().parse(cashuTokenB2) as GenericLoadable.Loaded>).loaded
assertEquals(cashuTokenB2, parsed[0].token)
assertEquals("http://lbutlh5lfggq5r7xpiwhrajdl7sxpupgagazxl65w4c5cg72wtofasad.onion:3338", parsed[0].mint)
diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/DMFileDecryptionTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/DMFileDecryptionTest.kt
index d00972ce2a..7e3e34a6ec 100644
--- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/DMFileDecryptionTest.kt
+++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/DMFileDecryptionTest.kt
@@ -58,7 +58,6 @@ class DMFileDecryptionTest {
val request =
Request
.Builder()
- .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.url(url)
.get()
.build()
diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt
index d891ebecae..ef98f7fa90 100644
--- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt
+++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt
@@ -24,8 +24,9 @@ import android.graphics.Bitmap
import android.graphics.Color
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
-import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AccountSettings
+import com.vitorpamplona.amethyst.model.LocalCache
+import com.vitorpamplona.amethyst.model.nipB7Blossom.BlossomServerListState
import com.vitorpamplona.amethyst.service.okhttp.DefaultContentTypeInterceptor
import com.vitorpamplona.amethyst.service.uploads.FileHeader
import com.vitorpamplona.amethyst.service.uploads.ImageDownloader
@@ -37,6 +38,8 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
+import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
+import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent
import com.vitorpamplona.quartz.utils.sha256.sha256
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.fail
@@ -48,6 +51,7 @@ import okhttp3.OkHttpClient
import org.junit.Assert
import org.junit.Ignore
import org.junit.Test
+import org.junit.runner.Request.method
import org.junit.runner.RunWith
import java.io.ByteArrayOutputStream
import kotlin.random.Random
@@ -55,10 +59,17 @@ import kotlin.random.Random
@RunWith(AndroidJUnit4::class)
class ImageUploadTesting {
companion object {
- val account =
- Account(
- AccountSettings(KeyPair()),
- scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
+ val accountSettings = AccountSettings(KeyPair())
+ val signer = NostrSignerInternal(accountSettings.keyPair)
+ val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
+ val cache = LocalCache
+
+ val blossomServerListState =
+ BlossomServerListState(
+ signer = signer,
+ cache = cache,
+ scope = scope,
+ settings = accountSettings,
)
}
@@ -107,7 +118,7 @@ class ImageUploadTesting {
sensitiveContent = null,
serverBaseUrl = server.baseUrl,
okHttpClient = { client },
- httpAuth = account::createBlossomUploadAuth,
+ httpAuth = blossomServerListState::createBlossomUploadAuth,
context = InstrumentationRegistry.getInstrumentation().targetContext,
)
@@ -135,25 +146,27 @@ class ImageUploadTesting {
{ client },
)
- val paylod = getBitmap()
- val inputStream = paylod.inputStream()
+ val payload = getBitmap()
+ val inputStream = payload.inputStream()
val result =
Nip96Uploader()
.upload(
inputStream = inputStream,
- length = paylod.size.toLong(),
+ length = payload.size.toLong(),
contentType = "image/png",
alt = null,
sensitiveContent = null,
server = serverInfo,
okHttpClient = { client },
onProgress = {},
- httpAuth = account::createHTTPAuthorization,
+ httpAuth = { url, method, body ->
+ signer.sign(HTTPAuthorizationEvent.build(url, method, body))
+ },
context = InstrumentationRegistry.getInstrumentation().targetContext,
)
val url = result.url!!
- val size = result.size
+ val size = result.size?.toInt()
val dim = result.dimension
val hash = result.sha256
@@ -179,7 +192,7 @@ class ImageUploadTesting {
assertEquals("${server.name}: Invalid dimensions", it.dim.toString(), dim.toString())
}
if (size != null) {
- assertEquals("${server.name}: Invalid size", it.size.toString(), size)
+ assertEquals("${server.name}: Invalid size", it.size, size)
}
},
onFailure = { fail("${server.name}: It should not fail") },
@@ -221,6 +234,7 @@ class ImageUploadTesting {
testBase(ServerName("sove", "https://sove.rent", ServerType.NIP96))
}
+ @Ignore("Not Working anymore")
@Test()
fun testNostrBuild() =
runBlocking {
@@ -235,6 +249,7 @@ class ImageUploadTesting {
}
@Test()
+ @Ignore("Not Working anymore")
fun testVoidCat() =
runBlocking {
testBase(ServerName("void.cat", "https://void.cat", ServerType.NIP96))
diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt
index 0e38d73796..d039911477 100644
--- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt
+++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt
@@ -25,17 +25,15 @@ import com.vitorpamplona.amethyst.service.ots.OkHttpBitcoinExplorer
import com.vitorpamplona.amethyst.service.ots.OkHttpCalendarBuilder
import com.vitorpamplona.amethyst.service.ots.OtsBlockHeightCache
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
-import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
+import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
import junit.framework.TestCase.assertEquals
+import kotlinx.coroutines.runBlocking
import okhttp3.OkHttpClient
-import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
@RunWith(AndroidJUnit4::class)
class OkHttpOtsTest {
@@ -49,30 +47,30 @@ class OkHttpOtsTest {
val resolver =
OtsResolver(
OkHttpBitcoinExplorer(
- OkHttpBitcoinExplorer.MEMPOOL_API_URL,
+ baseAPI = OkHttpBitcoinExplorer.MEMPOOL_API_URL,
client = OkHttpClient.Builder().build(),
- otsCache,
+ cache = otsCache,
),
OkHttpCalendarBuilder { OkHttpClient.Builder().build() },
)
@Test
fun verifyNostrEvent() {
- val ots = EventMapper.fromJson(otsEvent) as OtsEvent
+ val ots = JsonMapper.fromJson(otsEvent) as OtsEvent
println(resolver.info(ots.otsByteArray()))
assertEquals(1707688818L, ots.verify(resolver))
}
@Test
fun verifyNostrEvent2() {
- val ots = EventMapper.fromJson(otsEvent2) as OtsEvent
+ val ots = JsonMapper.fromJson(otsEvent2) as OtsEvent
println(resolver.info(ots.otsByteArray()))
assertEquals(1706322179L, ots.verify(resolver))
}
@Test
fun verifyNostrPendingEvent() {
- val ots = EventMapper.fromJson(otsPendingEvent) as OtsEvent
+ val ots = JsonMapper.fromJson(otsPendingEvent) as OtsEvent
println(resolver.info(ots.otsByteArray()))
assertEquals(null, ots.verify(resolver))
}
@@ -80,20 +78,15 @@ class OkHttpOtsTest {
@Test
fun createOTSEventAndVerify() {
val signer = NostrSignerInternal(KeyPair())
- var ots: OtsEvent? = null
-
- val countDownLatch = CountDownLatch(1)
val otsFile = OtsEvent.stamp(otsEvent2Digest, resolver)
- signer.sign(OtsEvent.build(otsEvent2Digest, otsFile)) {
- ots = it
- countDownLatch.countDown()
- }
+ val ots =
+ runBlocking {
+ signer.sign(OtsEvent.build(otsEvent2Digest, otsFile))
+ }
- Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS))
-
- println(ots!!.toJson())
+ println(ots.toJson())
println(resolver.info(ots.otsByteArray()))
// Should not be valid because we need to wait for confirmations
diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt
index 61497dfb17..f1d881ce5b 100644
--- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt
+++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt
@@ -25,10 +25,14 @@ import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.LocalCache
+import com.vitorpamplona.amethyst.service.location.LocationState
+import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal.ThreadFeedFilter
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
-import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
+import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
+import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
+import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip01Core.verify
import junit.framework.TestCase
@@ -36,92 +40,116 @@ import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withContext
+import okhttp3.OkHttpClient
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ThreadDualAxisChartAssemblerTest {
- val db =
- """
- [
- {"id":"741f5367a9415f4d6f19c0f57a1e4647c8ed8309b53b0da2d82fc4ebfba03b2c","pubkey":"d85c99afd244911e0aaf800cbea4221df557f06f8a4ff2cbe84b24e0b9e728fc","created_at":1684674845,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"Getting my head around this","sig":"069874040bac26a219777fc0f90b8f4df71e38c30e3e6a953d53222499d8e0c5a8f32c6b4204d14eb335bb654f01c5610372d9dc00062284b8e0f2bb98c7ed85"},
- {"id":"22323fc72b4c37f93ea21f6069684339ce5f63111161c81f2aa3de4a21bfe83b","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1683571810,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"test","sig":"f6d3bcf0f8e07d06720a2527f95910ee5a66aa889dd9261514921a155bbc3e9a3c7d721f5a2ec948cd795183f85dbe80f2ee2e36663f9d1de33fc03274ccb9d9"},
- {"id":"14525fcaae530a029f782fd361dd0cd66634c3e23020bd19e66fe11c1e254e32","pubkey":"afd563434a737334d69db899e4a32fe38d73a182bb6d1e91d83a2c4c4e04737c","created_at":1682968749,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"This is most amazing thing yet for nostr!","sig":"3dcbb058b8fa6e6f388f5b1a26d21b888dbc91bb831b0ed80ee30d9d2494fbbbb40dac9daf637b948c3afe1f32c2c662eca8d7acc0ba31f23b89edc8ade99631"},
- {"id":"98ae0d6d10e494ed0bf70feb577e8225c6a6732c7af3d29f88bfe1b87d4439e6","pubkey":"83fd07de9b763334cc9d46f2785c2558e6c2eabfe7d0c6ec214667cbaec50d47","created_at":1681995243,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"true if big","sig":"bb7d97e81207096d8615ebb0458c3023fa1b8f3aac463f3f0293ae94e2b90d043ad371067ce984a2ab2ba49cd7432a553f128b8d2d678238551538ad657bd75c"},
- {"id":"36e262e71b7e8bcae946f69885b8c3614e318e82864437342cf50e8b9ab7229d","pubkey":"b111d517452f9ef015e16d60ae623a6b66af63024eec941b0653bfee0dd667d4","created_at":1681979196,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"this is wonderful. great work.","sig":"205a46d867c22cbb09c7dcc4351330a95304c0d80540c85b9ea39f7f464ea60df1ef5006c1f93b862536f04d8a7855e4cc2a915d7a01437cd30f7f12c716b239"},
- {"id":"37725c2924ca267d66c2c27c2dae65550c07e7b883034cf1ea69671883430642","pubkey":"90b9bec74789688e515125596ab6350bfe646176ac75742275063922c5fea010","created_at":1681944950,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"Epic!","sig":"c461b97a3cdc5cf967e6f32c59c94b1b912c296feb1cbb7943c81731e558b10a3674982d6990567260d9fb033f5a3d3db8984b1eb362715d68ba46034a8b1b86"},
- {"id":"8ba54cfb6375270e8ae97a7e0992c1a0dbaa4cd46af8309d67a839e86789fde6","pubkey":"c48e29f04b482cc01ca1f9ef8c86ef8318c059e0e9353235162f080f26e14c11","created_at":1681944377,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"This is incredible... Bravo, Pablo, and shoutout to Gigi.","sig":"fb1a84d4859742b001b810c98a5e9301bc861fa2d65a711cb21c57f6e62c98f27e5b0c0c6432ef1111bc6cc52aa8cb0ae909d28af50c497d125950bfaeade475"},
- {"id":"53410bc6d47e87f3f18ecbc93c716b5a6ef8ee3805516b2ff4d155154a685b7c","pubkey":"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","created_at":1681945005,"kind":1,"tags":[["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["e","8ba54cfb6375270e8ae97a7e0992c1a0dbaa4cd46af8309d67a839e86789fde6"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["p","c48e29f04b482cc01ca1f9ef8c86ef8318c059e0e9353235162f080f26e14c11"]],"content":"Thank you, my friend 🤙 ","sig":"3d2fbcefdfac2183601076c8f9e518e05620cefe74cba233684cce931a69b51befa329c94734e434805bd9382ff3dad36e42d1955d4e47c822e973ba69b7377f"},
- {"id":"e383476cb1ce5accde11d4b1338424fa32c3724cf96e6214af8e5e852981728a","pubkey":"50de492cfe5472450df1a0176fdf6d915e97cb5d9f8d3eccef7d25ff0a8871de","created_at":1681932060,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"https://nostr.build/p/nb5753.png","sig":"9df6e3872f420ed5225aaa242f1c806b4739f17e265f02ca593a3c31c5fb5cb48705ee6bc0510f81f123527aa96ec4d07adf829362e2196bd7b833deded4c2b8"},
- {"id":"e2d8aaed336d3c0f73a9ca46a89fdb2da62a6d172936a91b0067a68797b3bcb8","pubkey":"50de492cfe5472450df1a0176fdf6d915e97cb5d9f8d3eccef7d25ff0a8871de","created_at":1681931869,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"Very Cool, I started playing around!","sig":"5980b2d0ef114d50c5f45b5d0c55bfc56b16a0a1807774ec2a86c4e352e17e3ec78877c73f2fedfbfd097a018df8e965107d6efd4ad9c0a0e1ecc89332d51cec"},
- {"id":"b92e4d6a5d0e8d1d2d2421044b84a4d11f2188261c55145d782b1b6bf0995009","pubkey":"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","created_at":1681932193,"kind":1,"tags":[["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f","","root"],["e","e2d8aaed336d3c0f73a9ca46a89fdb2da62a6d172936a91b0067a68797b3bcb8","","reply"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"]],"content":"love seeing it!!!","sig":"47850c205f1fa6d8836cda347c7d12c4307d35a110629eac48b4fff26600a14a5c5c8a7877b851db062a89e0157be112eae60ba0594525c1cc4d355906368dbb"},
- {"id":"da13e14cc8bcc243e0373dde14533d3829b8b621e214ca3c99c90f3dd9e11b8a","pubkey":"b488b594d84bb3e7c8421a4b91b19a006dc13d0c00da304a0263355d02195f04","created_at":1681931637,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"Great thoughts ","sig":"34c81430952508746fd5d5a4c4f7df0f5762aeb17d075a74f7b0daf387a7b8772edf4152731cff6650219e1aa9f8f63924410baa03091a698af0513cbf57e0af"},
- {"id":"b5234d90a1543ba60765c57ac3fc7140129a4ac28bbd013531ec9b85e256ea55","pubkey":"b488b594d84bb3e7c8421a4b91b19a006dc13d0c00da304a0263355d02195f04","created_at":1681931560,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"I like comments ","sig":"6915f4c2a08622339957495931eabc37f8d0c50ab1f3ec42327b67d15e50f50ff4bc302848b606e6d7c170696f8d744e6f896223f30ecb39a3adffe58020ff6f"},
- {"id":"fc4e4a1230b002e4ae08251a0b26107de7f518800188b7a504f5de62d8b07996","pubkey":"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","created_at":1681834615,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"This is exactly why I built this. I am enamored by this idea.","sig":"bdd1b3532efa3badeb3807fe74d31c25f17c84bbe39fe3c692d3d73efa39408089843ebb3c049840f2d525e6849f8254179a888bedc776ee7ad5e6997dc10c44"},
- {"id":"d4a0b4f08d98d82a04292654ec132723cc2cf3fa24ffb6c0833426cb9372f4d5","pubkey":"39a8b17475be0db44e313f9fd032ffde183c8abd6498e4932a873330d2cd4868","created_at":1680618462,"kind":1,"tags":[["e","44d3ecbb5752e96becc330d9b56352ea595d37b4c55edd8701fd24f18df5eee2"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["p","39a8b17475be0db44e313f9fd032ffde183c8abd6498e4932a873330d2cd4868"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"Unfortunately when boosting this it gets broken 🥹\n#[1]","sig":"c4a4197df4fec008f2f565170d09c46f0b9a7217ed90432cb53d1dddf70ad8520722b6e4f04ec9e52dd219ee01387c6191f449cd10a6c2488b1d2866424b7a8a"},
- {"id":"8cdc4676aca93bbafcfbe6784f9b2df54e8ca20fbe69ba55fda487736bfdb7f6","pubkey":"6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93","created_at":1680636068,"kind":1,"tags":[["e","44d3ecbb5752e96becc330d9b56352ea595d37b4c55edd8701fd24f18df5eee2"],["e","d4a0b4f08d98d82a04292654ec132723cc2cf3fa24ffb6c0833426cb9372f4d5"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["p","39a8b17475be0db44e313f9fd032ffde183c8abd6498e4932a873330d2cd4868"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"we early ","sig":"b8026e43c61b491014915caf79289968b87e52aa87a487ebecbf6cb2b3c07d460f2ab1a86da99b1c5410a0f5d14e77cf27f78c56bf1ab709f0d616ad2711de78"},
- {"id":"e15b386824fbfdcbf1b50b8860f03062cef534a3ea5339cc837536fb2a58465e","pubkey":"d3d2b986796c9e2067c58fc9017cace5adc1876e9b674719078d5c13d82e54a9","created_at":1690025613,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"]],"content":"test 3","sig":"1a34f9bb9eca624f4e0c3de2ad4ad7d9c915cd16e3f1e41066a69d3799a5bb19689a8e0d894e00649cae7f7d27beadbab2b04d4083f7f2ce68e5ee65af229f55"},
- {"id":"ba9a8a1a8afb0b53fb5d4fa3f5130fe557a8d8d56fac7af9ad3443531d2a2933","pubkey":"95181f816dc80a141e9158a2a398bec37f580f4309633321a1ca53757bf08794","created_at":1690373626,"kind":1,"tags":[["e","e15b386824fbfdcbf1b50b8860f03062cef534a3ea5339cc837536fb2a58465e"],["p","d3d2b986796c9e2067c58fc9017cace5adc1876e9b674719078d5c13d82e54a9"]],"content":"I see your tests. 1-3 🫡","sig":"cc8d3af65578cc2436878fb5dbf3ac27751b50f91b02dd944094e9f9f2965193e32fb20ce68cfe5555ecfb4b13fd06b9cdb673dc71e5db47199b8e6cde2d2e60"},
- {"id":"b0425132a3dd4142a0f78986166aaa28021cc8fb440c95c321a95afce3d5e056","pubkey":"d3d2b986796c9e2067c58fc9017cace5adc1876e9b674719078d5c13d82e54a9","created_at":1690025593,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"]],"content":"test 2","sig":"0226d5836991836ba105cd483def0baa4255bad19a8213ae483c0168efb4de6e493233ed1fdc83e038d5227e0254dc0b461e7a7fa8032a7eada56b9840e8762f"},
- {"id":"e9bb4e2d56bd2be2952570bd52b102c23444a62ada5b78ba086f086d9147651a","pubkey":"d3d2b986796c9e2067c58fc9017cace5adc1876e9b674719078d5c13d82e54a9","created_at":1690025573,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"]],"content":"test","sig":"ba66f1123e93a70645f3f01814378d23609cc2fda38f083410e0e06b037dc9fc45d139b690e2e892278f768c97a03e02e0a9cd0a8b385271db180e1e8838784d"},
- {"id":"4d7b21c462f3fbf27a1882dbaaec4e99e5a5d18a2c18d1f2d1f0736684ad157c","pubkey":"84142dede040f7e0852ba867cfa921986b7b7e92c4b7bc5c72fbd2a4577545e4","created_at":1688308094,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"]],"content":"testing","sig":"9f991643229822577c967d6d71831a6d98993dbbd3613b04abda0907a944b8eb20f6f8477bc8e922308e2e2135243ab78a31a93f285ee6215ac5536141a7d95d"},
- {"id":"9cdbced750e6b1e1274b7df47cb433f565414dd08c897167eba761eadee841cc","pubkey":"726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11","created_at":1687990252,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"]],"content":"Running zapthreads","sig":"1cd2209548f5a791807e4a8aeafbf3a2fb28cee013ad5f444f86dffd448668c05480d7714b892314f5fa50b7c65a23708362c1d5b0cb669c64cfb06ca8486ba9"},
- {"id":"5799aac7a9b06f3cae3d3791b79df29d14173515cfdbf34398aab73ed4a44121","pubkey":"726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11","created_at":1687996513,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"],["e","9cdbced750e6b1e1274b7df47cb433f565414dd08c897167eba761eadee841cc","","reply"]],"content":"Testing nested comments","sig":"335f9969d23e28fef45adfda63da236f1d4d72dfb5873fc78961d47ee948e9333632ce8a45df3c09fc431cf8d29d2fb99bea9c9a4b3502465b05a0328d563231"},
- {"id":"7a18dda355525d468b31bba4fa947cba98cc19048d4a3099d5e9ba045d878c26","pubkey":"45c41f21e1cf715fa6d9ca20b8e002a574db7bb49e96ee89834c66dac5446b7a","created_at":1680618145,"kind":1,"tags":[["e","1a1fe6c838400f3347f97a9adce08c10068f3bc35a279520c911b025c387b061",""],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93",""],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599",""],["title","Purple Text, Orange Highlights"],["summary","Using nostr as a base layer to create, consume, and monetize long-form content."],["image","https://dergigi.com/assets/images/reader.jpg"],["published_at","1680613802"]],"content":"good read. the highlighted part to be a shareable event 1 idea seems interesting, might explore it on flycat","sig":"ee913e997b907a03300616c36cffa12d2362932bd5e249cde39a44ec1250c7622703dddac167b093c3e3a28d41ebfeb9aed9d663d82bea9f8deaed4e342248dc"},
- {"id":"a3b3825af621727f9af3bd77392fe38c04d71658024916af7fe4c5867ef73eaa","pubkey":"619af6a60b3fe4c733aaca061c522cc9c7cf1d87ef4c908facc5ed936d3bdf23","created_at":1681941926,"kind":1,"tags":[["p","330fb1431ff9d8c250706bbcdc016d5495a3f744e047a408173e92ae7ee42dac"],["e","d7ef8220e3521779e2e0c3ff0afb9738f241e44ace8ed1d5237cab36330a69d0"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"Good or bad time?","sig":"5629e21769b018c7f050ad35e20c3926e5f9c415ebcd54656e874cff7948a9117de6fb32d18f993b7c96f480f3e4baffb079a3db7c126c74b0188006bba41768"},
- {"id":"c5ad64b1b72776a068c39f4549d089032432814a146849eba0650d1b329fb285","pubkey":"126df36019a793922eae86bfcfedc5240b68d67953892237e3eb500ab92140bf","created_at":1681911407,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","f2610be11393f884c020119316d3304bc06c48ab01f9cbcf5a5a8290031bf20e"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"Cool","sig":"62240add277f55e896ae24276fdae60f1580be9de4889e546fd2141b73e07b3e48f225a57914a5e0e5a2fd87570a1dfe1af4f869b355a7c455aa77f395c10bdb"},
- {"id":"6a58f8315af5badb1bdaeb5489417b94621a4d8e192ae2fedcca0c5dcf0c9cd4","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1681821926,"kind":1,"tags":[["p","73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc"],["e","81a6379301d7b517f96cc7d070b5b30fb43db97c5c814be3bc9f58d8fcbaf190"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"test 5","sig":"eeba84f251d8d79e0f16213b766aee8079a3a5018407c3f7d1e8161c762e6a735bb695ac54898e04d7abe245ec12f24d680927351d37a6f31557344adeebd3c2"},
- {"id":"6e9bb03c7c40d67fec0d0bb872548ec207ba0ac4533efa137d7bcaca9fb4b191","pubkey":"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","created_at":1681755803,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","9defafc0783f16b2ef9ac02d2e808a103d5d38f12905b7840420634af67d0821"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"test1","sig":"17c2489ee28dc32df68ebf87ced6f5f9af0e3f091b31298fece408acba43236bc6c9f50d756f61bcf46a204e043141535e0cd281d541146cbfac259814cd2cbe"},
- {"id":"e2ae784b239cac4bad38136e4bd758b87dd261b659ef460450064bf9073edcb3","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1681648013,"kind":1,"tags":[["p","73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc"],["e","81a6379301d7b517f96cc7d070b5b30fb43db97c5c814be3bc9f58d8fcbaf190"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"powerful quote 👀","sig":"eda9e5565000892dacab78469936d96aad7fb0dc0828630563b8ccb731f08e704eacfefff246b6bb668332e046f5650f2a9e4186c108f07cba07eefc775c6c58"},
- {"id":"45d4fc726f2cc5b524be862c14fdadc1a24b25b8c6c011eedf2d2909589263e7","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1681821952,"kind":1,"tags":[["p","73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc"],["e","e2ae784b239cac4bad38136e4bd758b87dd261b659ef460450064bf9073edcb3"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"test 7","sig":"e2c35766479d549700f4dbaeae385cfdacee1275ddf24777643ca7d7335c6d1baf7f89ac25d1dc90688bc49f8d2f5e4857d2234035291628a0c0ac760485bab0"},
- {"id":"7a4a2419824669f07081abe2132f8cc0027efbce066ccdf187c897bb7ffa5dc3","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1681821936,"kind":1,"tags":[["p","73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc"],["e","e2ae784b239cac4bad38136e4bd758b87dd261b659ef460450064bf9073edcb3"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"test 5","sig":"875339b3e666fb1013d6563552997dcdd02e87b1f290ad25b0d7dd085d2f87dd10a4e6c20dfe3fc109d1809733b93675505341fe149e4399c42371c743fafc73"},
- {"id":"674c62f84afdc045bc3623ea132d90afdfe4b64249807f65302231115af5406d","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1681648340,"kind":1,"tags":[["p","73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc"],["e","e2ae784b239cac4bad38136e4bd758b87dd261b659ef460450064bf9073edcb3"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"nested reply","sig":"f0f4fdd2d34e59255137c457b9d42fa9b5357e824f3154340de964616f6daf21e3b03491363b0060f97c5061445d7b2854eed328c5cced751b2b480e5ce8ce9a"},
- {"id":"d54761f672669ea4f4b7592f3b0a30ee28de340b0a7e46b91af94e66905171c9","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1681648345,"kind":1,"tags":[["p","73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc"],["e","674c62f84afdc045bc3623ea132d90afdfe4b64249807f65302231115af5406d"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"another one","sig":"dd732eb380542dd8068449d5617bb8b6c1dfe1495c9facbd46aad3ca9b084122479df30861b8e85e13e7b0c9bf9bce589b7fc062daa086c1ff8a0b1d1ffc7aa7"},
- {"id":"00813a18ac9084cd0948c27027a980e34039a3011f30279a8b52ad87da5a3031","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1681648351,"kind":1,"tags":[["p","73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc"],["e","d54761f672669ea4f4b7592f3b0a30ee28de340b0a7e46b91af94e66905171c9"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"yet another one","sig":"6332c8a441439487898fd5b423e42dde7d1d7aadfcf9b36defd78d76d1685ba099ac86ae00a1dac6ed5e8a166422d59bbb93bfccb2596dc1c5461efcfcf345b4"},
- {"id":"87a5bd25aa084cefb3357fc9c2a5b327254fab35fdd7b2d4bd0acddc63d0abe8","pubkey":"726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11","created_at":1687990701,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"],["e","00813a18ac9084cd0948c27027a980e34039a3011f30279a8b52ad87da5a3031","","reply"]],"content":"and another one more","sig":"6a385fb259ff40d9b6bd8bb1d5a2e20057ec1c5b5ed2c307fbc40e4a96d4c191b80b6ea3f3f5748c907d4b0e6e64a92210a0be2889af856ad084ec6806f3cbd8"},
- {"id":"512962dbada5fd5015fc727a107d5c3f569662de67eab8e5da5a8065012cf11e","pubkey":"7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194","created_at":1688194800,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"],["e","87a5bd25aa084cefb3357fc9c2a5b327254fab35fdd7b2d4bd0acddc63d0abe8","","reply"]],"content":"another one","sig":"b4bc4d8206a08de0a918043129b27c8863d17f60e4f58b4d2b535326625870d32640b04f247eb9eb5df1d62fe98c8e1b0341bdd119655553488300ec9d5b4036"},
- {"id":"ce6e32e3e17b6901d2cc70b60f3743e24f885bb6e9da6d88cff516079eac1883","pubkey":"726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11","created_at":1688234921,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"],["e","512962dbada5fd5015fc727a107d5c3f569662de67eab8e5da5a8065012cf11e","","reply"]],"content":"yet another one, testing 0.0.3","sig":"13bd41c4029c6a7ee41cb03d12e831e9e9b6e14d43a61c78a72657070b45385ba2ce98e8121049fbcbdb7b2dd777c36c7867ba70b5e6a4798a9b866910ae5b62"}
- ]
- """.trimIndent()
+ companion object {
+ val keyPair = KeyPair()
+ val account =
+ Account(
+ settings = AccountSettings(keyPair = keyPair),
+ signer = NostrSignerInternal(keyPair),
+ scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
+ geolocationFlow = MutableStateFlow(LocationState.LocationResult.Loading),
+ cache = LocalCache,
+ client =
+ NostrClient(
+ OkHttpWebSocket.Builder {
+ OkHttpClient
+ .Builder()
+ .followRedirects(true)
+ .followSslRedirects(true)
+ .build()
+ },
+ CoroutineScope(Dispatchers.IO + SupervisorJob()),
+ ),
+ )
- val header =
- """
- {
- "content": "Not too long ago, I tried to paint a picture of what\na [vision for a value-enabled web][vew]\ncould look like. Now, only a couple of months later,\nall this stuff is being built. On nostr, and on lightning. Orange and\npurple, a match made in heaven.\n\nIt goes without saying that I'm beyond delighted. What a time to be alive!\n\n## nostr\n\nHere's the thing that nostr got right, and it's the same thing that\nBitcoin got right: information is easy to spread and hard to stifle.[^fn-stifle]\nInformation can be copied quickly and perfectly, which is, I believe,\nthe underlying reason for its desire to be free.\n\n[^fn-stifle]: That's a [Satoshi quote][stifle], of course: \"Bitcoin's solution is to use a peer-to-peer network to check for double-spending. In a nutshell, the network works like a distributed timestamp server, stamping the first transaction to spend a coin. It takes advantage of the nature of information being easy to spread but hard to stifle.\"\n\n[stifle]: https://satoshi.nakamotoinstitute.org/posts/p2pfoundation/1/\n\nEasy to spread, hard to stifle. That's the base reality of the nature\nof information. As always, the smart thing is to work with nature, not\nagainst it.[^1] That's what's beautiful about the orange coin and\nthe purple ostrich: both manage to work with the peculiarities of\ninformation, not against them. Both realize that information can and should be\ncopied, as it can be perfectly read and easily spread, always. Both understand\nthat resistance to censorship comes from writing to many places, making the cost\nof deletion prohibitive.\n\n> Information does not just want to be free,\n> it longs to be free. Information expands to fill the available\n> storage space. Information is Rumor's younger, stronger cousin;\n> Information is fleeter of foot, has more eyes, knows more, and\n> understands less than Rumor.\n>\n> Eric Hughes, [A Cypherpunk's Manifesto][manifesto]\n\n[manifesto]: https://nakamotoinstitute.org/static/docs/cypherpunk-manifesto.txt\n\nNostr is quickly establishing itself as a base layer for information exchange,\none that is identity-native and value-enabled. It is distinctly different from\nsystems that came before it, just like Bitcoin is distinctly different from\nmonies that came before it.\n\nAs of today, the focus of nostr is mostly on short text notes, the so-called\n\"type 1\" events more commonly known as *tweets*.[^fn-kinds] However, as you should be aware\nby now, nostr is way more than just an alternative to twitter. It is a new\nparadigm. Change the note kind from `1` to `30023` and you don't have an\nalternative to Twitter, but a replacement for Medium, Substack, and all the\nother long-form platforms. I believe that special-purpose clients that focus on\ncertain content types will emerge over time, just like we have seen the\nemergence of special-purpose platforms in the Web 2.0 era. This time, however,\nthe network effects are cumulative, not separate. A new paradigm.\n\nLet me now turn to one such special-purpose client, a nostr-based reading app.\n\n[^fn-kinds]: Refer to the various NIPs to discover the multitude of [event kinds][kinds] defined by the protocol.\n\n[kinds]: https://github.com/nostr-protocol/nips#event-kinds\n[nip23]: https://github.com/nostr-protocol/nips/blob/master/23.md\n\n## Reading\n\nI'm constantly surprised that, even though most people do read a lot\nonline, very few people seem to have a reading workflow or reading\ntools.\n\nWhy that is is anyone's guess, but maybe the added value of such tools\nis not readily apparent. You can just read the stuff right there, on the\nad-ridden, dead-ugly site, right? Why should you sign up for another\nsite, use another app, or bind yourself to another closed platform?\n\nThat's a fair point, but the success of Medium and Substack shows that\nthere is an appetite for clean reading and writing, as well as providing\navenues for authors to get paid for their writing (and a willingness of\nreaders to support said authors, just because).\n\nThe problem is, of course, that all of these platforms are *platforms*,\nwhich is to say, walled gardens that imprison readers and writers alike.\nWorse than that: they are fiat platforms, which means that\npermissionless value-flows are not only absent from their DNA, they are\noutright impossible.[^2]\n\nNostr fixes this.\n\n\n\nThe beauty of nostr is that it is not a platform. It's a protocol,\nwhich means that you don't have to sign up for it---you can create an\nidentity yourself. You don't have to ask for permission; you just *do*,\nwithout having to rely on the benevolence of whatever dictator is in\ncharge of the platform right now.\n\nNostr is *not* a platform, and yet, powerful tools and services can be\nbuilt and monetized on top of it. This is good for users, good for\nservice providers, and good for the network(s) at large. Win-win-win.\n\nSo what am I talking about, exactly? How can nostr improve everyone's\nreading (and writing) experience?\n\nAllow me to paint a (rough) picture of what I have in mind. Nostr\nalready supports private and public bookmarks, so let's start from\nthere.\n\nImagine a special-purpose client that scans all your bookmarks for long-form\ncontent.[^fn-urls] Everything that you marked to be read later is shown in an orderly\nfashion, which is to say searchable, sortable, filterable, and displayed without\ndistractions. Voilà, you have yourself a reading app. That's, in essence, how\nPocket, Readwise, and other reading apps work. But all these apps are walled\ngardens without much interoperability and without direct monetization.\n\n[^fn-urls]: In the nostr world long-form content is simply markdown as defined in [NIP-23][nip23], but it could also be a link to an article or PDF, which in turn could get [converted into markdown][readability] and posted as an event to a special relay.\n\n[readability]: https://github.com/mozilla/readability\n\nBitcoin fixes the direct monetization part.[^fn-v4v] Nostr fixes the interoperability part.\n\n[^fn-v4v]: ...because Bitcoin makes [V4V][busking] practical. (Paywalls are not the way.)\n\nAlright, we got ourselves a boring reading app. Great. Now, imagine that\nusers are able to highlight passages. These highlights, just like\nbookmarks now, could be private or public. When shared publicly,\nsomething interesting emerges: an overlay on existing content, a lens on\nthe written Web. In other words: *swarm highlights*.\n\nImagine a visual overlay of all public highlights, automatically shining\na light on what the swarm of readers found most useful, insightful,\nfunny, etc.\n\n\n\nFurther, imagine the possibility of sharing these highlights as a \"type 1\" event\nwith one click, automatically tagging the highlighter(s)---as well as the\nauthor, of course---so that eventual sat-flows can be split and forwarded\nautomatically.\n\n\n\nVoilà, you have a system that allows for value to flow back to those who\nprovide it, be it authors, editors, curators, or readers that willingly\nslog through the information jungle to share and highlight the best\nstuff (which is a form of curation, of course).\n\nZaps make nostr a defacto address book[^fn-pp] of payment information, which is\nto say lightning addresses, as of now. Thanks to [nostr wallet connect][nwc] (among\nother developments), sending sats ~~will soon be~~ is already as\nfrictionless as leaving a like.\n\n[^fn-pp]: The Yellow Pages are dead, long live [The Purple Pages](http://purplepag.es/)!\n\nValue-for-value and participatory payment flows are something that\ntraditional reading apps desperately lack, be it Pocket, Instapaper,\nReadwise, or the simple reading mode that is part of every browser.\n\nA neat side-effect of a more structured way to share passages of text is\nthat it enables semi-structured discussions around said\npassages---which could be another useful overlay inside\nspecial-purpose clients, providing context and further insights.[^5]\n\nFurther, imagine the option of seamlessly switching from text-on-screen\nto text-to-speech, allowing the user to stream sats if desired, as\nPodcasting 2.0 clients already do.[^3]\n\nImagine user-built curations of the best articles of the week, bundled\nneatly for your reading pleasure, incentivized by a small value split\nthat allows the curator to participate in the flow of sats.\n\nYou get the idea.\n\nI'm sure that the various implementation details will be hashed out,\nbut as I see it, 90% of the stuff is already there. Maybe we'll need\nanother NIP or two, but I don't see a reason why this can't be\nbuilt---and, more importantly: I don't see a reason why it wouldn't\nbe sustainable for everyone involved.\n\nMost puzzle pieces are already there, and the rest of them can probably\nbe implemented by custom event types. From the point of view of nostr,\nmost everything is an event: bookmarks are events, highlights are\nevents, marking something as read is an event, and sharing an excerpt or\na highlight is an event. Public actions are out in the open, private\nactions are encrypted, the data is not in a silo, and everyone wins.\nEspecially the users, those who are at the edge of the network and\nusually lose out on the value generated.\n\nIn this case, the reading case, the users are mostly \"consumers\" of\ncontent. What changes from the producing perspective, the perspective of\nthe writer?\n\n## Writing\n\nBack to the one thing that nostr got right: information is easy to\nspread but hard to stifle. In addition to that, digital information can\nbe copied perfectly, which is why it shouldn't matter where stuff is\npublished in the first place.\n\nAllow me to repeat this point in all caps, for emphasis: **IT SHOULD NOT\nMATTER WHERE INFORMATION IS PUBLISHED**, and, maybe even more\nimportantly, it shouldn't matter if it is published in a hundred\ndifferent places at once.[^fn-torrents]\n\nWhat matters is trust and accuracy, which is to say, digital signatures\nand reputation. To translate this to nostr speak: because every event is\nsigned by default, as long as you trust the person behind the signature,\nit doesn't matter from which relay the information is fetched.\n\nThis is already true (or mostly true) on the regular web. Whether you\nread the internet archive version of an article or the version that is\npublished by an online magazine, the version on the author's website,\nor the version read by some guy that has read more about Bitcoin than\nanyone else you know[^fn-guy]---it's all the same, essentially. What matters\nis the information itself.\n\n[^fn-guy]: There is only one such guy, as we all know, and it's this Guy: nostr:npub1h8nk2346qezka5cpm8jjh3yl5j88pf4ly2ptu7s6uu55wcfqy0wq36rpev\n\nPractically speaking, the source of truth in a hypernostrized world is---you\nguessed it---an event. An event signed by the author, which allows for\nthe information to be wrapped in a tamper-proof manner, which in turn\nallows the information to spread far and wide---without it being\nhosted in one place.\n\nThe first clients that focus on long-form content already exist, and I expect\nmore clients to pop up over time.[^4] As mentioned before, one could easily\nimagine [prism-like value splits][prism] seamlessly integrated into these\nclients, splitting zaps automatically to compensate writers, editors,\nproofreaders, and illustrators in a V4V fashion. Further, one could imagine\nvarious compute-intensive services built into these special-purpose clients,\nsuch as GPT Ghostwriters, or writing aids such as Grammarly and the like. All\nthese services could be seamlessly paid for in sats, without the requirement of\nany sign-ups or the gathering of any user data. That's the beauty of [money\nproper][rediscovery].\n\n\n\nPlagiarism is one issue that needs to be dealt with, of course. Humans\nare greedy, and some humans are assholes. Neither bitcoin nor nostr\nfixes this. However, while plagiarism detection is not necessarily\ntrivial, it is also not impossible, especially if most texts are\npublished on nostr first. Nostr-based publishing tools allow for\nOpenTimestamp attestations thanks\nto [NIP-03](https://github.com/nostr-protocol/nips/blob/master/03.md),\nwhich in turn allows for plagiarism detection based on \"first seen\"\nlookups.\n\nThat's just one way to deal with the problem, of course. In any case,\nI'm confident that we'll figure it out.\n\n## Value\n\nI believe that in the open ~~attention~~ information economy we find\nourselves in, value will mostly derive from effective curation,\ndissemination, and transmission of information, *not* the exclusive\nownership of it.\n\nAlthough it is still early days,\nthe [statistics](https://stats.podcastindex.org/v4v) around Podcasting\n2.0 and [nostr zaps](https://zaplife.lol/) clearly show that (a) people\nare willing to monetarily reward content they care about, and (b) the\nwillingness to send sats *increases* as friction *decreases*.\n\nThe ingenious thing about boostagrams and zaps is that they are direct\nand visible, which is to say, public and interactive. They are neither\nregular transactions nor simple donations---they are something else\nentirely. An unforgable value signal, a special form of gratitude and\nappreciation.\n\nContrast that with a link to Paypal or Patreon: impersonal, slow,\nindirect, and friction-laden. It's the opposite of a super-charged\ninteraction.\n\nWhile today's information jungle increasingly presents itself in the\nform of (short) videos and (long-form) audio, I believe that we will see\na renaissance of the written word, especially if we manage to move away\nfrom an economy built around attention, towards an economy built upon\nvalue and insight.\n\nThe orange future now has a purple hue, and I believe that it will be as\nbright as ever. We just have a lot of building to do.\n\n---\n\n## Further Reading\n\n- [A Vision for a Value-Enabled Web][vew]\n- [The Freedom of Value][busking]\n- [The Rediscovery of Money][prism]\n- [Lightning Prisms][rediscovery]\n\n[vew]: https://dergigi.com/vew\n[prism]: https://dergigi.com/prism\n[rediscovery]: https://dergigi.com/rediscovery\n[busking]: https://dergigi.com/busking\n\n## NIPs and Resources\n\n- [Nostr Resources][nr]\n- [value4value.info](https://value4value.info/)\n- [nips.be](https://nips.be/)\n- [NIP-23: Long-form content](https://github.com/nostr-protocol/nips/blob/master/23.md)\n- [NIP-57: Event-specific zap markers](https://github.com/nostr-protocol/nips/blob/master/57.md)\n- [NIP-47: Nostr Wallet Connect](https://github.com/getAlby/nips/blob/master/47.md)\n- [NIP-03: OpenTimestamps attestations for events](https://github.com/nostr-protocol/nips/blob/master/03.md)\n\nOriginally published on [dergigi.com](https://dergigi.com/reader)\n\n---\n\n[^1]: Paywalls work against this nature, which is why I consider them misguided at best and incredibly retarded at worst.\n\n[^2]: Fiat doesn't work for the [value-enabled web][vew], as fiat rails can never be open and permissionless. Digital fiat is never money. It is---and always will be---[credit][rediscovery].\n\n[^3]: Whether the recipient is a text-to-speech service provider or a human narrator doesn't even matter too much, sats will flow just the same.\n\n[^4]: [BlogStack](https://blogstack.io/) and [Habla](https://habla.news/) being two of them.\n\n[^5]: Use a URI as the discussion base (instead of a highlight), and you got yourself a [Disqus](https://disqus.com/) in purple feathers!\n\n[^fn-torrents]: That's what torrents got right, and [ipfs] for that matter.\n\n[nr]: https://nostr-resources.com\n[nwc]: https://nwc.getalby.com/\n[ipfs]: https://fiatjaf.com/d5031e5b.html\n",
- "created_at": 1680614039,
- "id": "9517aa60334990596fdea4493c1bde429c55e9cefb84d8d88a6758c4e3ebcabc",
- "kind": 30023,
- "pubkey": "6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93",
- "sig": "c8c8c7ac8c288f6a6e1090e0e5f7eed85daaaba03eeab0a4936535f95ce066c08723bedcfb80a6af4941a9fafb9fe351950b9376d13ff0e81030cd4391840f04",
- "tags": [
- ["d","1680612926599"],
- ["title","Purple Text, Orange Highlights"],
- ["summary","Using nostr as a base layer to create, consume, and monetize long-form content."],
- ["published_at","1680613802"],
- ["t","nostr"],
- ["t","reading"],
- ["t","writing"],
- ["t","readability"],
- ["t","highlights"],
- ["t","pocket"],
- ["t","instapaper"],
- ["t","readwise"],
- ["t","disqus"],
- ["t","v4v"],
- ["t","value4value"],
- ["image","https://dergigi.com/assets/images/reader.jpg"],
- ["p","b9e76546ba06456ed301d9e52bc49fa48e70a6bf2282be7a1ae72947612023dc"]
- ]
- }
- """.trimIndent()
+ val db =
+ """
+ [
+ {"id":"741f5367a9415f4d6f19c0f57a1e4647c8ed8309b53b0da2d82fc4ebfba03b2c","pubkey":"d85c99afd244911e0aaf800cbea4221df557f06f8a4ff2cbe84b24e0b9e728fc","created_at":1684674845,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"Getting my head around this","sig":"069874040bac26a219777fc0f90b8f4df71e38c30e3e6a953d53222499d8e0c5a8f32c6b4204d14eb335bb654f01c5610372d9dc00062284b8e0f2bb98c7ed85"},
+ {"id":"22323fc72b4c37f93ea21f6069684339ce5f63111161c81f2aa3de4a21bfe83b","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1683571810,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"test","sig":"f6d3bcf0f8e07d06720a2527f95910ee5a66aa889dd9261514921a155bbc3e9a3c7d721f5a2ec948cd795183f85dbe80f2ee2e36663f9d1de33fc03274ccb9d9"},
+ {"id":"14525fcaae530a029f782fd361dd0cd66634c3e23020bd19e66fe11c1e254e32","pubkey":"afd563434a737334d69db899e4a32fe38d73a182bb6d1e91d83a2c4c4e04737c","created_at":1682968749,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"This is most amazing thing yet for nostr!","sig":"3dcbb058b8fa6e6f388f5b1a26d21b888dbc91bb831b0ed80ee30d9d2494fbbbb40dac9daf637b948c3afe1f32c2c662eca8d7acc0ba31f23b89edc8ade99631"},
+ {"id":"98ae0d6d10e494ed0bf70feb577e8225c6a6732c7af3d29f88bfe1b87d4439e6","pubkey":"83fd07de9b763334cc9d46f2785c2558e6c2eabfe7d0c6ec214667cbaec50d47","created_at":1681995243,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"true if big","sig":"bb7d97e81207096d8615ebb0458c3023fa1b8f3aac463f3f0293ae94e2b90d043ad371067ce984a2ab2ba49cd7432a553f128b8d2d678238551538ad657bd75c"},
+ {"id":"36e262e71b7e8bcae946f69885b8c3614e318e82864437342cf50e8b9ab7229d","pubkey":"b111d517452f9ef015e16d60ae623a6b66af63024eec941b0653bfee0dd667d4","created_at":1681979196,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"this is wonderful. great work.","sig":"205a46d867c22cbb09c7dcc4351330a95304c0d80540c85b9ea39f7f464ea60df1ef5006c1f93b862536f04d8a7855e4cc2a915d7a01437cd30f7f12c716b239"},
+ {"id":"37725c2924ca267d66c2c27c2dae65550c07e7b883034cf1ea69671883430642","pubkey":"90b9bec74789688e515125596ab6350bfe646176ac75742275063922c5fea010","created_at":1681944950,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"Epic!","sig":"c461b97a3cdc5cf967e6f32c59c94b1b912c296feb1cbb7943c81731e558b10a3674982d6990567260d9fb033f5a3d3db8984b1eb362715d68ba46034a8b1b86"},
+ {"id":"8ba54cfb6375270e8ae97a7e0992c1a0dbaa4cd46af8309d67a839e86789fde6","pubkey":"c48e29f04b482cc01ca1f9ef8c86ef8318c059e0e9353235162f080f26e14c11","created_at":1681944377,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"This is incredible... Bravo, Pablo, and shoutout to Gigi.","sig":"fb1a84d4859742b001b810c98a5e9301bc861fa2d65a711cb21c57f6e62c98f27e5b0c0c6432ef1111bc6cc52aa8cb0ae909d28af50c497d125950bfaeade475"},
+ {"id":"53410bc6d47e87f3f18ecbc93c716b5a6ef8ee3805516b2ff4d155154a685b7c","pubkey":"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","created_at":1681945005,"kind":1,"tags":[["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["e","8ba54cfb6375270e8ae97a7e0992c1a0dbaa4cd46af8309d67a839e86789fde6"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["p","c48e29f04b482cc01ca1f9ef8c86ef8318c059e0e9353235162f080f26e14c11"]],"content":"Thank you, my friend 🤙 ","sig":"3d2fbcefdfac2183601076c8f9e518e05620cefe74cba233684cce931a69b51befa329c94734e434805bd9382ff3dad36e42d1955d4e47c822e973ba69b7377f"},
+ {"id":"e383476cb1ce5accde11d4b1338424fa32c3724cf96e6214af8e5e852981728a","pubkey":"50de492cfe5472450df1a0176fdf6d915e97cb5d9f8d3eccef7d25ff0a8871de","created_at":1681932060,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"https://nostr.build/p/nb5753.png","sig":"9df6e3872f420ed5225aaa242f1c806b4739f17e265f02ca593a3c31c5fb5cb48705ee6bc0510f81f123527aa96ec4d07adf829362e2196bd7b833deded4c2b8"},
+ {"id":"e2d8aaed336d3c0f73a9ca46a89fdb2da62a6d172936a91b0067a68797b3bcb8","pubkey":"50de492cfe5472450df1a0176fdf6d915e97cb5d9f8d3eccef7d25ff0a8871de","created_at":1681931869,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"Very Cool, I started playing around!","sig":"5980b2d0ef114d50c5f45b5d0c55bfc56b16a0a1807774ec2a86c4e352e17e3ec78877c73f2fedfbfd097a018df8e965107d6efd4ad9c0a0e1ecc89332d51cec"},
+ {"id":"b92e4d6a5d0e8d1d2d2421044b84a4d11f2188261c55145d782b1b6bf0995009","pubkey":"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","created_at":1681932193,"kind":1,"tags":[["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f","","root"],["e","e2d8aaed336d3c0f73a9ca46a89fdb2da62a6d172936a91b0067a68797b3bcb8","","reply"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"]],"content":"love seeing it!!!","sig":"47850c205f1fa6d8836cda347c7d12c4307d35a110629eac48b4fff26600a14a5c5c8a7877b851db062a89e0157be112eae60ba0594525c1cc4d355906368dbb"},
+ {"id":"da13e14cc8bcc243e0373dde14533d3829b8b621e214ca3c99c90f3dd9e11b8a","pubkey":"b488b594d84bb3e7c8421a4b91b19a006dc13d0c00da304a0263355d02195f04","created_at":1681931637,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"Great thoughts ","sig":"34c81430952508746fd5d5a4c4f7df0f5762aeb17d075a74f7b0daf387a7b8772edf4152731cff6650219e1aa9f8f63924410baa03091a698af0513cbf57e0af"},
+ {"id":"b5234d90a1543ba60765c57ac3fc7140129a4ac28bbd013531ec9b85e256ea55","pubkey":"b488b594d84bb3e7c8421a4b91b19a006dc13d0c00da304a0263355d02195f04","created_at":1681931560,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"I like comments ","sig":"6915f4c2a08622339957495931eabc37f8d0c50ab1f3ec42327b67d15e50f50ff4bc302848b606e6d7c170696f8d744e6f896223f30ecb39a3adffe58020ff6f"},
+ {"id":"fc4e4a1230b002e4ae08251a0b26107de7f518800188b7a504f5de62d8b07996","pubkey":"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","created_at":1681834615,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"This is exactly why I built this. I am enamored by this idea.","sig":"bdd1b3532efa3badeb3807fe74d31c25f17c84bbe39fe3c692d3d73efa39408089843ebb3c049840f2d525e6849f8254179a888bedc776ee7ad5e6997dc10c44"},
+ {"id":"d4a0b4f08d98d82a04292654ec132723cc2cf3fa24ffb6c0833426cb9372f4d5","pubkey":"39a8b17475be0db44e313f9fd032ffde183c8abd6498e4932a873330d2cd4868","created_at":1680618462,"kind":1,"tags":[["e","44d3ecbb5752e96becc330d9b56352ea595d37b4c55edd8701fd24f18df5eee2"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["p","39a8b17475be0db44e313f9fd032ffde183c8abd6498e4932a873330d2cd4868"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"Unfortunately when boosting this it gets broken 🥹\n#[1]","sig":"c4a4197df4fec008f2f565170d09c46f0b9a7217ed90432cb53d1dddf70ad8520722b6e4f04ec9e52dd219ee01387c6191f449cd10a6c2488b1d2866424b7a8a"},
+ {"id":"8cdc4676aca93bbafcfbe6784f9b2df54e8ca20fbe69ba55fda487736bfdb7f6","pubkey":"6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93","created_at":1680636068,"kind":1,"tags":[["e","44d3ecbb5752e96becc330d9b56352ea595d37b4c55edd8701fd24f18df5eee2"],["e","d4a0b4f08d98d82a04292654ec132723cc2cf3fa24ffb6c0833426cb9372f4d5"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["p","39a8b17475be0db44e313f9fd032ffde183c8abd6498e4932a873330d2cd4868"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"we early ","sig":"b8026e43c61b491014915caf79289968b87e52aa87a487ebecbf6cb2b3c07d460f2ab1a86da99b1c5410a0f5d14e77cf27f78c56bf1ab709f0d616ad2711de78"},
+ {"id":"e15b386824fbfdcbf1b50b8860f03062cef534a3ea5339cc837536fb2a58465e","pubkey":"d3d2b986796c9e2067c58fc9017cace5adc1876e9b674719078d5c13d82e54a9","created_at":1690025613,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"]],"content":"test 3","sig":"1a34f9bb9eca624f4e0c3de2ad4ad7d9c915cd16e3f1e41066a69d3799a5bb19689a8e0d894e00649cae7f7d27beadbab2b04d4083f7f2ce68e5ee65af229f55"},
+ {"id":"ba9a8a1a8afb0b53fb5d4fa3f5130fe557a8d8d56fac7af9ad3443531d2a2933","pubkey":"95181f816dc80a141e9158a2a398bec37f580f4309633321a1ca53757bf08794","created_at":1690373626,"kind":1,"tags":[["e","e15b386824fbfdcbf1b50b8860f03062cef534a3ea5339cc837536fb2a58465e"],["p","d3d2b986796c9e2067c58fc9017cace5adc1876e9b674719078d5c13d82e54a9"]],"content":"I see your tests. 1-3 🫡","sig":"cc8d3af65578cc2436878fb5dbf3ac27751b50f91b02dd944094e9f9f2965193e32fb20ce68cfe5555ecfb4b13fd06b9cdb673dc71e5db47199b8e6cde2d2e60"},
+ {"id":"b0425132a3dd4142a0f78986166aaa28021cc8fb440c95c321a95afce3d5e056","pubkey":"d3d2b986796c9e2067c58fc9017cace5adc1876e9b674719078d5c13d82e54a9","created_at":1690025593,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"]],"content":"test 2","sig":"0226d5836991836ba105cd483def0baa4255bad19a8213ae483c0168efb4de6e493233ed1fdc83e038d5227e0254dc0b461e7a7fa8032a7eada56b9840e8762f"},
+ {"id":"e9bb4e2d56bd2be2952570bd52b102c23444a62ada5b78ba086f086d9147651a","pubkey":"d3d2b986796c9e2067c58fc9017cace5adc1876e9b674719078d5c13d82e54a9","created_at":1690025573,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"]],"content":"test","sig":"ba66f1123e93a70645f3f01814378d23609cc2fda38f083410e0e06b037dc9fc45d139b690e2e892278f768c97a03e02e0a9cd0a8b385271db180e1e8838784d"},
+ {"id":"4d7b21c462f3fbf27a1882dbaaec4e99e5a5d18a2c18d1f2d1f0736684ad157c","pubkey":"84142dede040f7e0852ba867cfa921986b7b7e92c4b7bc5c72fbd2a4577545e4","created_at":1688308094,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"]],"content":"testing","sig":"9f991643229822577c967d6d71831a6d98993dbbd3613b04abda0907a944b8eb20f6f8477bc8e922308e2e2135243ab78a31a93f285ee6215ac5536141a7d95d"},
+ {"id":"9cdbced750e6b1e1274b7df47cb433f565414dd08c897167eba761eadee841cc","pubkey":"726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11","created_at":1687990252,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"]],"content":"Running zapthreads","sig":"1cd2209548f5a791807e4a8aeafbf3a2fb28cee013ad5f444f86dffd448668c05480d7714b892314f5fa50b7c65a23708362c1d5b0cb669c64cfb06ca8486ba9"},
+ {"id":"5799aac7a9b06f3cae3d3791b79df29d14173515cfdbf34398aab73ed4a44121","pubkey":"726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11","created_at":1687996513,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"],["e","9cdbced750e6b1e1274b7df47cb433f565414dd08c897167eba761eadee841cc","","reply"]],"content":"Testing nested comments","sig":"335f9969d23e28fef45adfda63da236f1d4d72dfb5873fc78961d47ee948e9333632ce8a45df3c09fc431cf8d29d2fb99bea9c9a4b3502465b05a0328d563231"},
+ {"id":"7a18dda355525d468b31bba4fa947cba98cc19048d4a3099d5e9ba045d878c26","pubkey":"45c41f21e1cf715fa6d9ca20b8e002a574db7bb49e96ee89834c66dac5446b7a","created_at":1680618145,"kind":1,"tags":[["e","1a1fe6c838400f3347f97a9adce08c10068f3bc35a279520c911b025c387b061",""],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93",""],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599",""],["title","Purple Text, Orange Highlights"],["summary","Using nostr as a base layer to create, consume, and monetize long-form content."],["image","https://dergigi.com/assets/images/reader.jpg"],["published_at","1680613802"]],"content":"good read. the highlighted part to be a shareable event 1 idea seems interesting, might explore it on flycat","sig":"ee913e997b907a03300616c36cffa12d2362932bd5e249cde39a44ec1250c7622703dddac167b093c3e3a28d41ebfeb9aed9d663d82bea9f8deaed4e342248dc"},
+ {"id":"a3b3825af621727f9af3bd77392fe38c04d71658024916af7fe4c5867ef73eaa","pubkey":"619af6a60b3fe4c733aaca061c522cc9c7cf1d87ef4c908facc5ed936d3bdf23","created_at":1681941926,"kind":1,"tags":[["p","330fb1431ff9d8c250706bbcdc016d5495a3f744e047a408173e92ae7ee42dac"],["e","d7ef8220e3521779e2e0c3ff0afb9738f241e44ace8ed1d5237cab36330a69d0"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"Good or bad time?","sig":"5629e21769b018c7f050ad35e20c3926e5f9c415ebcd54656e874cff7948a9117de6fb32d18f993b7c96f480f3e4baffb079a3db7c126c74b0188006bba41768"},
+ {"id":"c5ad64b1b72776a068c39f4549d089032432814a146849eba0650d1b329fb285","pubkey":"126df36019a793922eae86bfcfedc5240b68d67953892237e3eb500ab92140bf","created_at":1681911407,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","f2610be11393f884c020119316d3304bc06c48ab01f9cbcf5a5a8290031bf20e"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"Cool","sig":"62240add277f55e896ae24276fdae60f1580be9de4889e546fd2141b73e07b3e48f225a57914a5e0e5a2fd87570a1dfe1af4f869b355a7c455aa77f395c10bdb"},
+ {"id":"6a58f8315af5badb1bdaeb5489417b94621a4d8e192ae2fedcca0c5dcf0c9cd4","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1681821926,"kind":1,"tags":[["p","73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc"],["e","81a6379301d7b517f96cc7d070b5b30fb43db97c5c814be3bc9f58d8fcbaf190"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"test 5","sig":"eeba84f251d8d79e0f16213b766aee8079a3a5018407c3f7d1e8161c762e6a735bb695ac54898e04d7abe245ec12f24d680927351d37a6f31557344adeebd3c2"},
+ {"id":"6e9bb03c7c40d67fec0d0bb872548ec207ba0ac4533efa137d7bcaca9fb4b191","pubkey":"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","created_at":1681755803,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","9defafc0783f16b2ef9ac02d2e808a103d5d38f12905b7840420634af67d0821"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"test1","sig":"17c2489ee28dc32df68ebf87ced6f5f9af0e3f091b31298fece408acba43236bc6c9f50d756f61bcf46a204e043141535e0cd281d541146cbfac259814cd2cbe"},
+ {"id":"e2ae784b239cac4bad38136e4bd758b87dd261b659ef460450064bf9073edcb3","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1681648013,"kind":1,"tags":[["p","73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc"],["e","81a6379301d7b517f96cc7d070b5b30fb43db97c5c814be3bc9f58d8fcbaf190"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"powerful quote 👀","sig":"eda9e5565000892dacab78469936d96aad7fb0dc0828630563b8ccb731f08e704eacfefff246b6bb668332e046f5650f2a9e4186c108f07cba07eefc775c6c58"},
+ {"id":"45d4fc726f2cc5b524be862c14fdadc1a24b25b8c6c011eedf2d2909589263e7","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1681821952,"kind":1,"tags":[["p","73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc"],["e","e2ae784b239cac4bad38136e4bd758b87dd261b659ef460450064bf9073edcb3"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"test 7","sig":"e2c35766479d549700f4dbaeae385cfdacee1275ddf24777643ca7d7335c6d1baf7f89ac25d1dc90688bc49f8d2f5e4857d2234035291628a0c0ac760485bab0"},
+ {"id":"7a4a2419824669f07081abe2132f8cc0027efbce066ccdf187c897bb7ffa5dc3","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1681821936,"kind":1,"tags":[["p","73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc"],["e","e2ae784b239cac4bad38136e4bd758b87dd261b659ef460450064bf9073edcb3"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"test 5","sig":"875339b3e666fb1013d6563552997dcdd02e87b1f290ad25b0d7dd085d2f87dd10a4e6c20dfe3fc109d1809733b93675505341fe149e4399c42371c743fafc73"},
+ {"id":"674c62f84afdc045bc3623ea132d90afdfe4b64249807f65302231115af5406d","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1681648340,"kind":1,"tags":[["p","73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc"],["e","e2ae784b239cac4bad38136e4bd758b87dd261b659ef460450064bf9073edcb3"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"nested reply","sig":"f0f4fdd2d34e59255137c457b9d42fa9b5357e824f3154340de964616f6daf21e3b03491363b0060f97c5061445d7b2854eed328c5cced751b2b480e5ce8ce9a"},
+ {"id":"d54761f672669ea4f4b7592f3b0a30ee28de340b0a7e46b91af94e66905171c9","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1681648345,"kind":1,"tags":[["p","73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc"],["e","674c62f84afdc045bc3623ea132d90afdfe4b64249807f65302231115af5406d"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"another one","sig":"dd732eb380542dd8068449d5617bb8b6c1dfe1495c9facbd46aad3ca9b084122479df30861b8e85e13e7b0c9bf9bce589b7fc062daa086c1ff8a0b1d1ffc7aa7"},
+ {"id":"00813a18ac9084cd0948c27027a980e34039a3011f30279a8b52ad87da5a3031","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1681648351,"kind":1,"tags":[["p","73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc"],["e","d54761f672669ea4f4b7592f3b0a30ee28de340b0a7e46b91af94e66905171c9"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"yet another one","sig":"6332c8a441439487898fd5b423e42dde7d1d7aadfcf9b36defd78d76d1685ba099ac86ae00a1dac6ed5e8a166422d59bbb93bfccb2596dc1c5461efcfcf345b4"},
+ {"id":"87a5bd25aa084cefb3357fc9c2a5b327254fab35fdd7b2d4bd0acddc63d0abe8","pubkey":"726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11","created_at":1687990701,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"],["e","00813a18ac9084cd0948c27027a980e34039a3011f30279a8b52ad87da5a3031","","reply"]],"content":"and another one more","sig":"6a385fb259ff40d9b6bd8bb1d5a2e20057ec1c5b5ed2c307fbc40e4a96d4c191b80b6ea3f3f5748c907d4b0e6e64a92210a0be2889af856ad084ec6806f3cbd8"},
+ {"id":"512962dbada5fd5015fc727a107d5c3f569662de67eab8e5da5a8065012cf11e","pubkey":"7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194","created_at":1688194800,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"],["e","87a5bd25aa084cefb3357fc9c2a5b327254fab35fdd7b2d4bd0acddc63d0abe8","","reply"]],"content":"another one","sig":"b4bc4d8206a08de0a918043129b27c8863d17f60e4f58b4d2b535326625870d32640b04f247eb9eb5df1d62fe98c8e1b0341bdd119655553488300ec9d5b4036"},
+ {"id":"ce6e32e3e17b6901d2cc70b60f3743e24f885bb6e9da6d88cff516079eac1883","pubkey":"726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11","created_at":1688234921,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"],["e","512962dbada5fd5015fc727a107d5c3f569662de67eab8e5da5a8065012cf11e","","reply"]],"content":"yet another one, testing 0.0.3","sig":"13bd41c4029c6a7ee41cb03d12e831e9e9b6e14d43a61c78a72657070b45385ba2ce98e8121049fbcbdb7b2dd777c36c7867ba70b5e6a4798a9b866910ae5b62"}
+ ]
+ """.trimIndent()
+
+ val header =
+ """
+ {
+ "content": "Not too long ago, I tried to paint a picture of what\na [vision for a value-enabled web][vew]\ncould look like. Now, only a couple of months later,\nall this stuff is being built. On nostr, and on lightning. Orange and\npurple, a match made in heaven.\n\nIt goes without saying that I'm beyond delighted. What a time to be alive!\n\n## nostr\n\nHere's the thing that nostr got right, and it's the same thing that\nBitcoin got right: information is easy to spread and hard to stifle.[^fn-stifle]\nInformation can be copied quickly and perfectly, which is, I believe,\nthe underlying reason for its desire to be free.\n\n[^fn-stifle]: That's a [Satoshi quote][stifle], of course: \"Bitcoin's solution is to use a peer-to-peer network to check for double-spending. In a nutshell, the network works like a distributed timestamp server, stamping the first transaction to spend a coin. It takes advantage of the nature of information being easy to spread but hard to stifle.\"\n\n[stifle]: https://satoshi.nakamotoinstitute.org/posts/p2pfoundation/1/\n\nEasy to spread, hard to stifle. That's the base reality of the nature\nof information. As always, the smart thing is to work with nature, not\nagainst it.[^1] That's what's beautiful about the orange coin and\nthe purple ostrich: both manage to work with the peculiarities of\ninformation, not against them. Both realize that information can and should be\ncopied, as it can be perfectly read and easily spread, always. Both understand\nthat resistance to censorship comes from writing to many places, making the cost\nof deletion prohibitive.\n\n> Information does not just want to be free,\n> it longs to be free. Information expands to fill the available\n> storage space. Information is Rumor's younger, stronger cousin;\n> Information is fleeter of foot, has more eyes, knows more, and\n> understands less than Rumor.\n>\n> Eric Hughes, [A Cypherpunk's Manifesto][manifesto]\n\n[manifesto]: https://nakamotoinstitute.org/static/docs/cypherpunk-manifesto.txt\n\nNostr is quickly establishing itself as a base layer for information exchange,\none that is identity-native and value-enabled. It is distinctly different from\nsystems that came before it, just like Bitcoin is distinctly different from\nmonies that came before it.\n\nAs of today, the focus of nostr is mostly on short text notes, the so-called\n\"type 1\" events more commonly known as *tweets*.[^fn-kinds] However, as you should be aware\nby now, nostr is way more than just an alternative to twitter. It is a new\nparadigm. Change the note kind from `1` to `30023` and you don't have an\nalternative to Twitter, but a replacement for Medium, Substack, and all the\nother long-form platforms. I believe that special-purpose clients that focus on\ncertain content types will emerge over time, just like we have seen the\nemergence of special-purpose platforms in the Web 2.0 era. This time, however,\nthe network effects are cumulative, not separate. A new paradigm.\n\nLet me now turn to one such special-purpose client, a nostr-based reading app.\n\n[^fn-kinds]: Refer to the various NIPs to discover the multitude of [event kinds][kinds] defined by the protocol.\n\n[kinds]: https://github.com/nostr-protocol/nips#event-kinds\n[nip23]: https://github.com/nostr-protocol/nips/blob/master/23.md\n\n## Reading\n\nI'm constantly surprised that, even though most people do read a lot\nonline, very few people seem to have a reading workflow or reading\ntools.\n\nWhy that is is anyone's guess, but maybe the added value of such tools\nis not readily apparent. You can just read the stuff right there, on the\nad-ridden, dead-ugly site, right? Why should you sign up for another\nsite, use another app, or bind yourself to another closed platform?\n\nThat's a fair point, but the success of Medium and Substack shows that\nthere is an appetite for clean reading and writing, as well as providing\navenues for authors to get paid for their writing (and a willingness of\nreaders to support said authors, just because).\n\nThe problem is, of course, that all of these platforms are *platforms*,\nwhich is to say, walled gardens that imprison readers and writers alike.\nWorse than that: they are fiat platforms, which means that\npermissionless value-flows are not only absent from their DNA, they are\noutright impossible.[^2]\n\nNostr fixes this.\n\n\n\nThe beauty of nostr is that it is not a platform. It's a protocol,\nwhich means that you don't have to sign up for it---you can create an\nidentity yourself. You don't have to ask for permission; you just *do*,\nwithout having to rely on the benevolence of whatever dictator is in\ncharge of the platform right now.\n\nNostr is *not* a platform, and yet, powerful tools and services can be\nbuilt and monetized on top of it. This is good for users, good for\nservice providers, and good for the network(s) at large. Win-win-win.\n\nSo what am I talking about, exactly? How can nostr improve everyone's\nreading (and writing) experience?\n\nAllow me to paint a (rough) picture of what I have in mind. Nostr\nalready supports private and public bookmarks, so let's start from\nthere.\n\nImagine a special-purpose client that scans all your bookmarks for long-form\ncontent.[^fn-urls] Everything that you marked to be read later is shown in an orderly\nfashion, which is to say searchable, sortable, filterable, and displayed without\ndistractions. Voilà, you have yourself a reading app. That's, in essence, how\nPocket, Readwise, and other reading apps work. But all these apps are walled\ngardens without much interoperability and without direct monetization.\n\n[^fn-urls]: In the nostr world long-form content is simply markdown as defined in [NIP-23][nip23], but it could also be a link to an article or PDF, which in turn could get [converted into markdown][readability] and posted as an event to a special relay.\n\n[readability]: https://github.com/mozilla/readability\n\nBitcoin fixes the direct monetization part.[^fn-v4v] Nostr fixes the interoperability part.\n\n[^fn-v4v]: ...because Bitcoin makes [V4V][busking] practical. (Paywalls are not the way.)\n\nAlright, we got ourselves a boring reading app. Great. Now, imagine that\nusers are able to highlight passages. These highlights, just like\nbookmarks now, could be private or public. When shared publicly,\nsomething interesting emerges: an overlay on existing content, a lens on\nthe written Web. In other words: *swarm highlights*.\n\nImagine a visual overlay of all public highlights, automatically shining\na light on what the swarm of readers found most useful, insightful,\nfunny, etc.\n\n\n\nFurther, imagine the possibility of sharing these highlights as a \"type 1\" event\nwith one click, automatically tagging the highlighter(s)---as well as the\nauthor, of course---so that eventual sat-flows can be split and forwarded\nautomatically.\n\n\n\nVoilà, you have a system that allows for value to flow back to those who\nprovide it, be it authors, editors, curators, or readers that willingly\nslog through the information jungle to share and highlight the best\nstuff (which is a form of curation, of course).\n\nZaps make nostr a defacto address book[^fn-pp] of payment information, which is\nto say lightning addresses, as of now. Thanks to [nostr wallet connect][nwc] (among\nother developments), sending sats ~~will soon be~~ is already as\nfrictionless as leaving a like.\n\n[^fn-pp]: The Yellow Pages are dead, long live [The Purple Pages](http://purplepag.es/)!\n\nValue-for-value and participatory payment flows are something that\ntraditional reading apps desperately lack, be it Pocket, Instapaper,\nReadwise, or the simple reading mode that is part of every browser.\n\nA neat side-effect of a more structured way to share passages of text is\nthat it enables semi-structured discussions around said\npassages---which could be another useful overlay inside\nspecial-purpose clients, providing context and further insights.[^5]\n\nFurther, imagine the option of seamlessly switching from text-on-screen\nto text-to-speech, allowing the user to stream sats if desired, as\nPodcasting 2.0 clients already do.[^3]\n\nImagine user-built curations of the best articles of the week, bundled\nneatly for your reading pleasure, incentivized by a small value split\nthat allows the curator to participate in the flow of sats.\n\nYou get the idea.\n\nI'm sure that the various implementation details will be hashed out,\nbut as I see it, 90% of the stuff is already there. Maybe we'll need\nanother NIP or two, but I don't see a reason why this can't be\nbuilt---and, more importantly: I don't see a reason why it wouldn't\nbe sustainable for everyone involved.\n\nMost puzzle pieces are already there, and the rest of them can probably\nbe implemented by custom event types. From the point of view of nostr,\nmost everything is an event: bookmarks are events, highlights are\nevents, marking something as read is an event, and sharing an excerpt or\na highlight is an event. Public actions are out in the open, private\nactions are encrypted, the data is not in a silo, and everyone wins.\nEspecially the users, those who are at the edge of the network and\nusually lose out on the value generated.\n\nIn this case, the reading case, the users are mostly \"consumers\" of\ncontent. What changes from the producing perspective, the perspective of\nthe writer?\n\n## Writing\n\nBack to the one thing that nostr got right: information is easy to\nspread but hard to stifle. In addition to that, digital information can\nbe copied perfectly, which is why it shouldn't matter where stuff is\npublished in the first place.\n\nAllow me to repeat this point in all caps, for emphasis: **IT SHOULD NOT\nMATTER WHERE INFORMATION IS PUBLISHED**, and, maybe even more\nimportantly, it shouldn't matter if it is published in a hundred\ndifferent places at once.[^fn-torrents]\n\nWhat matters is trust and accuracy, which is to say, digital signatures\nand reputation. To translate this to nostr speak: because every event is\nsigned by default, as long as you trust the person behind the signature,\nit doesn't matter from which relay the information is fetched.\n\nThis is already true (or mostly true) on the regular web. Whether you\nread the internet archive version of an article or the version that is\npublished by an online magazine, the version on the author's website,\nor the version read by some guy that has read more about Bitcoin than\nanyone else you know[^fn-guy]---it's all the same, essentially. What matters\nis the information itself.\n\n[^fn-guy]: There is only one such guy, as we all know, and it's this Guy: nostr:npub1h8nk2346qezka5cpm8jjh3yl5j88pf4ly2ptu7s6uu55wcfqy0wq36rpev\n\nPractically speaking, the source of truth in a hypernostrized world is---you\nguessed it---an event. An event signed by the author, which allows for\nthe information to be wrapped in a tamper-proof manner, which in turn\nallows the information to spread far and wide---without it being\nhosted in one place.\n\nThe first clients that focus on long-form content already exist, and I expect\nmore clients to pop up over time.[^4] As mentioned before, one could easily\nimagine [prism-like value splits][prism] seamlessly integrated into these\nclients, splitting zaps automatically to compensate writers, editors,\nproofreaders, and illustrators in a V4V fashion. Further, one could imagine\nvarious compute-intensive services built into these special-purpose clients,\nsuch as GPT Ghostwriters, or writing aids such as Grammarly and the like. All\nthese services could be seamlessly paid for in sats, without the requirement of\nany sign-ups or the gathering of any user data. That's the beauty of [money\nproper][rediscovery].\n\n\n\nPlagiarism is one issue that needs to be dealt with, of course. Humans\nare greedy, and some humans are assholes. Neither bitcoin nor nostr\nfixes this. However, while plagiarism detection is not necessarily\ntrivial, it is also not impossible, especially if most texts are\npublished on nostr first. Nostr-based publishing tools allow for\nOpenTimestamp attestations thanks\nto [NIP-03](https://github.com/nostr-protocol/nips/blob/master/03.md),\nwhich in turn allows for plagiarism detection based on \"first seen\"\nlookups.\n\nThat's just one way to deal with the problem, of course. In any case,\nI'm confident that we'll figure it out.\n\n## Value\n\nI believe that in the open ~~attention~~ information economy we find\nourselves in, value will mostly derive from effective curation,\ndissemination, and transmission of information, *not* the exclusive\nownership of it.\n\nAlthough it is still early days,\nthe [statistics](https://stats.podcastindex.org/v4v) around Podcasting\n2.0 and [nostr zaps](https://zaplife.lol/) clearly show that (a) people\nare willing to monetarily reward content they care about, and (b) the\nwillingness to send sats *increases* as friction *decreases*.\n\nThe ingenious thing about boostagrams and zaps is that they are direct\nand visible, which is to say, public and interactive. They are neither\nregular transactions nor simple donations---they are something else\nentirely. An unforgable value signal, a special form of gratitude and\nappreciation.\n\nContrast that with a link to Paypal or Patreon: impersonal, slow,\nindirect, and friction-laden. It's the opposite of a super-charged\ninteraction.\n\nWhile today's information jungle increasingly presents itself in the\nform of (short) videos and (long-form) audio, I believe that we will see\na renaissance of the written word, especially if we manage to move away\nfrom an economy built around attention, towards an economy built upon\nvalue and insight.\n\nThe orange future now has a purple hue, and I believe that it will be as\nbright as ever. We just have a lot of building to do.\n\n---\n\n## Further Reading\n\n- [A Vision for a Value-Enabled Web][vew]\n- [The Freedom of Value][busking]\n- [The Rediscovery of Money][prism]\n- [Lightning Prisms][rediscovery]\n\n[vew]: https://dergigi.com/vew\n[prism]: https://dergigi.com/prism\n[rediscovery]: https://dergigi.com/rediscovery\n[busking]: https://dergigi.com/busking\n\n## NIPs and Resources\n\n- [Nostr Resources][nr]\n- [value4value.info](https://value4value.info/)\n- [nips.be](https://nips.be/)\n- [NIP-23: Long-form content](https://github.com/nostr-protocol/nips/blob/master/23.md)\n- [NIP-57: Event-specific zap markers](https://github.com/nostr-protocol/nips/blob/master/57.md)\n- [NIP-47: Nostr Wallet Connect](https://github.com/getAlby/nips/blob/master/47.md)\n- [NIP-03: OpenTimestamps attestations for events](https://github.com/nostr-protocol/nips/blob/master/03.md)\n\nOriginally published on [dergigi.com](https://dergigi.com/reader)\n\n---\n\n[^1]: Paywalls work against this nature, which is why I consider them misguided at best and incredibly retarded at worst.\n\n[^2]: Fiat doesn't work for the [value-enabled web][vew], as fiat rails can never be open and permissionless. Digital fiat is never money. It is---and always will be---[credit][rediscovery].\n\n[^3]: Whether the recipient is a text-to-speech service provider or a human narrator doesn't even matter too much, sats will flow just the same.\n\n[^4]: [BlogStack](https://blogstack.io/) and [Habla](https://habla.news/) being two of them.\n\n[^5]: Use a URI as the discussion base (instead of a highlight), and you got yourself a [Disqus](https://disqus.com/) in purple feathers!\n\n[^fn-torrents]: That's what torrents got right, and [ipfs] for that matter.\n\n[nr]: https://nostr-resources.com\n[nwc]: https://nwc.getalby.com/\n[ipfs]: https://fiatjaf.com/d5031e5b.html\n",
+ "created_at": 1680614039,
+ "id": "9517aa60334990596fdea4493c1bde429c55e9cefb84d8d88a6758c4e3ebcabc",
+ "kind": 30023,
+ "pubkey": "6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93",
+ "sig": "c8c8c7ac8c288f6a6e1090e0e5f7eed85daaaba03eeab0a4936535f95ce066c08723bedcfb80a6af4941a9fafb9fe351950b9376d13ff0e81030cd4391840f04",
+ "tags": [
+ ["d","1680612926599"],
+ ["title","Purple Text, Orange Highlights"],
+ ["summary","Using nostr as a base layer to create, consume, and monetize long-form content."],
+ ["published_at","1680613802"],
+ ["t","nostr"],
+ ["t","reading"],
+ ["t","writing"],
+ ["t","readability"],
+ ["t","highlights"],
+ ["t","pocket"],
+ ["t","instapaper"],
+ ["t","readwise"],
+ ["t","disqus"],
+ ["t","v4v"],
+ ["t","value4value"],
+ ["image","https://dergigi.com/assets/images/reader.jpg"],
+ ["p","b9e76546ba06456ed301d9e52bc49fa48e70a6bf2282be7a1ae72947612023dc"]
+ ]
+ }
+ """.trimIndent()
+ }
@Test
fun threadOrderTest() =
runBlocking {
val eventArray =
- EventMapper.mapper.readValue>(db) as List + Event.fromJson(header)
+ JsonMapper.mapper.readValue>(db) as List + Event.fromJson(header)
var counter = 0
eventArray.forEach {
@@ -138,11 +166,6 @@ class ThreadDualAxisChartAssemblerTest {
null,
)
- val account = Account(AccountSettings(KeyPair()), scope = CoroutineScope(Dispatchers.IO + SupervisorJob()))
- withContext(Dispatchers.Main) {
- val user = account.userProfile().flow()
- }
-
val filter = ThreadFeedFilter(account, naddr.toTag())
val calculatedFeed = filter.feed()
diff --git a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt
index 2bb1fdda0f..8f832704ec 100644
--- a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt
+++ b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt
@@ -89,14 +89,14 @@ object PushDistributorHandler : PushDistributorActions {
override fun saveDistributor(distributor: String) {
unifiedPush.saveDistributor(appContext(), distributor)
- unifiedPush.registerApp(appContext())
+ unifiedPush.register(appContext())
}
override fun removeSavedDistributor() {
- unifiedPush.safeRemoveDistributor(appContext())
+ unifiedPush.removeDistributor(appContext())
}
fun forceRemoveDistributor(context: Context) {
- unifiedPush.forceRemoveDistributor(context)
+ UnifiedPush.removeDistributor(context)
}
}
diff --git a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt
index 33b3ffdc76..eed9a8c81b 100644
--- a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt
+++ b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt
@@ -34,7 +34,10 @@ import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import org.unifiedpush.android.connector.FailedReason
import org.unifiedpush.android.connector.MessagingReceiver
+import org.unifiedpush.android.connector.data.PushEndpoint
+import org.unifiedpush.android.connector.data.PushMessage
class PushMessageReceiver : MessagingReceiver() {
companion object {
@@ -48,10 +51,10 @@ class PushMessageReceiver : MessagingReceiver() {
override fun onMessage(
context: Context,
- message: ByteArray,
+ message: PushMessage,
instance: String,
) {
- val messageStr = message.decodeToString()
+ val messageStr = message.content.decodeToString()
Log.d(TAG, "New message $messageStr for Instance: $instance")
scope.launch {
try {
@@ -81,10 +84,10 @@ class PushMessageReceiver : MessagingReceiver() {
override fun onNewEndpoint(
context: Context,
- endpoint: String,
+ endpoint: PushEndpoint,
instance: String,
) {
- val sanitizedEndpoint = if (endpoint.endsWith("?up=1")) endpoint.dropLast(5) else endpoint
+ val sanitizedEndpoint = if (endpoint.url.endsWith("?up=1")) endpoint.url.dropLast(5) else endpoint.url
if (sanitizedEndpoint != pushHandler.getSavedEndpoint()) {
Log.d(TAG, "New endpoint provided:- $endpoint for Instance: $instance ${pushHandler.getSavedEndpoint()} $sanitizedEndpoint")
pushHandler.setEndpoint(sanitizedEndpoint)
@@ -102,6 +105,7 @@ class PushMessageReceiver : MessagingReceiver() {
override fun onRegistrationFailed(
context: Context,
+ reason: FailedReason,
instance: String,
) {
Log.d(TAG, "Registration failed for Instance: $instance")
diff --git a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt
index 0d080d7b03..b6e13bc24f 100644
--- a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt
+++ b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt
@@ -24,7 +24,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
-import com.vitorpamplona.amethyst.ui.navigation.INav
+import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists
diff --git a/amethyst/src/main/AndroidManifest.xml b/amethyst/src/main/AndroidManifest.xml
index f65b84ef8e..1adc0f6598 100644
--- a/amethyst/src/main/AndroidManifest.xml
+++ b/amethyst/src/main/AndroidManifest.xml
@@ -23,6 +23,9 @@
+
+
+
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt
index 91b9e0ad57..d115ad178d 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt
@@ -37,17 +37,19 @@ import com.vitorpamplona.amethyst.service.notifications.PokeyReceiver
import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager
import com.vitorpamplona.amethyst.service.okhttp.EncryptionKeyCache
import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket
+import com.vitorpamplona.amethyst.service.okhttp.ProxySettingsAnchor
import com.vitorpamplona.amethyst.service.ots.OtsBlockHeightCache
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCacheFactory
import com.vitorpamplona.amethyst.service.relayClient.CacheClientConnector
-import com.vitorpamplona.amethyst.service.relayClient.RelaySpeedLogger
+import com.vitorpamplona.amethyst.service.relayClient.RelayProxyClientConnector
import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.AuthCoordinator
import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model.NotifyCoordinator
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator
+import com.vitorpamplona.amethyst.service.relayClient.speedLogger.RelaySpeedLogger
import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory
import com.vitorpamplona.amethyst.ui.tor.TorManager
-import com.vitorpamplona.ammolite.relays.NostrClient
+import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
@@ -89,38 +91,40 @@ class Amethyst : Application() {
scope = applicationIOScope,
)
- // Connects the NostrClient class with okHttp
- val factory =
- OkHttpWebSocket.BuilderFactory { _, useProxy ->
- okHttpClients.getHttpClient(useProxy)
- }
+ val torProxySettingsAnchor = ProxySettingsAnchor()
- // Provides a relay pool
- val client: NostrClient = NostrClient(factory)
+ // Connects the NostrClient class with okHttp
+ val websocketBuilder =
+ OkHttpWebSocket.Builder { url ->
+ okHttpClients.getHttpClient(torProxySettingsAnchor.useProxy(url))
+ }
// Caches all events in Memory
val cache: LocalCache = LocalCache
+ // Organizes cache clearing
+ val trimmingService = MemoryTrimmingService(cache)
+
+ // Provides a relay pool
+ val client: NostrClient = NostrClient(websocketBuilder, applicationIOScope)
+
+ // Watches for changes on Tor and Relay List Settings
+ val relayProxyClientConnector = RelayProxyClientConnector(torProxySettingsAnchor, okHttpClients, connManager, client, applicationIOScope)
+
// Verifies and inserts in the cache from all relays, all subscriptions
- val cacheClientConnector = CacheClientConnector(client, cache)
+ val cacheClientConnector = CacheClientConnector(client, cache, applicationIOScope)
// Show messages from the Relay and controls their dismissal
val notifyCoordinator = NotifyCoordinator(client)
// Authenticates with relays.
- val authCoordinator = AuthCoordinator(client)
+ val authCoordinator = AuthCoordinator(client, applicationIOScope)
val logger = if (isDebug) RelaySpeedLogger(client) else null
- // Organizes cache clearing
- val trimmingService = MemoryTrimmingService(cache)
-
// Coordinates all subscriptions for the Nostr Client
val sources: RelaySubscriptionsCoordinator = RelaySubscriptionsCoordinator(LocalCache, client, applicationIOScope)
- // Trash.
- val serviceManager = ServiceManager(client, applicationIOScope)
-
// saves the .content of NIP-95 blobs in disk to save memory
val nip95cache: File by lazy { Nip95CacheFactory.new(this) }
@@ -150,7 +154,10 @@ class Amethyst : Application() {
}
// initializes diskcache on an IO thread.
- applicationIOScope.launch { videoCache }
+ applicationIOScope.launch {
+ diskCache
+ videoCache
+ }
// registers to receive events
pokeyReceiver.register(this)
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt
index 766ae8b776..7d3d0719b3 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt
@@ -30,12 +30,13 @@ import com.vitorpamplona.amethyst.model.LocalCache
import kotlin.time.DurationUnit
import kotlin.time.measureTimedValue
+@Suppress("SENSELESS_COMPARISON")
val isDebug = BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "benchmark"
fun debugState(context: Context) {
- Amethyst.instance.client
- .allSubscriptions()
- .forEach { Log.d("STATE DUMP", "${it.key} ${it.value.joinToString { it.filter.toDebugJson() }}") }
+ // Amethyst.instance.client
+ // .allSubscriptions()
+ // .forEach { Log.d("STATE DUMP", "${it.key} ${it.value.filters.joinToString { it.filter.toJson() }}") }
val totalMemoryMb = Runtime.getRuntime().totalMemory() / (1024 * 1024)
val freeMemoryMb = Runtime.getRuntime().freeMemory() / (1024 * 1024)
@@ -58,7 +59,16 @@ fun debugState(context: Context) {
Log.d("STATE DUMP", "Memory Class $memClass MB (largeHeap $isLargeHeap)")
}
- Log.d("STATE DUMP", "Connected Relays: " + Amethyst.instance.client.connectedRelays())
+ Log.d(
+ "STATE DUMP",
+ "Connected Relays: " +
+ Amethyst.instance.client
+ .relayStatusFlow()
+ .value.connected.size + "/" +
+ Amethyst.instance.client
+ .relayStatusFlow()
+ .value.available.size,
+ )
Log.d(
"STATE DUMP",
@@ -112,7 +122,7 @@ fun debugState(context: Context) {
Log.d(
"STATE DUMP",
"Spam: " +
- LocalCache.antiSpam.spamMessages.size() + " / " + LocalCache.antiSpam.recentMessages.size(),
+ LocalCache.antiSpam.spamMessages.size() + " / " + LocalCache.antiSpam.recentEventIds.size() + " / " + LocalCache.antiSpam.recentAddressables.size(),
)
Log.d(
@@ -132,21 +142,24 @@ fun debugState(context: Context) {
LocalCache.addressables
.sumByGroup(groupMap = { _, it -> it.event?.kind }, sumOf = { _, it -> it.event?.countMemory() ?: 0L })
- qttNotes.toList().sortedByDescending { bytesNotes.get(it.first) }.forEach { (kind, qtt) ->
- Log.d("STATE DUMP", "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesNotes.get(kind)?.div((1024 * 1024))}MB ")
+ qttNotes.toList().sortedByDescending { bytesNotes[it.first] }.forEach { (kind, qtt) ->
+ Log.d("STATE DUMP", "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesNotes[kind]?.div((1024 * 1024))}MB ")
}
- qttAddressables.toList().sortedByDescending { bytesNotes.get(it.first) }.forEach { (kind, qtt) ->
- Log.d("STATE DUMP", "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesAddressables.get(kind)?.div((1024 * 1024))}MB ")
+ qttAddressables.toList().sortedByDescending { bytesNotes[it.first] }.forEach { (kind, qtt) ->
+ Log.d("STATE DUMP", "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesAddressables[kind]?.div((1024 * 1024))}MB ")
}
}
inline fun logTime(
debugMessage: String,
+ minToReportMs: Int = 1,
block: () -> T,
): T =
if (isDebug) {
val (result, elapsed) = measureTimedValue(block)
- Log.d("DEBUG-TIME", "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: $debugMessage")
+ if (elapsed.inWholeMilliseconds > minToReportMs) {
+ Log.d("DEBUG-TIME", "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: $debugMessage")
+ }
result
} else {
block()
@@ -154,11 +167,14 @@ inline fun logTime(
inline fun logTime(
debugMessage: (T) -> String,
+ minToReportMs: Int = 1,
block: () -> T,
): T =
if (isDebug) {
val (result, elapsed) = measureTimedValue(block)
- Log.d("DEBUG-TIME", "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: ${debugMessage(result)}")
+ if (elapsed.inWholeMilliseconds > minToReportMs) {
+ Log.d("DEBUG-TIME", "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: ${debugMessage(result)}")
+ }
result
} else {
block()
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt
index 5349ffe8ff..62507b79fb 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt
@@ -27,33 +27,37 @@ import android.util.Log
import androidx.compose.runtime.Immutable
import androidx.core.content.edit
import com.fasterxml.jackson.module.kotlin.readValue
+import com.vitorpamplona.amethyst.model.ALL_FOLLOWS
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
-import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
import com.vitorpamplona.amethyst.model.Settings
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
import com.vitorpamplona.amethyst.ui.tor.TorSettings
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
-import com.vitorpamplona.ammolite.relays.RelaySetupInfo
-import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
-import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
+import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip19Bech32.toNpub
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
+import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
-import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
+import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent
+import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
+import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
+import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent
+import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
+import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
@@ -82,7 +86,6 @@ private object PrefKeys {
const val SAVED_ACCOUNTS = "all_saved_accounts"
const val NOSTR_PRIVKEY = "nostr_privkey"
const val NOSTR_PUBKEY = "nostr_pubkey"
- const val RELAYS = "relays"
const val LOCAL_RELAY_SERVERS = "localRelayServers"
const val DEFAULT_FILE_SERVER = "defaultFileServer"
const val DEFAULT_HOME_FOLLOW_LIST = "defaultHomeFollowList"
@@ -95,10 +98,15 @@ private object PrefKeys {
const val LATEST_DM_RELAY_LIST = "latestDMRelayList"
const val LATEST_NIP65_RELAY_LIST = "latestNIP65RelayList"
const val LATEST_SEARCH_RELAY_LIST = "latestSearchRelayList"
+ const val LATEST_BLOCKED_RELAY_LIST = "latestBlockedRelayList"
+ const val LATEST_TRUSTED_RELAY_LIST = "latestTrustedRelayList"
const val LATEST_MUTE_LIST = "latestMuteList"
const val LATEST_PRIVATE_HOME_RELAY_LIST = "latestPrivateHomeRelayList"
const val LATEST_APP_SPECIFIC_DATA = "latestAppSpecificData"
const val LATEST_CHANNEL_LIST = "latestChannelList"
+ const val LATEST_COMMUNITY_LIST = "latestCommunityList"
+ const val LATEST_HASHTAG_LIST = "latestHashtagList"
+ const val LATEST_GEOHASH_LIST = "latestGeohashList"
const val LATEST_EPHEMERAL_LIST = "latestEphemeralChatList"
const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog"
const val HIDE_BLOCK_ALERT_DIALOG = "hide_block_alert_dialog"
@@ -120,8 +128,8 @@ object LocalPreferences {
private const val COMMA = ","
private var currentAccount: String? = null
- private var savedAccounts: MutableStateFlow?> = MutableStateFlow(null)
- private var cachedAccounts: MutableMap = mutableMapOf()
+ private val savedAccounts: MutableStateFlow?> = MutableStateFlow(null)
+ private val cachedAccounts: MutableMap = mutableMapOf()
suspend fun currentAccount(): String? {
if (currentAccount == null) {
@@ -155,7 +163,7 @@ object LocalPreferences {
with(encryptedPreferences()) {
val newSystemOfAccounts =
getString(PrefKeys.ALL_ACCOUNT_INFO, "[]")?.let {
- EventMapper.mapper.readValue>(it)
+ JsonMapper.mapper.readValue>(it)
}
if (!newSystemOfAccounts.isNullOrEmpty()) {
@@ -175,7 +183,7 @@ object LocalPreferences {
savedAccounts.emit(migrated)
- edit { putString(PrefKeys.ALL_ACCOUNT_INFO, EventMapper.mapper.writeValueAsString(savedAccounts.value)) }
+ edit { putString(PrefKeys.ALL_ACCOUNT_INFO, JsonMapper.mapper.writeValueAsString(savedAccounts.value)) }
}
}
}
@@ -195,7 +203,7 @@ object LocalPreferences {
.edit {
putString(
PrefKeys.ALL_ACCOUNT_INFO,
- EventMapper.mapper.writeValueAsString(accounts.filter { !it.isTransient }),
+ JsonMapper.mapper.writeValueAsString(accounts.filter { !it.isTransient }),
)
}
}
@@ -249,7 +257,7 @@ object LocalPreferences {
if (npub == null) DEBUG_PREFERENCES_NAME else "${DEBUG_PREFERENCES_NAME}_$npub"
Amethyst.instance.getSharedPreferences(preferenceFile, Context.MODE_PRIVATE)
} else {
- return Amethyst.instance.encryptedStorage(npub)
+ Amethyst.instance.encryptedStorage(npub)
}
}
@@ -262,7 +270,7 @@ object LocalPreferences {
* deleted
*/
@SuppressLint("ApplySharedPref")
- suspend fun updatePrefsForLogout(accountInfo: AccountInfo) {
+ suspend fun deleteAccount(accountInfo: AccountInfo) {
Log.d("LocalPreferences", "Saving to encrypted storage updatePrefsForLogout ${accountInfo.npub}")
withContext(Dispatchers.IO) {
encryptedPreferences(accountInfo.npub).edit(commit = true) { clear() }
@@ -277,7 +285,7 @@ object LocalPreferences {
}
}
- suspend fun updatePrefsForLogin(accountSettings: AccountSettings) {
+ suspend fun setDefaultAccount(accountSettings: AccountSettings) {
setCurrentAccount(accountSettings)
saveToEncryptedStorage(accountSettings)
}
@@ -299,11 +307,10 @@ object LocalPreferences {
settings.keyPair.privKey?.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHexKey()) }
}
settings.keyPair.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHexKey()) }
- putString(PrefKeys.RELAYS, EventMapper.mapper.writeValueAsString(settings.localRelays))
putString(
PrefKeys.DEFAULT_FILE_SERVER,
- EventMapper.mapper.writeValueAsString(settings.defaultFileServer),
+ JsonMapper.mapper.writeValueAsString(settings.defaultFileServer),
)
putString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, settings.defaultHomeFollowList.value)
putString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, settings.defaultStoriesFollowList.value)
@@ -317,12 +324,12 @@ object LocalPreferences {
)
putString(
PrefKeys.ZAP_PAYMENT_REQUEST_SERVER,
- EventMapper.mapper.writeValueAsString(settings.zapPaymentRequest),
+ JsonMapper.mapper.writeValueAsString(settings.zapPaymentRequest.value?.denormalize()),
)
if (settings.backupContactList != null) {
putString(
PrefKeys.LATEST_CONTACT_LIST,
- EventMapper.mapper.writeValueAsString(settings.backupContactList),
+ JsonMapper.mapper.writeValueAsString(settings.backupContactList),
)
} else {
remove(PrefKeys.LATEST_CONTACT_LIST)
@@ -331,7 +338,7 @@ object LocalPreferences {
if (settings.backupUserMetadata != null) {
putString(
PrefKeys.LATEST_USER_METADATA,
- EventMapper.mapper.writeValueAsString(settings.backupUserMetadata),
+ JsonMapper.mapper.writeValueAsString(settings.backupUserMetadata),
)
} else {
remove(PrefKeys.LATEST_USER_METADATA)
@@ -340,7 +347,7 @@ object LocalPreferences {
if (settings.backupDMRelayList != null) {
putString(
PrefKeys.LATEST_DM_RELAY_LIST,
- EventMapper.mapper.writeValueAsString(settings.backupDMRelayList),
+ JsonMapper.mapper.writeValueAsString(settings.backupDMRelayList),
)
} else {
remove(PrefKeys.LATEST_DM_RELAY_LIST)
@@ -349,7 +356,7 @@ object LocalPreferences {
if (settings.backupNIP65RelayList != null) {
putString(
PrefKeys.LATEST_NIP65_RELAY_LIST,
- EventMapper.mapper.writeValueAsString(settings.backupNIP65RelayList),
+ JsonMapper.mapper.writeValueAsString(settings.backupNIP65RelayList),
)
} else {
remove(PrefKeys.LATEST_NIP65_RELAY_LIST)
@@ -358,14 +365,32 @@ object LocalPreferences {
if (settings.backupSearchRelayList != null) {
putString(
PrefKeys.LATEST_SEARCH_RELAY_LIST,
- EventMapper.mapper.writeValueAsString(settings.backupSearchRelayList),
+ JsonMapper.mapper.writeValueAsString(settings.backupSearchRelayList),
)
} else {
remove(PrefKeys.LATEST_SEARCH_RELAY_LIST)
}
- if (settings.localRelayServers.isNotEmpty()) {
- putStringSet(PrefKeys.LOCAL_RELAY_SERVERS, settings.localRelayServers)
+ if (settings.backupBlockedRelayList != null) {
+ putString(
+ PrefKeys.LATEST_BLOCKED_RELAY_LIST,
+ JsonMapper.mapper.writeValueAsString(settings.backupBlockedRelayList),
+ )
+ } else {
+ remove(PrefKeys.LATEST_BLOCKED_RELAY_LIST)
+ }
+
+ if (settings.backupTrustedRelayList != null) {
+ putString(
+ PrefKeys.LATEST_TRUSTED_RELAY_LIST,
+ JsonMapper.mapper.writeValueAsString(settings.backupTrustedRelayList),
+ )
+ } else {
+ remove(PrefKeys.LATEST_TRUSTED_RELAY_LIST)
+ }
+
+ if (settings.localRelayServers.value.isNotEmpty()) {
+ putStringSet(PrefKeys.LOCAL_RELAY_SERVERS, settings.localRelayServers.value)
} else {
remove(PrefKeys.LOCAL_RELAY_SERVERS)
}
@@ -373,7 +398,7 @@ object LocalPreferences {
if (settings.backupMuteList != null) {
putString(
PrefKeys.LATEST_MUTE_LIST,
- EventMapper.mapper.writeValueAsString(settings.backupMuteList),
+ JsonMapper.mapper.writeValueAsString(settings.backupMuteList),
)
} else {
remove(PrefKeys.LATEST_MUTE_LIST)
@@ -382,7 +407,7 @@ object LocalPreferences {
if (settings.backupPrivateHomeRelayList != null) {
putString(
PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST,
- EventMapper.mapper.writeValueAsString(settings.backupPrivateHomeRelayList),
+ JsonMapper.mapper.writeValueAsString(settings.backupPrivateHomeRelayList),
)
} else {
remove(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST)
@@ -391,7 +416,7 @@ object LocalPreferences {
if (settings.backupAppSpecificData != null) {
putString(
PrefKeys.LATEST_APP_SPECIFIC_DATA,
- EventMapper.mapper.writeValueAsString(settings.backupAppSpecificData),
+ JsonMapper.mapper.writeValueAsString(settings.backupAppSpecificData),
)
} else {
remove(PrefKeys.LATEST_APP_SPECIFIC_DATA)
@@ -400,16 +425,43 @@ object LocalPreferences {
if (settings.backupChannelList != null) {
putString(
PrefKeys.LATEST_CHANNEL_LIST,
- EventMapper.mapper.writeValueAsString(settings.backupChannelList),
+ JsonMapper.mapper.writeValueAsString(settings.backupChannelList),
)
} else {
remove(PrefKeys.LATEST_CHANNEL_LIST)
}
+ if (settings.backupCommunityList != null) {
+ putString(
+ PrefKeys.LATEST_COMMUNITY_LIST,
+ JsonMapper.mapper.writeValueAsString(settings.backupCommunityList),
+ )
+ } else {
+ remove(PrefKeys.LATEST_COMMUNITY_LIST)
+ }
+
+ if (settings.backupHashtagList != null) {
+ putString(
+ PrefKeys.LATEST_HASHTAG_LIST,
+ JsonMapper.mapper.writeValueAsString(settings.backupHashtagList),
+ )
+ } else {
+ remove(PrefKeys.LATEST_HASHTAG_LIST)
+ }
+
+ if (settings.backupGeohashList != null) {
+ putString(
+ PrefKeys.LATEST_HASHTAG_LIST,
+ JsonMapper.mapper.writeValueAsString(settings.backupGeohashList),
+ )
+ } else {
+ remove(PrefKeys.LATEST_HASHTAG_LIST)
+ }
+
if (settings.backupEphemeralChatList != null) {
putString(
PrefKeys.LATEST_EPHEMERAL_LIST,
- EventMapper.mapper.writeValueAsString(settings.backupEphemeralChatList),
+ JsonMapper.mapper.writeValueAsString(settings.backupEphemeralChatList),
)
} else {
remove(PrefKeys.LATEST_EPHEMERAL_LIST)
@@ -423,7 +475,7 @@ object LocalPreferences {
remove(PrefKeys.USE_PROXY)
remove(PrefKeys.PROXY_PORT)
- putString(PrefKeys.TOR_SETTINGS, EventMapper.mapper.writeValueAsString(settings.torSettings.toSettings()))
+ putString(PrefKeys.TOR_SETTINGS, JsonMapper.mapper.writeValueAsString(settings.torSettings.toSettings()))
val regularMap =
settings.lastReadPerRoute.value.mapValues {
@@ -432,13 +484,13 @@ object LocalPreferences {
putString(
PrefKeys.LAST_READ_PER_ROUTE,
- EventMapper.mapper.writeValueAsString(regularMap),
+ JsonMapper.mapper.writeValueAsString(regularMap),
)
putStringSet(PrefKeys.HAS_DONATED_IN_VERSION, settings.hasDonatedInVersion.value)
putString(
PrefKeys.PENDING_ATTESTATIONS,
- EventMapper.mapper.writeValueAsString(settings.pendingAttestations.value),
+ JsonMapper.mapper.writeValueAsString(settings.pendingAttestations.value),
)
}
}
@@ -453,9 +505,8 @@ object LocalPreferences {
prefs: SharedPreferences = encryptedPreferences(),
) {
Log.d("LocalPreferences", "Saving to shared settings")
- with(prefs.edit()) {
- putString(PrefKeys.SHARED_SETTINGS, EventMapper.mapper.writeValueAsString(sharedSettings))
- apply()
+ prefs.edit {
+ putString(PrefKeys.SHARED_SETTINGS, JsonMapper.mapper.writeValueAsString(sharedSettings))
}
}
@@ -463,7 +514,7 @@ object LocalPreferences {
Log.d("LocalPreferences", "Load shared settings")
with(prefs) {
return try {
- getString(PrefKeys.SHARED_SETTINGS, "{}")?.let { EventMapper.mapper.readValue(it) }
+ getString(PrefKeys.SHARED_SETTINGS, "{}")?.let { JsonMapper.mapper.readValue(it) }
} catch (e: Throwable) {
if (e is CancellationException) throw e
Log.w(
@@ -471,7 +522,6 @@ object LocalPreferences {
"Unable to decode shared preferences: ${getString(PrefKeys.SHARED_SETTINGS, null)}",
e,
)
- e.printStackTrace()
null
}
}
@@ -514,7 +564,7 @@ object LocalPreferences {
?: if (getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)) "com.greenart7c3.nostrsigner" else null
val defaultHomeFollowList =
- getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) ?: KIND3_FOLLOWS
+ getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) ?: ALL_FOLLOWS
val defaultStoriesFollowList =
getString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
val defaultNotificationFollowList =
@@ -522,8 +572,6 @@ object LocalPreferences {
val defaultDiscoveryFollowList =
getString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
- val localRelays = parseOrNull>(PrefKeys.RELAYS) ?: emptySet()
-
val zapPaymentRequestServer = parseOrNull(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER)
val defaultFileServer = parseOrNull(PrefKeys.DEFAULT_FILE_SERVER) ?: DEFAULT_MEDIA_SERVERS[0]
@@ -535,11 +583,16 @@ object LocalPreferences {
val latestDmRelayList = parseEventOrNull(PrefKeys.LATEST_DM_RELAY_LIST)
val latestNip65RelayList = parseEventOrNull(PrefKeys.LATEST_NIP65_RELAY_LIST)
val latestSearchRelayList = parseEventOrNull(PrefKeys.LATEST_SEARCH_RELAY_LIST)
+ val latestBlockedRelayList = parseEventOrNull(PrefKeys.LATEST_BLOCKED_RELAY_LIST)
+ val latestTrustedRelayList = parseEventOrNull(PrefKeys.LATEST_TRUSTED_RELAY_LIST)
val latestMuteList = parseEventOrNull(PrefKeys.LATEST_MUTE_LIST)
val latestPrivateHomeRelayList = parseEventOrNull(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST)
val latestAppSpecificData = parseEventOrNull(PrefKeys.LATEST_APP_SPECIFIC_DATA)
- val latestEphemeralList = parseEventOrNull(PrefKeys.LATEST_EPHEMERAL_LIST)
val latestChannelList = parseEventOrNull(PrefKeys.LATEST_CHANNEL_LIST)
+ val latestCommunityList = parseEventOrNull(PrefKeys.LATEST_COMMUNITY_LIST)
+ val latestHashtagList = parseEventOrNull(PrefKeys.LATEST_HASHTAG_LIST)
+ val latestGeohashList = parseEventOrNull(PrefKeys.LATEST_GEOHASH_LIST)
+ val latestEphemeralList = parseEventOrNull(PrefKeys.LATEST_EPHEMERAL_LIST)
val hideDeleteRequestDialog = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false)
val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false)
@@ -559,14 +612,13 @@ object LocalPreferences {
keyPair = keyPair,
transientAccount = false,
externalSignerPackageName = externalSignerPackageName,
- localRelays = localRelays,
- localRelayServers = localRelayServers,
+ localRelayServers = MutableStateFlow(localRelayServers),
defaultFileServer = defaultFileServer,
defaultHomeFollowList = MutableStateFlow(defaultHomeFollowList),
defaultStoriesFollowList = MutableStateFlow(defaultStoriesFollowList),
defaultNotificationFollowList = MutableStateFlow(defaultNotificationFollowList),
defaultDiscoveryFollowList = MutableStateFlow(defaultDiscoveryFollowList),
- zapPaymentRequest = zapPaymentRequestServer,
+ zapPaymentRequest = MutableStateFlow(zapPaymentRequestServer?.normalize()),
hideDeleteRequestDialog = hideDeleteRequestDialog,
hideBlockAlertDialog = hideBlockAlertDialog,
hideNIP17WarningDialog = hideNIP17WarningDialog,
@@ -575,10 +627,15 @@ object LocalPreferences {
backupNIP65RelayList = latestNip65RelayList,
backupDMRelayList = latestDmRelayList,
backupSearchRelayList = latestSearchRelayList,
+ backupBlockedRelayList = latestBlockedRelayList,
+ backupTrustedRelayList = latestTrustedRelayList,
backupPrivateHomeRelayList = latestPrivateHomeRelayList,
backupMuteList = latestMuteList,
backupAppSpecificData = latestAppSpecificData,
backupChannelList = latestChannelList,
+ backupCommunityList = latestCommunityList,
+ backupHashtagList = latestHashtagList,
+ backupGeohashList = latestGeohashList,
backupEphemeralChatList = latestEphemeralList,
torSettings = TorSettingsFlow.build(torSettings),
lastReadPerRoute = MutableStateFlow(lastReadPerRoute),
@@ -600,12 +657,11 @@ object LocalPreferences {
if (T::class.java.isInstance(Event::class.java)) {
Event.fromJson(value) as T?
} else {
- EventMapper.mapper.readValue(value)
+ JsonMapper.mapper.readValue(value)
}
} catch (e: Throwable) {
if (e is CancellationException) throw e
Log.w("LocalPreferences", "Error Decoding $key from Preferences with value $value", e)
- e.printStackTrace()
null
}
}
@@ -620,7 +676,6 @@ object LocalPreferences {
} catch (e: Throwable) {
if (e is CancellationException) throw e
Log.w("LocalPreferences", "Error Decoding $key from Preferences with value $value", e)
- e.printStackTrace()
null
}
}
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ParallelUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ParallelUtils.kt
deleted file mode 100644
index 7b48e4c071..0000000000
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ParallelUtils.kt
+++ /dev/null
@@ -1,95 +0,0 @@
-/**
- * Copyright (c) 2025 Vitor Pamplona
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy of
- * this software and associated documentation files (the "Software"), to deal in
- * the Software without restriction, including without limitation the rights to use,
- * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
- * Software, and to permit persons to whom the Software is furnished to do so,
- * subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
- * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
- * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
- * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
- * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- */
-package com.vitorpamplona.amethyst
-
-import kotlinx.coroutines.async
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.joinAll
-import kotlinx.coroutines.suspendCancellableCoroutine
-import kotlinx.coroutines.withTimeoutOrNull
-import kotlin.coroutines.Continuation
-import kotlin.coroutines.resume
-
-/**
- * Launches an async coroutine for each item, runs the
- * function and waits for everybody to finsih
- */
-suspend fun launchAndWaitAll(
- items: List,
- asyncFunc: suspend (T) -> Unit,
-) {
- coroutineScope {
- val jobs =
- items.map { next ->
- async {
- asyncFunc(next)
- }
- }
-
- // runs in parallel to avoid overcrowding Amber.
- withTimeoutOrNull(15000) {
- jobs.joinAll()
- }
- }
-}
-
-/**
- * Runs the function and waits for 10 seconds for any result.
- */
-suspend inline fun tryAndWait(
- timeoutMillis: Long = 10000,
- crossinline asyncFunc: (Continuation) -> Unit,
-): T? =
- withTimeoutOrNull(timeoutMillis) {
- suspendCancellableCoroutine { continuation ->
- asyncFunc(continuation)
- }
- }
-
-/**
- * Runs an async coroutine for each one of the items,
- * runs the request for that item,
- * and gathers all the results in the output map.
- */
-suspend fun collectSuccessfulOperations(
- items: List,
- runRequestFor: (T, (K) -> Unit) -> Unit,
- output: MutableList = mutableListOf(),
- onReady: suspend (List) -> Unit,
-) {
- if (items.isEmpty()) {
- onReady(output)
- return
- }
-
- launchAndWaitAll(items) {
- val result =
- tryAndWait { continuation ->
- runRequestFor(it) { result: K -> continuation.resume(result) }
- }
-
- if (result != null) {
- output.add(result)
- }
- }
-
- onReady(output)
-}
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt
deleted file mode 100644
index bc464ebdc1..0000000000
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt
+++ /dev/null
@@ -1,142 +0,0 @@
-/**
- * Copyright (c) 2025 Vitor Pamplona
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy of
- * this software and associated documentation files (the "Software"), to deal in
- * the Software without restriction, including without limitation the rights to use,
- * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
- * Software, and to permit persons to whom the Software is furnished to do so,
- * subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
- * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
- * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
- * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
- * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- */
-package com.vitorpamplona.amethyst
-
-import android.util.Log
-import androidx.compose.runtime.Stable
-import coil3.annotation.DelicateCoilApi
-import com.vitorpamplona.amethyst.model.Account
-import com.vitorpamplona.amethyst.model.LocalCache
-import com.vitorpamplona.ammolite.relays.NostrClient
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.launch
-
-@Stable
-class ServiceManager(
- val client: NostrClient,
- val scope: CoroutineScope,
-) {
- // to not open amber in a loop trying to use auth relays and registering for notifications
- private var isStarted: Boolean = false
-
- private var account: Account? = null
-
- private var collectorJob: Job? = null
-
- private fun start(account: Account) {
- this.account = account
- start()
- }
-
- @OptIn(DelicateCoilApi::class)
- private fun start() {
- Log.d("ServiceManager", "-- May Start (hasStarted: $isStarted) for account $account")
- if (isStarted && account != null) {
- Log.d("ServiceManager", "---- Restarting innactive relay Services with Tor: ${account?.settings?.torSettings?.torType?.value}")
- client.reconnect()
- return
- }
- Log.d("ServiceManager", "---- Starting Relay Services with Tor: ${account?.settings?.torSettings?.torType?.value}")
-
- val myAccount = account
-
- if (myAccount != null) {
- val relaySet = myAccount.connectToRelaysWithProxy.value
- client.reconnect(relaySet)
-
- collectorJob?.cancel()
- collectorJob = null
- collectorJob =
- scope.launch {
- myAccount.connectToRelaysWithProxy.collectLatest {
- delay(500)
- if (isStarted) {
- client.reconnect(it, onlyIfChanged = true)
- }
- }
- }
-
- isStarted = true
- }
- }
-
- private fun pause() {
- Log.d("ServiceManager", "-- Pausing Relay Services")
-
- collectorJob?.cancel()
- collectorJob = null
-
- client.reconnect(null)
- isStarted = false
- }
-
- fun cleanObservers() {
- LocalCache.cleanObservers()
- }
-
- // This method keeps the pause/start in a Syncronized block to
- // avoid concurrent pauses and starts.
- @Synchronized
- fun forceRestart(
- account: Account? = null,
- start: Boolean = true,
- pause: Boolean = true,
- ) {
- Log.d("ServiceManager", "-- Force Restart (start:$start) (pause:$pause) for $account")
- if (pause) {
- pause()
- }
-
- if (start) {
- if (account != null) {
- start(account)
- } else {
- start()
- }
- }
- }
-
- fun setAccountAndRestart(account: Account) {
- forceRestart(account, true, true)
- }
-
- fun forceRestart() {
- forceRestart(null, true, true)
- }
-
- fun justStartIfItHasAccount() {
- if (account != null) {
- forceRestart(null, true, false)
- }
- }
-
- fun pauseForGood() {
- forceRestart(null, false, true)
- }
-
- fun pauseAndLogOff() {
- account = null
- forceRestart(null, false, true)
- }
-}
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
index f153af0263..f0c81c0462 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
@@ -21,40 +21,77 @@
package com.vitorpamplona.amethyst.model
import android.util.Log
-import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
-import com.fasterxml.jackson.module.kotlin.readValue
-import com.fonfon.kgeohash.GeoHash
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
+import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListDecryptionCache
+import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState
+import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
+import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatListDecryptionCache
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatListState
+import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState
+import com.vitorpamplona.amethyst.model.nip01UserMetadata.AccountOutboxRelayState
+import com.vitorpamplona.amethyst.model.nip01UserMetadata.NotificationInboxRelayState
+import com.vitorpamplona.amethyst.model.nip01UserMetadata.UserMetadataState
+import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxOrProxyRelays
+import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListState
+import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowsPerOutboxRelay
+import com.vitorpamplona.amethyst.model.nip03Timestamp.OtsState
+import com.vitorpamplona.amethyst.model.nip17Dms.DmInboxRelayState
+import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState
+import com.vitorpamplona.amethyst.model.nip18Reposts.RepostAction
+import com.vitorpamplona.amethyst.model.nip25Reactions.ReactionAction
+import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
+import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatListDecryptionCache
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatListState
import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState
-import com.vitorpamplona.amethyst.service.checkNotInMainThread
+import com.vitorpamplona.amethyst.model.nip38UserStatuses.UserStatusAction
+import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState
+import com.vitorpamplona.amethyst.model.nip51Lists.BookmarkListState
+import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState
+import com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList.BlockPeopleListState
+import com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList.PeopleListDecryptionCache
+import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListDecryptionCache
+import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState
+import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListDecryptionCache
+import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState
+import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListDecryptionCache
+import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState
+import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListDecryptionCache
+import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListState
+import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListDecryptionCache
+import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListState
+import com.vitorpamplona.amethyst.model.nip51Lists.muteList.MuteListDecryptionCache
+import com.vitorpamplona.amethyst.model.nip51Lists.muteList.MuteListState
+import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListDecryptionCache
+import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState
+import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListDecryptionCache
+import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListState
+import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListDecryptionCache
+import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListState
+import com.vitorpamplona.amethyst.model.nip56Reports.ReportAction
+import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState
+import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListDecryptionCache
+import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState
+import com.vitorpamplona.amethyst.model.nip78AppSpecific.AppSpecificState
+import com.vitorpamplona.amethyst.model.nip96FileStorage.FileStorageServerListState
+import com.vitorpamplona.amethyst.model.nipB7Blossom.BlossomServerListState
+import com.vitorpamplona.amethyst.model.privacyOptions.PrivacyState
+import com.vitorpamplona.amethyst.model.serverList.MergedFollowListsState
+import com.vitorpamplona.amethyst.model.serverList.MergedFollowPlusMineRelayListsState
+import com.vitorpamplona.amethyst.model.serverList.MergedServerListState
+import com.vitorpamplona.amethyst.model.serverList.TrustedRelayListsState
+import com.vitorpamplona.amethyst.model.topNavFeeds.FeedDecryptionCaches
+import com.vitorpamplona.amethyst.model.topNavFeeds.FeedTopNavFilterState
+import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
+import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxLoaderState
+import com.vitorpamplona.amethyst.model.torState.TorRelayState
import com.vitorpamplona.amethyst.service.location.LocationState
-import com.vitorpamplona.amethyst.service.ots.OtsResolverBuilder
-import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentQueryState
+import com.vitorpamplona.amethyst.service.ots.OkHttpOtsResolverBuilder
import com.vitorpamplona.amethyst.service.uploads.FileHeader
-import com.vitorpamplona.amethyst.tryAndWait
-import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
-import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
-import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
-import com.vitorpamplona.amethyst.ui.tor.TorType
-import com.vitorpamplona.ammolite.relays.Constants
-import com.vitorpamplona.ammolite.relays.FeedType
-import com.vitorpamplona.ammolite.relays.Relay
-import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
-import com.vitorpamplona.ammolite.relays.RelaySetupInfo
-import com.vitorpamplona.ammolite.relays.RelaySetupInfoToConnect
-import com.vitorpamplona.ammolite.relays.TypedFilter
-import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
-import com.vitorpamplona.quartz.blossom.BlossomAuthorizationEvent
-import com.vitorpamplona.quartz.blossom.BlossomServersEvent
import com.vitorpamplona.quartz.experimental.bounties.BountyAddValueEvent
-import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent
import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
-import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryReadingStateEvent
@@ -75,33 +112,32 @@ import com.vitorpamplona.quartz.experimental.profileGallery.dimension
import com.vitorpamplona.quartz.experimental.profileGallery.fromEvent
import com.vitorpamplona.quartz.experimental.profileGallery.hash
import com.vitorpamplona.quartz.experimental.profileGallery.mimeType
+import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
-import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
+import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
-import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
+import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
+import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
+import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
+import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.downloadFirstEvent
+import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
+import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
+import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
-import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
-import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableNote
-import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedATags
-import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent
+import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses
import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEventIds
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashes
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip01Core.tags.people.hasAnyTaggedUser
-import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds
import com.vitorpamplona.quartz.nip01Core.tags.references.references
-import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
-import com.vitorpamplona.quartz.nip02FollowList.ReadWrite
-import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag
-import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
-import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
+import com.vitorpamplona.quartz.nip04Dm.PrivateDMCache
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.reply
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
@@ -110,10 +146,8 @@ import com.vitorpamplona.quartz.nip10Notes.content.findHashtags
import com.vitorpamplona.quartz.nip10Notes.content.findNostrUris
import com.vitorpamplona.quartz.nip10Notes.content.findURLs
import com.vitorpamplona.quartz.nip17Dm.NIP17Factory
-import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
-import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip18Reposts.quotes.quotes
@@ -121,53 +155,42 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.Entity
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
+import com.vitorpamplona.quartz.nip19Bech32.entities.NNote
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay
import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
-import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
-import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
import com.vitorpamplona.quartz.nip30CustomEmoji.emojis
-import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent
-import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent
-import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
import com.vitorpamplona.quartz.nip37Drafts.DraftBuilder
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
-import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
+import com.vitorpamplona.quartz.nip37Drafts.DraftEventCache
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
-import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
-import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
import com.vitorpamplona.quartz.nip47WalletConnect.Response
-import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
-import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent
-import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
-import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent
-import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
-import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
-import com.vitorpamplona.quartz.nip56Reports.ReportEvent
+import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
+import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
import com.vitorpamplona.quartz.nip56Reports.ReportType
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
+import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
+import com.vitorpamplona.quartz.nip57Zaps.PrivateZapCache
import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup
import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser
import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent
+import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
-import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
+import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
import com.vitorpamplona.quartz.nip68Picture.PictureMeta
import com.vitorpamplona.quartz.nip68Picture.pictureIMeta
import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent
import com.vitorpamplona.quartz.nip71Video.VideoMeta
import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent
-import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
-import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId
-import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryRequestEvent
import com.vitorpamplona.quartz.nip92IMeta.IMetaTag
import com.vitorpamplona.quartz.nip92IMeta.imetas
@@ -180,975 +203,219 @@ import com.vitorpamplona.quartz.nip94FileMetadata.magnet
import com.vitorpamplona.quartz.nip94FileMetadata.mimeType
import com.vitorpamplona.quartz.nip94FileMetadata.originalHash
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
-import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent
import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent
-import com.vitorpamplona.quartz.utils.DualCase
+import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
+import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.FlowPreview
-import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.combineTransform
-import kotlinx.coroutines.flow.debounce
-import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.flow.transformLatest
-import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import org.czeal.rfc3986.URIReference
import java.math.BigDecimal
-import java.util.Base64
import java.util.Locale
-import kotlin.coroutines.cancellation.CancellationException
-import kotlin.coroutines.resume
@OptIn(DelicateCoroutinesApi::class)
@Stable
class Account(
val settings: AccountSettings = AccountSettings(KeyPair()),
- val signer: NostrSigner = settings.createSigner(),
+ val signer: NostrSigner,
+ geolocationFlow: StateFlow,
+ val cache: LocalCache,
+ val client: NostrClient,
val scope: CoroutineScope,
) {
- companion object {
- const val APP_SPECIFIC_DATA_D_TAG = "AmethystSettings"
- }
-
- var transientHiddenUsers: MutableStateFlow> = MutableStateFlow(setOf())
-
- @Immutable
- class LiveFollowList(
- val authors: Set = emptySet(),
- val authorsPlusMe: Set,
- val hashtags: Set = emptySet(),
- val geotags: Set = emptySet(),
- val addresses: Set = emptySet(),
- ) {
- val geotagScopes: Set = geotags.mapTo(mutableSetOf()) { GeohashId.toScope(it) }
- val hashtagScopes: Set = hashtags.mapTo(mutableSetOf()) { HashtagId.toScope(it) }
- }
-
- class FeedsBaseFlows(
- val listName: String,
- val peopleList: StateFlow = MutableStateFlow(NoteState(Note(" "))),
- val kind3: StateFlow = MutableStateFlow(null),
- val location: StateFlow = MutableStateFlow(null),
- )
-
- val connectToRelaysFlow =
- combineTransform(
- getNIP65RelayListFlow(),
- getDMRelayListFlow(),
- getSearchRelayListFlow(),
- getPrivateOutboxRelayListFlow(),
- userProfile().flow().relays.stateFlow,
- ) { nip65RelayList, dmRelayList, searchRelayList, privateOutBox, userProfile ->
- checkNotInMainThread()
- emit(
- normalizeAndCombineRelayListsWithFallbacks(
- kind3RelayList = kind3Relays(),
- newDMRelayEvent = dmRelayList.note.event as? ChatMessageRelayListEvent,
- searchRelayEvent = searchRelayList.note.event as? SearchRelayListEvent,
- privateOutboxRelayEvent = privateOutBox.note.event as? PrivateOutboxRelayListEvent,
- nip65RelayEvent = nip65RelayList.note.event as? AdvertisedRelayListEvent,
- ).toTypedArray(),
- )
- }
-
- private fun normalizeAndCombineRelayListsWithFallbacks(
- kind3RelayList: Array? = null,
- newDMRelayEvent: ChatMessageRelayListEvent? = null,
- searchRelayEvent: SearchRelayListEvent? = null,
- privateOutboxRelayEvent: PrivateOutboxRelayListEvent? = null,
- nip65RelayEvent: AdvertisedRelayListEvent? = null,
- localRelayList: Set? = null,
- ) = normalizeAndCombineRelayLists(
- baseRelaySet = kind3RelayList ?: convertLocalRelays(),
- newDMRelayEvent = newDMRelayEvent ?: settings.backupDMRelayList,
- searchRelayEvent = searchRelayEvent ?: settings.backupSearchRelayList,
- privateOutboxRelayEvent = privateOutboxRelayEvent ?: settings.backupPrivateHomeRelayList,
- nip65RelayEvent = nip65RelayEvent ?: settings.backupNIP65RelayList,
- localRelayList = localRelayList ?: settings.localRelayServers,
- )
-
- private fun normalizeAndCombineRelayLists(
- baseRelaySet: Array,
- newDMRelayEvent: ChatMessageRelayListEvent?,
- searchRelayEvent: SearchRelayListEvent?,
- privateOutboxRelayEvent: PrivateOutboxRelayListEvent?,
- nip65RelayEvent: AdvertisedRelayListEvent?,
- localRelayList: Set,
- ): List {
- val newDMRelaySet = newDMRelayEvent?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet()
- val searchRelaySet = (searchRelayEvent?.relays() ?: DefaultSearchRelayList).map { RelayUrlFormatter.normalize(it) }.toSet()
- val nip65RelaySet =
- nip65RelayEvent?.relays()?.map {
- AdvertisedRelayListEvent.AdvertisedRelayInfo(
- RelayUrlFormatter.normalize(it.relayUrl),
- it.type,
- )
- }
- val privateOutboxRelaySet = privateOutboxRelayEvent?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet()
- val localRelaySet = localRelayList.map { RelayUrlFormatter.normalize(it) }.toSet()
-
- return combineRelayLists(
- baseRelaySet = baseRelaySet,
- newDMRelaySet = newDMRelaySet,
- searchRelaySet = searchRelaySet,
- privateOutboxRelaySet = privateOutboxRelaySet,
- nip65RelaySet = nip65RelaySet,
- localRelaySet = localRelaySet,
- )
- }
-
- private fun combineRelayLists(
- baseRelaySet: Array,
- newDMRelaySet: Set,
- searchRelaySet: Set,
- privateOutboxRelaySet: Set,
- nip65RelaySet: List?,
- localRelaySet: Set,
- ): List {
- // ------
- // DMs
- // ------
- var mappedRelaySet =
- baseRelaySet.map {
- if (newDMRelaySet.contains(it.url)) {
- RelaySetupInfo(it.url, true, true, it.feedTypes + FeedType.PRIVATE_DMS)
- } else {
- it
- }
- }
-
- newDMRelaySet.forEach { newUrl ->
- if (mappedRelaySet.none { it.url == newUrl }) {
- mappedRelaySet = mappedRelaySet +
- RelaySetupInfo(
- newUrl,
- true,
- true,
- setOf(
- FeedType.PRIVATE_DMS,
- ),
- )
- }
- }
-
- // ------
- // SEARCH
- // ------
-
- mappedRelaySet =
- mappedRelaySet.map {
- if (searchRelaySet.contains(it.url)) {
- RelaySetupInfo(it.url, true, it.write || false, it.feedTypes + FeedType.SEARCH)
- } else {
- it
- }
- }
-
- searchRelaySet.forEach { newUrl ->
- if (mappedRelaySet.none { it.url == newUrl }) {
- mappedRelaySet = mappedRelaySet +
- RelaySetupInfo(
- newUrl,
- true,
- false,
- setOf(
- FeedType.SEARCH,
- ),
- )
- }
- }
-
- // --------------
- // PRIVATE OUTBOX
- // --------------
-
- mappedRelaySet =
- mappedRelaySet.map {
- if (privateOutboxRelaySet.contains(it.url)) {
- RelaySetupInfo(it.url, true, true, it.feedTypes + setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL, FeedType.PRIVATE_DMS))
- } else {
- it
- }
- }
-
- privateOutboxRelaySet.forEach { newUrl ->
- if (mappedRelaySet.none { it.url == newUrl }) {
- mappedRelaySet = mappedRelaySet +
- RelaySetupInfo(
- newUrl,
- true,
- true,
- setOf(
- FeedType.FOLLOWS,
- FeedType.PUBLIC_CHATS,
- FeedType.GLOBAL,
- FeedType.PRIVATE_DMS,
- ),
- )
- }
- }
-
- // --------------
- // Local Storage
- // --------------
-
- mappedRelaySet =
- mappedRelaySet.map {
- if (localRelaySet.contains(it.url)) {
- RelaySetupInfo(it.url, true, true, it.feedTypes + setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL, FeedType.PRIVATE_DMS))
- } else {
- it
- }
- }
-
- localRelaySet.forEach { newUrl ->
- if (mappedRelaySet.none { it.url == newUrl }) {
- mappedRelaySet = mappedRelaySet +
- RelaySetupInfo(
- newUrl,
- true,
- true,
- setOf(
- FeedType.FOLLOWS,
- FeedType.PUBLIC_CHATS,
- FeedType.GLOBAL,
- FeedType.PRIVATE_DMS,
- ),
- )
- }
- }
-
- // --------------
- // NIP-65 Public Inbox/Outbox
- // --------------
-
- mappedRelaySet =
- mappedRelaySet.map { relay ->
- val nip65setup = nip65RelaySet?.firstOrNull { relay.url == it.relayUrl }
- if (nip65setup != null) {
- val write = nip65setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.BOTH || nip65setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.WRITE
-
- RelaySetupInfo(
- relay.url,
- true,
- relay.write || write,
- relay.feedTypes +
- setOf(
- FeedType.FOLLOWS,
- FeedType.GLOBAL,
- FeedType.PUBLIC_CHATS,
- ),
- )
- } else {
- relay
- }
- }
-
- nip65RelaySet?.forEach { newNip65Setup ->
- if (mappedRelaySet.none { it.url == newNip65Setup.relayUrl }) {
- val write = newNip65Setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.BOTH || newNip65Setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.WRITE
-
- mappedRelaySet = mappedRelaySet +
- RelaySetupInfo(
- newNip65Setup.relayUrl,
- true,
- write,
- setOf(
- FeedType.FOLLOWS,
- FeedType.PUBLIC_CHATS,
- ),
- )
- }
- }
- return mappedRelaySet
- }
-
- val connectToRelays =
- connectToRelaysFlow
- .flowOn(Dispatchers.Default)
- .stateIn(
- scope,
- SharingStarted.Eagerly,
- normalizeAndCombineRelayListsWithFallbacks(
- kind3Relays(),
- getDMRelayList(),
- getSearchRelayList(),
- getPrivateOutboxRelayList(),
- getNIP65RelayList(),
- ).toTypedArray(),
- )
-
- val connectToRelaysWithProxy =
- combineTransform(
- connectToRelays,
- settings.torSettings.torType,
- settings.torSettings.onionRelaysViaTor,
- settings.torSettings.trustedRelaysViaTor,
- ) { relays, torType, useTorForOnionRelays, useTorForTrustedRelays ->
- emit(
- relays
- .map {
- RelaySetupInfoToConnect(
- it.url,
- torType != TorType.OFF && checkLocalHostOnionAndThen(it.url, useTorForOnionRelays, useTorForTrustedRelays),
- it.read,
- it.write,
- it.feedTypes,
- )
- }.toTypedArray(),
- )
- }.flowOn(Dispatchers.Default)
- .stateIn(
- scope,
- SharingStarted.Eagerly,
- normalizeAndCombineRelayListsWithFallbacks(
- kind3Relays(),
- getDMRelayList(),
- getSearchRelayList(),
- getPrivateOutboxRelayList(),
- getNIP65RelayList(),
- ).map {
- RelaySetupInfoToConnect(
- it.url,
- settings.torSettings.torType.value != TorType.OFF &&
- checkLocalHostOnionAndThen(
- it.url,
- settings.torSettings.onionRelaysViaTor.value,
- settings.torSettings.trustedRelaysViaTor.value,
- ),
- it.read,
- it.write,
- it.feedTypes,
- )
- }.toTypedArray(),
- )
-
- fun buildFollowLists(latestContactList: ContactListEvent?): LiveFollowList {
- // makes sure the output include only valid p tags
- val verifiedFollowingUsers = latestContactList?.verifiedFollowKeySet() ?: emptySet()
-
- return LiveFollowList(
- authors = verifiedFollowingUsers,
- authorsPlusMe = verifiedFollowingUsers + signer.pubKey,
- hashtags =
- latestContactList
- ?.unverifiedFollowTagSet()
- ?.map { it.lowercase() }
- ?.toSet() ?: emptySet(),
- geotags =
- latestContactList
- ?.geohashes()
- ?.toSet() ?: emptySet(),
- addresses =
- latestContactList
- ?.verifiedFollowAddressSet()
- ?.toSet() ?: emptySet(),
- )
- }
-
- fun normalizeDMRelayListWithBackup(note: Note): Set {
- val event = note.event as? ChatMessageRelayListEvent ?: settings.backupDMRelayList
- return event?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet()
- }
-
- val normalizedDmRelaySet =
- getDMRelayListFlow()
- .map { normalizeDMRelayListWithBackup(it.note) }
- .flowOn(Dispatchers.Default)
- .stateIn(
- scope,
- SharingStarted.Eagerly,
- normalizeDMRelayListWithBackup(getDMRelayListNote()),
- )
-
- fun normalizePrivateOutboxRelayListWithBackup(note: Note): Set {
- val event = note.event as? PrivateOutboxRelayListEvent ?: settings.backupPrivateHomeRelayList
- return event?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet()
- }
-
- val normalizedPrivateOutBoxRelaySet =
- getPrivateOutboxRelayListFlow()
- .map { normalizePrivateOutboxRelayListWithBackup(it.note) }
- .flowOn(Dispatchers.Default)
- .stateIn(
- scope,
- SharingStarted.Eagerly,
- normalizePrivateOutboxRelayListWithBackup(getPrivateOutboxRelayListNote()),
- )
-
- fun normalizeNIP65WriteRelayListWithBackup(note: Note): Set {
- val event = note.event as? AdvertisedRelayListEvent ?: settings.backupNIP65RelayList
- return event?.writeRelays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet()
- }
-
- val normalizedNIP65WriteRelayList =
- getNIP65RelayListFlow()
- .map { normalizeNIP65WriteRelayListWithBackup(it.note) }
- .flowOn(Dispatchers.Default)
- .stateIn(
- scope,
- SharingStarted.Eagerly,
- normalizeNIP65WriteRelayListWithBackup(getNIP65RelayListNote()),
- )
-
- @OptIn(ExperimentalCoroutinesApi::class)
- val liveKind3FollowsFlow: Flow =
- userProfile().flow().follows.stateFlow.transformLatest {
- emit(buildFollowLists(it.user.latestContactList))
- }
-
- val liveKind3Follows =
- liveKind3FollowsFlow
- .flowOn(Dispatchers.Default)
- .stateIn(
- scope,
- SharingStarted.Eagerly,
- buildFollowLists(userProfile().latestContactList ?: settings.backupContactList),
- )
-
- fun loadFlowsFor(listName: String): FeedsBaseFlows =
- when (listName) {
- GLOBAL_FOLLOWS -> FeedsBaseFlows(listName)
- KIND3_FOLLOWS -> FeedsBaseFlows(listName, kind3 = liveKind3Follows)
- AROUND_ME ->
- FeedsBaseFlows(
- listName,
- location = Amethyst.instance.locationManager.geohashStateFlow,
- )
- else -> {
- val note = LocalCache.checkGetOrCreateAddressableNote(listName)
- if (note != null) {
- FeedsBaseFlows(
- listName,
- peopleList =
- note
- .flow()
- .metadata.stateFlow,
- )
- } else {
- FeedsBaseFlows(listName)
- }
- }
- }
-
- fun compute50kmLine(geoHash: GeoHash): List {
- val hashes = mutableListOf()
-
- hashes.add(geoHash.toString())
-
- var currentGeoHash = geoHash
- repeat(5) {
- currentGeoHash = currentGeoHash.westernNeighbour
- hashes.add(currentGeoHash.toString())
- }
-
- currentGeoHash = geoHash
- repeat(5) {
- currentGeoHash = currentGeoHash.easternNeighbour
- hashes.add(currentGeoHash.toString())
- }
-
- return hashes
- }
-
- fun compute50kmRange(geoHash: GeoHash): List {
- val hashes = mutableListOf()
-
- hashes.addAll(compute50kmLine(geoHash))
-
- var currentGeoHash = geoHash
- repeat(5) {
- currentGeoHash = currentGeoHash.northernNeighbour
- hashes.addAll(compute50kmLine(currentGeoHash))
- }
-
- currentGeoHash = geoHash
- repeat(5) {
- currentGeoHash = currentGeoHash.southernNeighbour
- hashes.addAll(compute50kmLine(currentGeoHash))
- }
-
- return hashes
- }
-
- suspend fun mapIntoFollowLists(
- listName: String,
- kind3: LiveFollowList?,
- noteState: NoteState,
- location: LocationState.LocationResult?,
- ): LiveFollowList? =
- if (listName == GLOBAL_FOLLOWS) {
- null
- } else if (listName == KIND3_FOLLOWS) {
- kind3
- } else if (listName == AROUND_ME) {
- val geohashResult = location ?: Amethyst.instance.locationManager.geohashStateFlow.value
- if (geohashResult is LocationState.LocationResult.Success) {
- // 2 neighbors deep = 25x25km
- LiveFollowList(
- authorsPlusMe = setOf(signer.pubKey),
- geotags = compute50kmRange(geohashResult.geoHash).toSet(),
- )
- } else {
- LiveFollowList(authorsPlusMe = setOf(signer.pubKey))
- }
- } else {
- val noteEvent = noteState.note.event
- if (noteEvent is GeneralListEvent) {
- waitToDecrypt(noteEvent) ?: LiveFollowList(authorsPlusMe = setOf(signer.pubKey))
- } else if (noteEvent is FollowListEvent) {
- LiveFollowList(authors = noteEvent.pubKeys().toSet(), authorsPlusMe = setOf(signer.pubKey) + noteEvent.pubKeys())
- } else {
- LiveFollowList(authorsPlusMe = setOf(signer.pubKey))
- }
- }
-
- @OptIn(ExperimentalCoroutinesApi::class)
- fun combinePeopleListFlows(peopleListFollowsSource: Flow): Flow =
- peopleListFollowsSource
- .transformLatest { listName ->
- val followList = loadFlowsFor(listName)
- emitAll(
- combine(followList.kind3, followList.peopleList, followList.location) { kind3, peopleList, location ->
- mapIntoFollowLists(followList.listName, kind3, peopleList, location)
- },
- )
- }
-
- val liveHomeFollowLists: StateFlow by lazy {
- combinePeopleListFlows(settings.defaultHomeFollowList)
- .flowOn(Dispatchers.Default)
- .stateIn(
- scope,
- SharingStarted.Eagerly,
- runBlocking {
- loadAndCombineFlows(settings.defaultHomeFollowList.value)
- },
- )
- }
-
- val liveServerList: StateFlow> by lazy {
- combine(getFileServersListFlow(), getBlossomServersListFlow()) { nip96, blossom ->
- mergeServerList(nip96.note.event as? FileServersEvent, blossom.note.event as? BlossomServersEvent)
- }.flowOn(Dispatchers.Default)
- .stateIn(
- scope,
- SharingStarted.Eagerly,
- runBlocking {
- mergeServerList(getFileServersList(), getBlossomServersList())
- },
- )
- }
-
- suspend fun loadAndCombineFlows(listName: String): LiveFollowList? {
- val flows = loadFlowsFor(listName)
- return mapIntoFollowLists(
- flows.listName,
- flows.kind3.value,
- flows.peopleList.value,
- flows.location.value,
- )
- }
-
- /**
- * filter onion and local host from write relays
- * for each user pubkey, a list of valid relays.
- */
- private fun assembleAuthorsPerWriteRelay(
- userList: Map>,
- hasOnionConnection: Boolean = false,
- ): Map> {
- checkNotInMainThread()
-
- val authorsPerRelayUrl = mutableMapOf>()
- val relayUrlsPerAuthor = mutableMapOf>()
-
- userList.forEach { userWriteRelayListPair ->
- userWriteRelayListPair.value.forEach { relayUrl ->
- if (!RelayUrlFormatter.isLocalHost(relayUrl) && (hasOnionConnection || !RelayUrlFormatter.isOnion(relayUrl))) {
- RelayUrlFormatter.normalizeOrNull(relayUrl)?.let { normRelayUrl ->
- val userSet = authorsPerRelayUrl[normRelayUrl]
- if (userSet != null) {
- userSet.add(userWriteRelayListPair.key)
- } else {
- authorsPerRelayUrl[normRelayUrl] = mutableSetOf(userWriteRelayListPair.key)
- }
-
- val relaySet = authorsPerRelayUrl[userWriteRelayListPair.key]
- if (relaySet != null) {
- relaySet.add(normRelayUrl)
- } else {
- relayUrlsPerAuthor[userWriteRelayListPair.key] = mutableSetOf(normRelayUrl)
- }
- }
- }
- }
- }
-
- // for each relay, authors that only use this relay go first.
- // then keeps order by pubkey asc
- val comparator = compareByDescending { relayUrlsPerAuthor[it]?.size ?: 0 }.thenBy { it }
-
- return authorsPerRelayUrl.mapValues {
- it.value.sortedWith(comparator)
- }
- }
-
- fun authorsPerRelay(
- followsNIP65RelayLists: List,
- defaultRelayList: List,
- torType: TorType,
- ): Map> = authorsPerRelay(followsNIP65RelayLists, defaultRelayList, torType != TorType.OFF)
-
- fun authorsPerRelay(
- followsNIP65RelayLists: List,
- defaultRelayList: List,
- acceptOnion: Boolean,
- ): Map> {
- checkNotInMainThread()
-
- val defaultSet = defaultRelayList.toSet()
-
- return assembleAuthorsPerWriteRelay(
- followsNIP65RelayLists
- .mapNotNull
- {
- val author = (it as? AddressableNote)?.address?.pubKeyHex
- val event = (it.event as? AdvertisedRelayListEvent)
-
- if (event != null) {
- val authorWriteRelays =
- event.writeRelays().map {
- RelayUrlFormatter.normalize(it)
- }
-
- val commonRelaysToMe = authorWriteRelays.filter { it in defaultSet }
- if (commonRelaysToMe.isNotEmpty()) {
- event.pubKey to commonRelaysToMe
- } else {
- event.pubKey to defaultRelayList
- }
- } else {
- if (author != null) {
- author to defaultRelayList
- } else {
- Log.e("Account", "This author should NEVER be null. Note: ${it.idHex}")
- null
- }
- }
- }.toMap(),
- hasOnionConnection = acceptOnion,
- )
- }
-
- @OptIn(ExperimentalCoroutinesApi::class)
- val liveHomeFollowListAdvertizedRelayListFlow: Flow?> =
- liveHomeFollowLists
- .transformLatest { followList ->
- if (followList != null) {
- emitAll(combine(followList.authorsPlusMe.map { getNIP65RelayListFlow(it) }) { it })
- } else {
- emit(null)
- }
- }
-
- val liveHomeListAuthorsPerRelayFlow: Flow