Manual merge, due to other unfinished work.
This commit is contained in:
@@ -25,8 +25,9 @@ If applicable, add a video and/or screenshots to help explain your problem.
|
|||||||
|
|
||||||
**Device (please complete the following information):**
|
**Device (please complete the following information):**
|
||||||
- Phone Brand/Model [e.g. Pixel 7 Pro]:
|
- Phone Brand/Model [e.g. Pixel 7 Pro]:
|
||||||
- Android Version [e.g. 33]:
|
- Android Version [e.g. 34]:
|
||||||
- App Version [e.g. v0.20.3]:
|
- App Version [e.g. v0.94.3]:
|
||||||
|
- App Flavour [e.g. Google Play or FDroid]:
|
||||||
- Amber Version (if using it to sign):
|
- Amber Version (if using it to sign):
|
||||||
|
|
||||||
**Bounty (in Bitcoin sats) offered for a solution**
|
**Bounty (in Bitcoin sats) offered for a solution**
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ android {
|
|||||||
release {
|
release {
|
||||||
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), 'proguard-rules.pro'
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
resValue "string", "app_name", "@string/app_name_release"
|
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
applicationIdSuffix '.debug'
|
applicationIdSuffix '.debug'
|
||||||
|
|||||||
Vendored
+2
-4
@@ -61,7 +61,5 @@
|
|||||||
|
|
||||||
# JSON parsing
|
# JSON parsing
|
||||||
-keep class com.vitorpamplona.quartz.** { *; }
|
-keep class com.vitorpamplona.quartz.** { *; }
|
||||||
-keep class com.vitorpamplona.amethyst.model.** { *; }
|
-keep class com.vitorpamplona.amethyst.** { *; }
|
||||||
-keep class com.vitorpamplona.amethyst.service.** { *; }
|
-keep class com.vitorpamplona.ammolite.** { *; }
|
||||||
-keep class com.vitorpamplona.ammolite.service.** { *; }
|
|
||||||
-keep class com.vitorpamplona.ammolite.relays.** { *; }
|
|
||||||
|
|||||||
+14
-5
@@ -21,17 +21,20 @@
|
|||||||
package com.vitorpamplona.amethyst
|
package com.vitorpamplona.amethyst
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
|
import com.vitorpamplona.amethyst.service.okhttp.EncryptedBlobInterceptor
|
||||||
|
import com.vitorpamplona.amethyst.service.okhttp.EncryptionKeyCache
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||||
import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM
|
import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM
|
||||||
import junit.framework.TestCase.assertEquals
|
import junit.framework.TestCase.assertEquals
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class DMDecryptionTest {
|
class DMFileDecryptionTest {
|
||||||
val okHttp = HttpClientManager.getHttpClient(false)
|
// Key cache to download and decrypt encrypted files before caching them.
|
||||||
|
val keyCache = EncryptionKeyCache()
|
||||||
|
|
||||||
val url = "https://cdn.satellite.earth/812fd4cf9d4d4b59c141ecd6a6c08c7571b5872237ad6477916cb2d119b5cacd"
|
val url = "https://cdn.satellite.earth/812fd4cf9d4d4b59c141ecd6a6c08c7571b5872237ad6477916cb2d119b5cacd"
|
||||||
val cipher =
|
val cipher =
|
||||||
@@ -44,7 +47,13 @@ class DMDecryptionTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun runDownloadAndDecryptVideo() {
|
fun runDownloadAndDecryptVideo() {
|
||||||
HttpClientManager.addCipherToCache(url, cipher, "video/mp4")
|
val client =
|
||||||
|
OkHttpClient
|
||||||
|
.Builder()
|
||||||
|
.addNetworkInterceptor(EncryptedBlobInterceptor(keyCache))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
keyCache.add(url, cipher, "video/mp4")
|
||||||
|
|
||||||
val request =
|
val request =
|
||||||
Request
|
Request
|
||||||
@@ -54,7 +63,7 @@ class DMDecryptionTest {
|
|||||||
.get()
|
.get()
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
okHttp.newCall(request).execute().use {
|
client.newCall(request).execute().use {
|
||||||
assertEquals(decryptedSize, it.body.bytes().size)
|
assertEquals(decryptedSize, it.body.bytes().size)
|
||||||
assertEquals(expectedMimeType, it.body.contentType().toString())
|
assertEquals(expectedMimeType, it.body.contentType().toString())
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
|||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||||
|
import com.vitorpamplona.amethyst.service.okhttp.DefaultContentTypeInterceptor
|
||||||
import com.vitorpamplona.amethyst.service.uploads.FileHeader
|
import com.vitorpamplona.amethyst.service.uploads.FileHeader
|
||||||
import com.vitorpamplona.amethyst.service.uploads.ImageDownloader
|
import com.vitorpamplona.amethyst.service.uploads.ImageDownloader
|
||||||
import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader
|
import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader
|
||||||
@@ -43,6 +44,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.Ignore
|
import org.junit.Ignore
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -52,11 +54,21 @@ import kotlin.random.Random
|
|||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class ImageUploadTesting {
|
class ImageUploadTesting {
|
||||||
val account =
|
companion object {
|
||||||
Account(
|
val account =
|
||||||
AccountSettings(KeyPair()),
|
Account(
|
||||||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
|
AccountSettings(KeyPair()),
|
||||||
)
|
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val client =
|
||||||
|
OkHttpClient
|
||||||
|
.Builder()
|
||||||
|
.followRedirects(true)
|
||||||
|
.followSslRedirects(true)
|
||||||
|
.addInterceptor(DefaultContentTypeInterceptor("Amethyst/${BuildConfig.VERSION_NAME}"))
|
||||||
|
.build()
|
||||||
|
|
||||||
private suspend fun getBitmap(): ByteArray {
|
private suspend fun getBitmap(): ByteArray {
|
||||||
val bitmap = Bitmap.createBitmap(200, 300, Bitmap.Config.ARGB_8888)
|
val bitmap = Bitmap.createBitmap(200, 300, Bitmap.Config.ARGB_8888)
|
||||||
@@ -94,7 +106,7 @@ class ImageUploadTesting {
|
|||||||
alt = null,
|
alt = null,
|
||||||
sensitiveContent = null,
|
sensitiveContent = null,
|
||||||
serverBaseUrl = server.baseUrl,
|
serverBaseUrl = server.baseUrl,
|
||||||
forceProxy = { false },
|
okHttpClient = { client },
|
||||||
httpAuth = account::createBlossomUploadAuth,
|
httpAuth = account::createBlossomUploadAuth,
|
||||||
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
||||||
)
|
)
|
||||||
@@ -105,7 +117,7 @@ class ImageUploadTesting {
|
|||||||
assertEquals("${server.baseUrl}/$initialHash", result.url?.removeSuffix(".png"))
|
assertEquals("${server.baseUrl}/$initialHash", result.url?.removeSuffix(".png"))
|
||||||
|
|
||||||
val imageData: ByteArray =
|
val imageData: ByteArray =
|
||||||
ImageDownloader().waitAndGetImage(result.url!!, false)?.bytes
|
ImageDownloader().waitAndGetImage(result.url!!, { client })?.bytes
|
||||||
?: run {
|
?: run {
|
||||||
fail("${server.name}: Should not be null")
|
fail("${server.name}: Should not be null")
|
||||||
return
|
return
|
||||||
@@ -120,7 +132,7 @@ class ImageUploadTesting {
|
|||||||
ServerInfoRetriever()
|
ServerInfoRetriever()
|
||||||
.loadInfo(
|
.loadInfo(
|
||||||
server.baseUrl,
|
server.baseUrl,
|
||||||
false,
|
{ client },
|
||||||
)
|
)
|
||||||
|
|
||||||
val paylod = getBitmap()
|
val paylod = getBitmap()
|
||||||
@@ -134,7 +146,7 @@ class ImageUploadTesting {
|
|||||||
alt = null,
|
alt = null,
|
||||||
sensitiveContent = null,
|
sensitiveContent = null,
|
||||||
server = serverInfo,
|
server = serverInfo,
|
||||||
forceProxy = { false },
|
okHttpClient = { client },
|
||||||
onProgress = {},
|
onProgress = {},
|
||||||
httpAuth = account::createHTTPAuthorization,
|
httpAuth = account::createHTTPAuthorization,
|
||||||
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
||||||
@@ -148,7 +160,7 @@ class ImageUploadTesting {
|
|||||||
Assert.assertTrue("${server.name}: Invalid result url", url.startsWith("http"))
|
Assert.assertTrue("${server.name}: Invalid result url", url.startsWith("http"))
|
||||||
|
|
||||||
val imageData: ByteArray =
|
val imageData: ByteArray =
|
||||||
ImageDownloader().waitAndGetImage(url, false)?.bytes
|
ImageDownloader().waitAndGetImage(url, { client })?.bytes
|
||||||
?: run {
|
?: run {
|
||||||
fail("${server.name}: Should not be null")
|
fail("${server.name}: Should not be null")
|
||||||
return
|
return
|
||||||
@@ -255,6 +267,7 @@ class ImageUploadTesting {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test()
|
@Test()
|
||||||
|
@Ignore("Not Working anymore/ Timeout")
|
||||||
fun testNostrCheckBlossom() =
|
fun testNostrCheckBlossom() =
|
||||||
runBlocking {
|
runBlocking {
|
||||||
testBase(ServerName("nostrcheck", "https://cdn.nostrcheck.me", ServerType.Blossom))
|
testBase(ServerName("nostrcheck", "https://cdn.nostrcheck.me", ServerType.Blossom))
|
||||||
|
|||||||
@@ -21,17 +21,17 @@
|
|||||||
package com.vitorpamplona.amethyst
|
package com.vitorpamplona.amethyst
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import com.vitorpamplona.amethyst.service.ots.OkHttpBlockstreamExplorer
|
import com.vitorpamplona.amethyst.service.ots.OkHttpBitcoinExplorer
|
||||||
import com.vitorpamplona.amethyst.service.ots.OkHttpCalendarBuilder
|
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.crypto.KeyPair
|
||||||
import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
|
import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
|
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
|
||||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
|
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
|
||||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.OpenTimestamps
|
|
||||||
import junit.framework.TestCase.assertEquals
|
import junit.framework.TestCase.assertEquals
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
@@ -44,31 +44,37 @@ class OkHttpOtsTest {
|
|||||||
val otsPendingEvent = "{\"id\":\"12fa15ad4b4cf9dc5940389325b69b93c5c1f59c049c701ee669b275299fdaf1\",\"pubkey\":\"dcaa6c8a2f47b6fef4a34b20e8843c59dbe7c5f07a402338c09fd147dd01d22b\",\"created_at\":1708877521,\"kind\":1040,\"tags\":[[\"e\",\"a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6\"],[\"alt\",\"Opentimestamps Attestation\"]],\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqGNPU2jhd4no+zg2ytDkuf5PIoivr8KHI8BL68aKGNbwELidvzr0usf55CkpKf6OABQI//AQK3sWd2tq+7KO8YNJIARJugjxBGXbZtPwCL0H4/7GL5+SAIPf4w0u+QyOLCtodHRwczovL2JvYi5idGMuY2FsZW5kYXIub3BlbnRpbWVzdGFtcHMub3Jn//AQPDZsJgN1TnJXoUzlsgo93wjwIIfBc7LUqkCbC1BLZRZ+6LXztK50UdH5xe7fn40bupkrCPEEZdtm0/AI0CADXN5ZIncAg9/jDS75DI4uLWh0dHBzOi8vYWxpY2UuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ/AQcELcSrE04cuGKlZQf2LeVwjwILUDSf9vK2GaefKTpn/LV2oUsQaA5WbqaP3C+1ZxQfRNCPEEZdtm0/AIbCtb+yRXFqUAg9/jDS75DI4pKGh0dHBzOi8vZmlubmV5LmNhbGVuZGFyLmV0ZXJuaXR5d2FsbC5jb20=\",\"sig\":\"f6854c0228c15c08aeb70bbabe9ed87bbb7289fab31b13cabac15138bb71179553e06080b83f4a813fbdaf614f63293beea3fc73fe865da6551193fa4d38de04\"}"
|
val otsPendingEvent = "{\"id\":\"12fa15ad4b4cf9dc5940389325b69b93c5c1f59c049c701ee669b275299fdaf1\",\"pubkey\":\"dcaa6c8a2f47b6fef4a34b20e8843c59dbe7c5f07a402338c09fd147dd01d22b\",\"created_at\":1708877521,\"kind\":1040,\"tags\":[[\"e\",\"a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6\"],[\"alt\",\"Opentimestamps Attestation\"]],\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqGNPU2jhd4no+zg2ytDkuf5PIoivr8KHI8BL68aKGNbwELidvzr0usf55CkpKf6OABQI//AQK3sWd2tq+7KO8YNJIARJugjxBGXbZtPwCL0H4/7GL5+SAIPf4w0u+QyOLCtodHRwczovL2JvYi5idGMuY2FsZW5kYXIub3BlbnRpbWVzdGFtcHMub3Jn//AQPDZsJgN1TnJXoUzlsgo93wjwIIfBc7LUqkCbC1BLZRZ+6LXztK50UdH5xe7fn40bupkrCPEEZdtm0/AI0CADXN5ZIncAg9/jDS75DI4uLWh0dHBzOi8vYWxpY2UuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ/AQcELcSrE04cuGKlZQf2LeVwjwILUDSf9vK2GaefKTpn/LV2oUsQaA5WbqaP3C+1ZxQfRNCPEEZdtm0/AIbCtb+yRXFqUAg9/jDS75DI4pKGh0dHBzOi8vZmlubmV5LmNhbGVuZGFyLmV0ZXJuaXR5d2FsbC5jb20=\",\"sig\":\"f6854c0228c15c08aeb70bbabe9ed87bbb7289fab31b13cabac15138bb71179553e06080b83f4a813fbdaf614f63293beea3fc73fe865da6551193fa4d38de04\"}"
|
||||||
|
|
||||||
val otsEvent2Digest = "a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6"
|
val otsEvent2Digest = "a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6"
|
||||||
|
val otsCache = OtsBlockHeightCache()
|
||||||
|
|
||||||
@Before
|
val resolver =
|
||||||
fun setup() {
|
OtsResolver(
|
||||||
OtsResolver.ots = OpenTimestamps(OkHttpBlockstreamExplorer(forceProxy = { false }), OkHttpCalendarBuilder(forceProxy = { false }))
|
OkHttpBitcoinExplorer(
|
||||||
}
|
OkHttpBitcoinExplorer.MEMPOOL_API_URL,
|
||||||
|
client = OkHttpClient.Builder().build(),
|
||||||
|
otsCache,
|
||||||
|
),
|
||||||
|
OkHttpCalendarBuilder { OkHttpClient.Builder().build() },
|
||||||
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun verifyNostrEvent() {
|
fun verifyNostrEvent() {
|
||||||
val ots = EventMapper.fromJson(otsEvent) as OtsEvent
|
val ots = EventMapper.fromJson(otsEvent) as OtsEvent
|
||||||
println(OtsResolver.info(ots.otsByteArray()))
|
println(resolver.info(ots.otsByteArray()))
|
||||||
assertEquals(1707688818L, ots.verify())
|
assertEquals(1707688818L, ots.verify(resolver))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun verifyNostrEvent2() {
|
fun verifyNostrEvent2() {
|
||||||
val ots = EventMapper.fromJson(otsEvent2) as OtsEvent
|
val ots = EventMapper.fromJson(otsEvent2) as OtsEvent
|
||||||
println(OtsResolver.info(ots.otsByteArray()))
|
println(resolver.info(ots.otsByteArray()))
|
||||||
assertEquals(1706322179L, ots.verify())
|
assertEquals(1706322179L, ots.verify(resolver))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun verifyNostrPendingEvent() {
|
fun verifyNostrPendingEvent() {
|
||||||
val ots = EventMapper.fromJson(otsPendingEvent) as OtsEvent
|
val ots = EventMapper.fromJson(otsPendingEvent) as OtsEvent
|
||||||
println(OtsResolver.info(ots.otsByteArray()))
|
println(resolver.info(ots.otsByteArray()))
|
||||||
assertEquals(null, ots.verify())
|
assertEquals(null, ots.verify(resolver))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -78,7 +84,7 @@ class OkHttpOtsTest {
|
|||||||
|
|
||||||
val countDownLatch = CountDownLatch(1)
|
val countDownLatch = CountDownLatch(1)
|
||||||
|
|
||||||
val otsFile = OtsEvent.stamp(otsEvent2Digest)
|
val otsFile = OtsEvent.stamp(otsEvent2Digest, resolver)
|
||||||
|
|
||||||
signer.sign(OtsEvent.build(otsEvent2Digest, otsFile)) {
|
signer.sign(OtsEvent.build(otsEvent2Digest, otsFile)) {
|
||||||
ots = it
|
ots = it
|
||||||
@@ -88,9 +94,9 @@ class OkHttpOtsTest {
|
|||||||
Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS))
|
Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS))
|
||||||
|
|
||||||
println(ots!!.toJson())
|
println(ots!!.toJson())
|
||||||
println(OtsResolver.info(ots!!.otsByteArray()))
|
println(resolver.info(ots.otsByteArray()))
|
||||||
|
|
||||||
// Should not be valid because we need to wait for confirmations
|
// Should not be valid because we need to wait for confirmations
|
||||||
assertEquals(null, ots!!.verify())
|
assertEquals(null, ots.verify(resolver))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name_release">Amethyst</string>
|
<string name="app_name">Amethyst</string>
|
||||||
<string name="app_name_debug">Amy Debug</string>
|
<string name="app_name_debug">Amy Debug</string>
|
||||||
<string name="app_name_benchmark">Amy Benchmark</string>
|
<string name="app_name_benchmark">Amy Benchmark</string>
|
||||||
</resources>
|
</resources>
|
||||||
+3
-1
@@ -89,7 +89,9 @@ class PushMessageReceiver : MessagingReceiver() {
|
|||||||
Log.d(TAG, "New endpoint provided:- $endpoint for Instance: $instance ${pushHandler.getSavedEndpoint()} $sanitizedEndpoint")
|
Log.d(TAG, "New endpoint provided:- $endpoint for Instance: $instance ${pushHandler.getSavedEndpoint()} $sanitizedEndpoint")
|
||||||
pushHandler.setEndpoint(sanitizedEndpoint)
|
pushHandler.setEndpoint(sanitizedEndpoint)
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
RegisterAccounts(LocalPreferences.allSavedAccounts()).go(sanitizedEndpoint)
|
PushNotificationUtils.checkAndInit(sanitizedEndpoint, LocalPreferences.allSavedAccounts()) {
|
||||||
|
Amethyst.instance.okHttpClients.getHttpClient(Amethyst.instance.torManager.isSocksReady())
|
||||||
|
}
|
||||||
notificationManager().getOrCreateZapChannel(appContext)
|
notificationManager().getOrCreateZapChannel(appContext)
|
||||||
notificationManager().getOrCreateDMChannel(appContext)
|
notificationManager().getOrCreateDMChannel(appContext)
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-16
@@ -20,28 +20,53 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.service.notifications
|
package com.vitorpamplona.amethyst.service.notifications
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.vitorpamplona.amethyst.AccountInfo
|
import com.vitorpamplona.amethyst.AccountInfo
|
||||||
import kotlinx.coroutines.CancellationException
|
import com.vitorpamplona.amethyst.service.retryIfException
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
object PushNotificationUtils {
|
object PushNotificationUtils {
|
||||||
var hasInit: Boolean = false
|
var lastToken: String? = null
|
||||||
|
var hasInit: List<AccountInfo>? = null
|
||||||
|
|
||||||
private val pushHandler = PushDistributorHandler
|
private val pushHandler = PushDistributorHandler
|
||||||
|
|
||||||
suspend fun init(accounts: List<AccountInfo>) {
|
suspend fun checkAndInit(
|
||||||
if (hasInit) {
|
accounts: List<AccountInfo>,
|
||||||
return
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
}
|
) = with(Dispatchers.IO) {
|
||||||
try {
|
if (!pushHandler.savedDistributorExists()) return
|
||||||
if (pushHandler.savedDistributorExists()) {
|
|
||||||
val currentDistributor = PushDistributorHandler.getSavedDistributor()
|
|
||||||
PushDistributorHandler.saveDistributor(currentDistributor)
|
|
||||||
|
|
||||||
RegisterAccounts(accounts).go(pushHandler.getSavedEndpoint())
|
val currentDistributor = PushDistributorHandler.getSavedDistributor()
|
||||||
}
|
PushDistributorHandler.saveDistributor(currentDistributor)
|
||||||
} catch (e: Exception) {
|
val token = pushHandler.getSavedEndpoint()
|
||||||
if (e is CancellationException) throw e
|
|
||||||
Log.d("Amethyst-OSSPushUtils", "Failed to get endpoint.")
|
if (hasInit?.equals(accounts) == true && lastToken == token) {
|
||||||
|
return@with
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerToken(token, accounts, okHttpClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun checkAndInit(
|
||||||
|
token: String,
|
||||||
|
accounts: List<AccountInfo>,
|
||||||
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
|
) = with(Dispatchers.IO) {
|
||||||
|
// initializes if the accounts are different or if the token has changed
|
||||||
|
if (hasInit?.equals(accounts) == true && lastToken == token) {
|
||||||
|
return@with
|
||||||
|
}
|
||||||
|
registerToken(token, accounts, okHttpClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun registerToken(
|
||||||
|
token: String,
|
||||||
|
accounts: List<AccountInfo>,
|
||||||
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
|
) = retryIfException("RegisterAccounts") {
|
||||||
|
RegisterAccounts(accounts, okHttpClient).go(token)
|
||||||
|
lastToken = token
|
||||||
|
hasInit = accounts.toList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".ui.MainActivity"
|
android:name=".ui.MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleInstance"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:windowSoftInputMode="adjustResize"
|
||||||
android:configChanges="orientation|screenSize|screenLayout"
|
android:configChanges="orientation|screenSize|screenLayout"
|
||||||
android:taskAffinity=".service.playback.pip.PipVideoActivity"
|
android:taskAffinity=".service.playback.pip.PipVideoActivity"
|
||||||
@@ -127,7 +127,7 @@
|
|||||||
android:autoRemoveFromRecents="true"
|
android:autoRemoveFromRecents="true"
|
||||||
android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize|keyboardHidden|keyboard|uiMode"
|
android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize|keyboardHidden|keyboard|uiMode"
|
||||||
android:supportsPictureInPicture="true"
|
android:supportsPictureInPicture="true"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleInstance"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:theme="@style/noAnimTheme"
|
android:theme="@style/noAnimTheme"
|
||||||
|
|||||||
@@ -21,145 +21,116 @@
|
|||||||
package com.vitorpamplona.amethyst
|
package com.vitorpamplona.amethyst
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Looper
|
|
||||||
import android.os.StrictMode
|
|
||||||
import android.os.StrictMode.ThreadPolicy
|
|
||||||
import android.os.StrictMode.VmPolicy
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.security.crypto.EncryptedSharedPreferences
|
import androidx.security.crypto.EncryptedSharedPreferences
|
||||||
import coil3.ImageLoader
|
|
||||||
import coil3.disk.DiskCache
|
import coil3.disk.DiskCache
|
||||||
import coil3.memory.MemoryCache
|
import coil3.memory.MemoryCache
|
||||||
|
import com.vitorpamplona.amethyst.service.connectivity.ConnectivityManager
|
||||||
|
import com.vitorpamplona.amethyst.service.images.ImageCacheFactory
|
||||||
|
import com.vitorpamplona.amethyst.service.images.ImageLoaderSetup
|
||||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||||
|
import com.vitorpamplona.amethyst.service.logging.Logging
|
||||||
import com.vitorpamplona.amethyst.service.notifications.PokeyReceiver
|
import com.vitorpamplona.amethyst.service.notifications.PokeyReceiver
|
||||||
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
|
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.OkHttpWebSocket
|
||||||
|
import com.vitorpamplona.amethyst.service.ots.OtsBlockHeightCache
|
||||||
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache
|
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache
|
||||||
import com.vitorpamplona.amethyst.ui.MainActivity
|
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCacheFactory
|
||||||
|
import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory
|
||||||
|
import com.vitorpamplona.amethyst.ui.tor.TorManager
|
||||||
import com.vitorpamplona.ammolite.relays.NostrClient
|
import com.vitorpamplona.ammolite.relays.NostrClient
|
||||||
|
import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache
|
||||||
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import okio.Path.Companion.toOkioPath
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.time.measureTimedValue
|
|
||||||
|
|
||||||
class Amethyst : Application() {
|
class Amethyst : Application() {
|
||||||
val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
val appAgent = "Amethyst/${BuildConfig.VERSION_NAME}"
|
||||||
|
|
||||||
val client: NostrClient = NostrClient(OkHttpWebSocket.BuilderFactory())
|
val exceptionHandler =
|
||||||
|
CoroutineExceptionHandler { _, throwable ->
|
||||||
|
Log.e("AmethystCoroutine", "Caught exception: ${throwable.message}", throwable)
|
||||||
|
}
|
||||||
|
|
||||||
// Service Manager is only active when the activity is active.
|
val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob() + exceptionHandler)
|
||||||
val serviceManager = ServiceManager(client, applicationIOScope)
|
|
||||||
|
// Key cache to download and decrypt encrypted files before caching them.
|
||||||
|
val keyCache = EncryptionKeyCache()
|
||||||
|
|
||||||
|
// App services that should be run as soon as there are subscribers to their flows
|
||||||
val locationManager = LocationState(this, applicationIOScope)
|
val locationManager = LocationState(this, applicationIOScope)
|
||||||
|
val torManager = TorManager(this, applicationIOScope)
|
||||||
|
val connManager = ConnectivityManager(this, applicationIOScope)
|
||||||
|
|
||||||
|
// Service that will run at all times.
|
||||||
val pokeyReceiver = PokeyReceiver()
|
val pokeyReceiver = PokeyReceiver()
|
||||||
|
|
||||||
|
val okHttpClients =
|
||||||
|
DualHttpClientManager(
|
||||||
|
userAgent = appAgent,
|
||||||
|
proxyPortProvider = torManager.activePortOrNull,
|
||||||
|
isMobileDataProvider = connManager.isMobileOrNull,
|
||||||
|
keyCache = keyCache,
|
||||||
|
scope = applicationIOScope,
|
||||||
|
)
|
||||||
|
|
||||||
|
val factory =
|
||||||
|
OkHttpWebSocket.BuilderFactory { _, useProxy ->
|
||||||
|
okHttpClients.getHttpClient(useProxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
val client: NostrClient = NostrClient(factory)
|
||||||
|
|
||||||
|
val serviceManager = ServiceManager(client, applicationIOScope)
|
||||||
|
|
||||||
|
val nip95cache: File by lazy { Nip95CacheFactory.new(this) }
|
||||||
|
val videoCache: VideoCache by lazy { VideoCacheFactory.new(this) }
|
||||||
|
val diskCache: DiskCache by lazy { ImageCacheFactory.newDisk(this) }
|
||||||
|
val memoryCache: MemoryCache by lazy { ImageCacheFactory.newMemory(this) }
|
||||||
|
|
||||||
|
val otsVerifCache by lazy { VerificationStateCache() }
|
||||||
|
val otsBlockHeightCache by lazy { OtsBlockHeightCache() }
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
Log.d("AmethystApp", "onCreate $this")
|
||||||
|
|
||||||
|
instance = this
|
||||||
|
|
||||||
|
if (isDebug()) {
|
||||||
|
Logging.setup()
|
||||||
|
}
|
||||||
|
|
||||||
|
// initializes diskcache on an IO thread.
|
||||||
|
applicationIOScope.launch { videoCache }
|
||||||
|
|
||||||
|
// registers to receive events
|
||||||
|
pokeyReceiver.register(this)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onTerminate() {
|
override fun onTerminate() {
|
||||||
super.onTerminate()
|
super.onTerminate()
|
||||||
unregisterReceiver(pokeyReceiver)
|
Log.d("AmethystApp", "onTerminate $this")
|
||||||
applicationIOScope.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun nip95cache() = safeCacheDir.resolve("NIP95")
|
pokeyReceiver.unregister(this)
|
||||||
|
applicationIOScope.cancel("Application onTerminate $this")
|
||||||
val videoCache: VideoCache by lazy {
|
|
||||||
val newCache = VideoCache()
|
|
||||||
runBlocking {
|
|
||||||
newCache.initFileCache(
|
|
||||||
this@Amethyst,
|
|
||||||
safeCacheDir.resolve("exoplayer"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
newCache
|
|
||||||
}
|
|
||||||
|
|
||||||
val coilCache: DiskCache by lazy {
|
|
||||||
DiskCache
|
|
||||||
.Builder()
|
|
||||||
.directory(safeCacheDir.resolve("image_cache").toOkioPath())
|
|
||||||
.maxSizePercent(0.2)
|
|
||||||
.maximumMaxSizeBytes(1024 * 1024 * 1024) // 1GB
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
val memoryCache: MemoryCache by lazy {
|
|
||||||
MemoryCache
|
|
||||||
.Builder()
|
|
||||||
.maxSizePercent(this)
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun contentResolverFn(): ContentResolver = contentResolver
|
fun contentResolverFn(): ContentResolver = contentResolver
|
||||||
|
|
||||||
override fun onCreate() {
|
fun isDebug() = BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "benchmark"
|
||||||
super.onCreate()
|
|
||||||
|
|
||||||
instance = this
|
fun setImageLoader(shouldUseTor: Boolean?) =
|
||||||
|
ImageLoaderSetup.setup(this, diskCache, memoryCache, isDebug()) {
|
||||||
HttpClientManager.setDefaultUserAgent("Amethyst/${BuildConfig.VERSION_NAME}")
|
shouldUseTor?.let { okHttpClients.getHttpClient(it) } ?: okHttpClients.getHttpClient(false)
|
||||||
|
|
||||||
if (BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "benchmark") {
|
|
||||||
StrictMode.setThreadPolicy(
|
|
||||||
ThreadPolicy
|
|
||||||
.Builder()
|
|
||||||
.detectAll()
|
|
||||||
.penaltyLog()
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
StrictMode.setVmPolicy(
|
|
||||||
VmPolicy
|
|
||||||
.Builder()
|
|
||||||
.detectAll()
|
|
||||||
.penaltyLog()
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
Looper.getMainLooper().setMessageLogging(LogMonitor())
|
|
||||||
ChoreographerHelper.start()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
|
||||||
val (value, elapsed) =
|
|
||||||
measureTimedValue {
|
|
||||||
// initializes the video cache in a thread
|
|
||||||
videoCache
|
|
||||||
}
|
|
||||||
Log.d("Rendering Metrics", "VideoCache initialized in $elapsed")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
registerReceiver(
|
|
||||||
pokeyReceiver,
|
|
||||||
IntentFilter(PokeyReceiver.POKEY_ACTION),
|
|
||||||
RECEIVER_EXPORTED,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
@Suppress("UnspecifiedRegisterReceiverFlag")
|
|
||||||
registerReceiver(
|
|
||||||
pokeyReceiver,
|
|
||||||
IntentFilter(PokeyReceiver.POKEY_ACTION),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun imageLoaderBuilder(): ImageLoader.Builder =
|
|
||||||
ImageLoader
|
|
||||||
.Builder(this)
|
|
||||||
.diskCache { coilCache }
|
|
||||||
.memoryCache { memoryCache }
|
|
||||||
|
|
||||||
fun encryptedStorage(npub: String? = null): EncryptedSharedPreferences = EncryptedStorage.preferences(instance, npub)
|
fun encryptedStorage(npub: String? = null): EncryptedSharedPreferences = EncryptedStorage.preferences(instance, npub)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -167,32 +138,16 @@ class Amethyst : Application() {
|
|||||||
*
|
*
|
||||||
* @param level the memory-related event that was raised.
|
* @param level the memory-related event that was raised.
|
||||||
*/
|
*/
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
|
||||||
override fun onTrimMemory(level: Int) {
|
override fun onTrimMemory(level: Int) {
|
||||||
super.onTrimMemory(level)
|
super.onTrimMemory(level)
|
||||||
println("Trim Memory $level")
|
Log.d("AmethystApp", "onTrimMemory $level")
|
||||||
GlobalScope.launch(Dispatchers.Default) {
|
applicationIOScope.launch(Dispatchers.Default) {
|
||||||
println("Trim Memory Inside $level")
|
|
||||||
serviceManager.trimMemory()
|
serviceManager.trimMemory()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createIntent(callbackUri: String): PendingIntent =
|
|
||||||
PendingIntent.getActivity(
|
|
||||||
this,
|
|
||||||
0,
|
|
||||||
Intent(Intent.ACTION_VIEW, callbackUri.toUri(), this, MainActivity::class.java),
|
|
||||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
|
||||||
)
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
lateinit var instance: Amethyst
|
lateinit var instance: Amethyst
|
||||||
private set
|
private set
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val Context.safeCacheDir: File
|
|
||||||
get() {
|
|
||||||
val cacheDir = checkNotNull(cacheDir) { "cacheDir == null" }
|
|
||||||
return cacheDir.apply { mkdirs() }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ fun debugState(context: Context) {
|
|||||||
|
|
||||||
Log.d(
|
Log.d(
|
||||||
"STATE DUMP",
|
"STATE DUMP",
|
||||||
"Image Disk Cache ${(Amethyst.instance.coilCache.size) / (1024 * 1024)}/${(Amethyst.instance.coilCache.maxSize) / (1024 * 1024)} MB",
|
"Image Disk Cache ${(Amethyst.instance.diskCache.size) / (1024 * 1024)}/${(Amethyst.instance.diskCache.maxSize) / (1024 * 1024)} MB",
|
||||||
)
|
)
|
||||||
Log.d(
|
Log.d(
|
||||||
"STATE DUMP",
|
"STATE DUMP",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import android.content.Context
|
|||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.core.content.edit
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
import com.vitorpamplona.amethyst.model.AccountLanguagePreferencesInternal
|
import com.vitorpamplona.amethyst.model.AccountLanguagePreferencesInternal
|
||||||
import com.vitorpamplona.amethyst.model.AccountReactionPreferencesInternal
|
import com.vitorpamplona.amethyst.model.AccountReactionPreferencesInternal
|
||||||
@@ -151,13 +152,13 @@ object LocalPreferences {
|
|||||||
if (info == null) {
|
if (info == null) {
|
||||||
currentAccount = null
|
currentAccount = null
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
encryptedPreferences().edit().clear().apply()
|
encryptedPreferences().edit { clear() }
|
||||||
}
|
}
|
||||||
} else if (currentAccount != info.npub) {
|
} else if (currentAccount != info.npub) {
|
||||||
currentAccount = info.npub
|
currentAccount = info.npub
|
||||||
if (!info.isTransient) {
|
if (!info.isTransient) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
encryptedPreferences().edit().apply { putString(PrefKeys.CURRENT_ACCOUNT, info.npub) }.apply()
|
encryptedPreferences().edit { putString(PrefKeys.CURRENT_ACCOUNT, info.npub) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,7 +190,7 @@ object LocalPreferences {
|
|||||||
|
|
||||||
savedAccounts.emit(migrated)
|
savedAccounts.emit(migrated)
|
||||||
|
|
||||||
edit().apply { putString(PrefKeys.ALL_ACCOUNT_INFO, EventMapper.mapper.writeValueAsString(savedAccounts.value)) }.apply()
|
edit { putString(PrefKeys.ALL_ACCOUNT_INFO, EventMapper.mapper.writeValueAsString(savedAccounts.value)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,13 +207,12 @@ object LocalPreferences {
|
|||||||
savedAccounts.emit(accounts)
|
savedAccounts.emit(accounts)
|
||||||
|
|
||||||
encryptedPreferences()
|
encryptedPreferences()
|
||||||
.edit()
|
.edit {
|
||||||
.apply {
|
|
||||||
putString(
|
putString(
|
||||||
PrefKeys.ALL_ACCOUNT_INFO,
|
PrefKeys.ALL_ACCOUNT_INFO,
|
||||||
EventMapper.mapper.writeValueAsString(accounts.filter { !it.isTransient }),
|
EventMapper.mapper.writeValueAsString(accounts.filter { !it.isTransient }),
|
||||||
)
|
)
|
||||||
}.apply()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,7 +280,7 @@ object LocalPreferences {
|
|||||||
suspend fun updatePrefsForLogout(accountInfo: AccountInfo) {
|
suspend fun updatePrefsForLogout(accountInfo: AccountInfo) {
|
||||||
Log.d("LocalPreferences", "Saving to encrypted storage updatePrefsForLogout ${accountInfo.npub}")
|
Log.d("LocalPreferences", "Saving to encrypted storage updatePrefsForLogout ${accountInfo.npub}")
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
encryptedPreferences(accountInfo.npub).edit().clear().commit()
|
encryptedPreferences(accountInfo.npub).edit(commit = true) { clear() }
|
||||||
removeAccount(accountInfo)
|
removeAccount(accountInfo)
|
||||||
deleteUserPreferenceFile(accountInfo.npub)
|
deleteUserPreferenceFile(accountInfo.npub)
|
||||||
|
|
||||||
@@ -304,142 +304,140 @@ object LocalPreferences {
|
|||||||
if (!settings.transientAccount) {
|
if (!settings.transientAccount) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val prefs = encryptedPreferences(settings.keyPair.pubKey.toNpub())
|
val prefs = encryptedPreferences(settings.keyPair.pubKey.toNpub())
|
||||||
prefs
|
prefs.edit {
|
||||||
.edit()
|
putBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, settings.externalSignerPackageName != null)
|
||||||
.apply {
|
if (settings.externalSignerPackageName != null) {
|
||||||
putBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, settings.externalSignerPackageName != null)
|
remove(PrefKeys.NOSTR_PRIVKEY)
|
||||||
if (settings.externalSignerPackageName != null) {
|
putString(PrefKeys.SIGNER_PACKAGE_NAME, settings.externalSignerPackageName)
|
||||||
remove(PrefKeys.NOSTR_PRIVKEY)
|
} else {
|
||||||
putString(PrefKeys.SIGNER_PACKAGE_NAME, settings.externalSignerPackageName)
|
remove(PrefKeys.SIGNER_PACKAGE_NAME)
|
||||||
} else {
|
settings.keyPair.privKey?.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHexKey()) }
|
||||||
remove(PrefKeys.SIGNER_PACKAGE_NAME)
|
}
|
||||||
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))
|
||||||
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),
|
||||||
|
)
|
||||||
|
putString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, settings.defaultHomeFollowList.value)
|
||||||
|
putString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, settings.defaultStoriesFollowList.value)
|
||||||
|
putString(
|
||||||
|
PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST,
|
||||||
|
settings.defaultNotificationFollowList.value,
|
||||||
|
)
|
||||||
|
putString(
|
||||||
|
PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST,
|
||||||
|
settings.defaultDiscoveryFollowList.value,
|
||||||
|
)
|
||||||
|
putString(
|
||||||
|
PrefKeys.ZAP_PAYMENT_REQUEST_SERVER,
|
||||||
|
EventMapper.mapper.writeValueAsString(settings.zapPaymentRequest),
|
||||||
|
)
|
||||||
|
if (settings.backupContactList != null) {
|
||||||
putString(
|
putString(
|
||||||
PrefKeys.DEFAULT_FILE_SERVER,
|
PrefKeys.LATEST_CONTACT_LIST,
|
||||||
EventMapper.mapper.writeValueAsString(settings.defaultFileServer),
|
EventMapper.mapper.writeValueAsString(settings.backupContactList),
|
||||||
)
|
)
|
||||||
putString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, settings.defaultHomeFollowList.value)
|
} else {
|
||||||
putString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, settings.defaultStoriesFollowList.value)
|
remove(PrefKeys.LATEST_CONTACT_LIST)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.backupUserMetadata != null) {
|
||||||
putString(
|
putString(
|
||||||
PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST,
|
PrefKeys.LATEST_USER_METADATA,
|
||||||
settings.defaultNotificationFollowList.value,
|
EventMapper.mapper.writeValueAsString(settings.backupUserMetadata),
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
remove(PrefKeys.LATEST_USER_METADATA)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.backupDMRelayList != null) {
|
||||||
putString(
|
putString(
|
||||||
PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST,
|
PrefKeys.LATEST_DM_RELAY_LIST,
|
||||||
settings.defaultDiscoveryFollowList.value,
|
EventMapper.mapper.writeValueAsString(settings.backupDMRelayList),
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
remove(PrefKeys.LATEST_DM_RELAY_LIST)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.backupNIP65RelayList != null) {
|
||||||
putString(
|
putString(
|
||||||
PrefKeys.ZAP_PAYMENT_REQUEST_SERVER,
|
PrefKeys.LATEST_NIP65_RELAY_LIST,
|
||||||
EventMapper.mapper.writeValueAsString(settings.zapPaymentRequest),
|
EventMapper.mapper.writeValueAsString(settings.backupNIP65RelayList),
|
||||||
)
|
)
|
||||||
if (settings.backupContactList != null) {
|
} else {
|
||||||
putString(
|
remove(PrefKeys.LATEST_NIP65_RELAY_LIST)
|
||||||
PrefKeys.LATEST_CONTACT_LIST,
|
}
|
||||||
EventMapper.mapper.writeValueAsString(settings.backupContactList),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
remove(PrefKeys.LATEST_CONTACT_LIST)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.backupUserMetadata != null) {
|
|
||||||
putString(
|
|
||||||
PrefKeys.LATEST_USER_METADATA,
|
|
||||||
EventMapper.mapper.writeValueAsString(settings.backupUserMetadata),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
remove(PrefKeys.LATEST_USER_METADATA)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.backupDMRelayList != null) {
|
|
||||||
putString(
|
|
||||||
PrefKeys.LATEST_DM_RELAY_LIST,
|
|
||||||
EventMapper.mapper.writeValueAsString(settings.backupDMRelayList),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
remove(PrefKeys.LATEST_DM_RELAY_LIST)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.backupNIP65RelayList != null) {
|
|
||||||
putString(
|
|
||||||
PrefKeys.LATEST_NIP65_RELAY_LIST,
|
|
||||||
EventMapper.mapper.writeValueAsString(settings.backupNIP65RelayList),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
remove(PrefKeys.LATEST_NIP65_RELAY_LIST)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.backupSearchRelayList != null) {
|
|
||||||
putString(
|
|
||||||
PrefKeys.LATEST_SEARCH_RELAY_LIST,
|
|
||||||
EventMapper.mapper.writeValueAsString(settings.backupSearchRelayList),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
remove(PrefKeys.LATEST_SEARCH_RELAY_LIST)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.localRelayServers.isNotEmpty()) {
|
|
||||||
putStringSet(PrefKeys.LOCAL_RELAY_SERVERS, settings.localRelayServers)
|
|
||||||
} else {
|
|
||||||
remove(PrefKeys.LOCAL_RELAY_SERVERS)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.backupMuteList != null) {
|
|
||||||
putString(
|
|
||||||
PrefKeys.LATEST_MUTE_LIST,
|
|
||||||
EventMapper.mapper.writeValueAsString(settings.backupMuteList),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
remove(PrefKeys.LATEST_MUTE_LIST)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.backupPrivateHomeRelayList != null) {
|
|
||||||
putString(
|
|
||||||
PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST,
|
|
||||||
EventMapper.mapper.writeValueAsString(settings.backupPrivateHomeRelayList),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
remove(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.backupAppSpecificData != null) {
|
|
||||||
putString(
|
|
||||||
PrefKeys.LATEST_APP_SPECIFIC_DATA,
|
|
||||||
EventMapper.mapper.writeValueAsString(settings.backupAppSpecificData),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
remove(PrefKeys.LATEST_APP_SPECIFIC_DATA)
|
|
||||||
}
|
|
||||||
|
|
||||||
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, settings.hideDeleteRequestDialog)
|
|
||||||
putBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, settings.hideNIP17WarningDialog)
|
|
||||||
putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, settings.hideBlockAlertDialog)
|
|
||||||
|
|
||||||
// migrating from previous design
|
|
||||||
remove(PrefKeys.USE_PROXY)
|
|
||||||
remove(PrefKeys.PROXY_PORT)
|
|
||||||
|
|
||||||
putString(PrefKeys.TOR_SETTINGS, EventMapper.mapper.writeValueAsString(settings.torSettings.toSettings()))
|
|
||||||
|
|
||||||
val regularMap =
|
|
||||||
settings.lastReadPerRoute.value.mapValues {
|
|
||||||
it.value.value
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (settings.backupSearchRelayList != null) {
|
||||||
putString(
|
putString(
|
||||||
PrefKeys.LAST_READ_PER_ROUTE,
|
PrefKeys.LATEST_SEARCH_RELAY_LIST,
|
||||||
EventMapper.mapper.writeValueAsString(regularMap),
|
EventMapper.mapper.writeValueAsString(settings.backupSearchRelayList),
|
||||||
)
|
)
|
||||||
putStringSet(PrefKeys.HAS_DONATED_IN_VERSION, settings.hasDonatedInVersion.value)
|
} else {
|
||||||
|
remove(PrefKeys.LATEST_SEARCH_RELAY_LIST)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.localRelayServers.isNotEmpty()) {
|
||||||
|
putStringSet(PrefKeys.LOCAL_RELAY_SERVERS, settings.localRelayServers)
|
||||||
|
} else {
|
||||||
|
remove(PrefKeys.LOCAL_RELAY_SERVERS)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.backupMuteList != null) {
|
||||||
putString(
|
putString(
|
||||||
PrefKeys.PENDING_ATTESTATIONS,
|
PrefKeys.LATEST_MUTE_LIST,
|
||||||
EventMapper.mapper.writeValueAsString(settings.pendingAttestations.value),
|
EventMapper.mapper.writeValueAsString(settings.backupMuteList),
|
||||||
)
|
)
|
||||||
}.apply()
|
} else {
|
||||||
|
remove(PrefKeys.LATEST_MUTE_LIST)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.backupPrivateHomeRelayList != null) {
|
||||||
|
putString(
|
||||||
|
PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST,
|
||||||
|
EventMapper.mapper.writeValueAsString(settings.backupPrivateHomeRelayList),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
remove(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.backupAppSpecificData != null) {
|
||||||
|
putString(
|
||||||
|
PrefKeys.LATEST_APP_SPECIFIC_DATA,
|
||||||
|
EventMapper.mapper.writeValueAsString(settings.backupAppSpecificData),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
remove(PrefKeys.LATEST_APP_SPECIFIC_DATA)
|
||||||
|
}
|
||||||
|
|
||||||
|
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, settings.hideDeleteRequestDialog)
|
||||||
|
putBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, settings.hideNIP17WarningDialog)
|
||||||
|
putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, settings.hideBlockAlertDialog)
|
||||||
|
|
||||||
|
// migrating from previous design
|
||||||
|
remove(PrefKeys.USE_PROXY)
|
||||||
|
remove(PrefKeys.PROXY_PORT)
|
||||||
|
|
||||||
|
putString(PrefKeys.TOR_SETTINGS, EventMapper.mapper.writeValueAsString(settings.torSettings.toSettings()))
|
||||||
|
|
||||||
|
val regularMap =
|
||||||
|
settings.lastReadPerRoute.value.mapValues {
|
||||||
|
it.value.value
|
||||||
|
}
|
||||||
|
|
||||||
|
putString(
|
||||||
|
PrefKeys.LAST_READ_PER_ROUTE,
|
||||||
|
EventMapper.mapper.writeValueAsString(regularMap),
|
||||||
|
)
|
||||||
|
putStringSet(PrefKeys.HAS_DONATED_IN_VERSION, settings.hasDonatedInVersion.value)
|
||||||
|
|
||||||
|
putString(
|
||||||
|
PrefKeys.PENDING_ATTESTATIONS,
|
||||||
|
EventMapper.mapper.writeValueAsString(settings.pendingAttestations.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.d("LocalPreferences", "Saved to encrypted storage")
|
Log.d("LocalPreferences", "Saved to encrypted storage")
|
||||||
@@ -501,161 +499,163 @@ object LocalPreferences {
|
|||||||
|
|
||||||
private suspend fun innerLoadCurrentAccountFromEncryptedStorage(npub: String?): AccountSettings? {
|
private suspend fun innerLoadCurrentAccountFromEncryptedStorage(npub: String?): AccountSettings? {
|
||||||
Log.d("LocalPreferences", "Load account from file $npub")
|
Log.d("LocalPreferences", "Load account from file $npub")
|
||||||
|
val result =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
checkNotInMainThread()
|
||||||
|
|
||||||
return withContext(Dispatchers.IO) {
|
return@withContext with(encryptedPreferences(npub)) {
|
||||||
checkNotInMainThread()
|
val privKey = getString(PrefKeys.NOSTR_PRIVKEY, null)
|
||||||
|
val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return@with null
|
||||||
|
val externalSignerPackageName =
|
||||||
|
getString(PrefKeys.SIGNER_PACKAGE_NAME, null)
|
||||||
|
?: if (getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)) "com.greenart7c3.nostrsigner" else null
|
||||||
|
|
||||||
return@withContext with(encryptedPreferences(npub)) {
|
val defaultHomeFollowList =
|
||||||
val privKey = getString(PrefKeys.NOSTR_PRIVKEY, null)
|
getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) ?: KIND3_FOLLOWS
|
||||||
val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return@with null
|
val defaultStoriesFollowList =
|
||||||
val externalSignerPackageName =
|
getString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
|
||||||
getString(PrefKeys.SIGNER_PACKAGE_NAME, null)
|
val defaultNotificationFollowList =
|
||||||
?: if (getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)) "com.greenart7c3.nostrsigner" else null
|
getString(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
|
||||||
|
val defaultDiscoveryFollowList =
|
||||||
|
getString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
|
||||||
|
|
||||||
val defaultHomeFollowList =
|
val defaultZapType =
|
||||||
getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) ?: KIND3_FOLLOWS
|
getString(PrefKeys.DEFAULT_ZAPTYPE, "")?.let { serverName ->
|
||||||
val defaultStoriesFollowList =
|
LnZapEvent.ZapType.entries.firstOrNull { it.name == serverName }
|
||||||
getString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
|
} ?: LnZapEvent.ZapType.PUBLIC
|
||||||
val defaultNotificationFollowList =
|
|
||||||
getString(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
|
|
||||||
val defaultDiscoveryFollowList =
|
|
||||||
getString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
|
|
||||||
|
|
||||||
val defaultZapType =
|
val localRelays = parseOrNull<Set<RelaySetupInfo>>(PrefKeys.RELAYS) ?: emptySet()
|
||||||
getString(PrefKeys.DEFAULT_ZAPTYPE, "")?.let { serverName ->
|
|
||||||
LnZapEvent.ZapType.entries.firstOrNull { it.name == serverName }
|
|
||||||
} ?: LnZapEvent.ZapType.PUBLIC
|
|
||||||
|
|
||||||
val localRelays = parseOrNull<Set<RelaySetupInfo>>(PrefKeys.RELAYS) ?: emptySet()
|
val zapPaymentRequestServer = parseOrNull<Nip47WalletConnect.Nip47URI>(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER)
|
||||||
|
val defaultFileServer = parseOrNull<ServerName>(PrefKeys.DEFAULT_FILE_SERVER) ?: DEFAULT_MEDIA_SERVERS[0]
|
||||||
|
|
||||||
val zapPaymentRequestServer = parseOrNull<Nip47WalletConnect.Nip47URI>(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER)
|
val pendingAttestations = parseOrNull<Map<HexKey, String>>(PrefKeys.PENDING_ATTESTATIONS) ?: mapOf()
|
||||||
val defaultFileServer = parseOrNull<ServerName>(PrefKeys.DEFAULT_FILE_SERVER) ?: DEFAULT_MEDIA_SERVERS[0]
|
val localRelayServers = getStringSet(PrefKeys.LOCAL_RELAY_SERVERS, null) ?: setOf()
|
||||||
|
|
||||||
val pendingAttestations = parseOrNull<Map<HexKey, String>>(PrefKeys.PENDING_ATTESTATIONS) ?: mapOf()
|
val latestUserMetadata = parseEventOrNull<MetadataEvent>(PrefKeys.LATEST_USER_METADATA)
|
||||||
val localRelayServers = getStringSet(PrefKeys.LOCAL_RELAY_SERVERS, null) ?: setOf()
|
val latestContactList = parseEventOrNull<ContactListEvent>(PrefKeys.LATEST_CONTACT_LIST)
|
||||||
|
val latestDmRelayList = parseEventOrNull<ChatMessageRelayListEvent>(PrefKeys.LATEST_DM_RELAY_LIST)
|
||||||
|
val latestNip65RelayList = parseEventOrNull<AdvertisedRelayListEvent>(PrefKeys.LATEST_NIP65_RELAY_LIST)
|
||||||
|
val latestSearchRelayList = parseEventOrNull<SearchRelayListEvent>(PrefKeys.LATEST_SEARCH_RELAY_LIST)
|
||||||
|
val latestMuteList = parseEventOrNull<MuteListEvent>(PrefKeys.LATEST_MUTE_LIST)
|
||||||
|
val latestPrivateHomeRelayList = parseEventOrNull<PrivateOutboxRelayListEvent>(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST)
|
||||||
|
val latestAppSpecificData = parseEventOrNull<AppSpecificDataEvent>(PrefKeys.LATEST_APP_SPECIFIC_DATA)
|
||||||
|
|
||||||
val latestUserMetadata = parseEventOrNull<MetadataEvent>(PrefKeys.LATEST_USER_METADATA)
|
val syncedSettings =
|
||||||
val latestContactList = parseEventOrNull<ContactListEvent>(PrefKeys.LATEST_CONTACT_LIST)
|
if (latestAppSpecificData != null) {
|
||||||
val latestDmRelayList = parseEventOrNull<ChatMessageRelayListEvent>(PrefKeys.LATEST_DM_RELAY_LIST)
|
null
|
||||||
val latestNip65RelayList = parseEventOrNull<AdvertisedRelayListEvent>(PrefKeys.LATEST_NIP65_RELAY_LIST)
|
} else {
|
||||||
val latestSearchRelayList = parseEventOrNull<SearchRelayListEvent>(PrefKeys.LATEST_SEARCH_RELAY_LIST)
|
// previous version. Delete this when ready.
|
||||||
val latestMuteList = parseEventOrNull<MuteListEvent>(PrefKeys.LATEST_MUTE_LIST)
|
val reactionChoices = parseOrNull<List<String>>(PrefKeys.REACTION_CHOICES)?.ifEmpty { DefaultReactions } ?: DefaultReactions
|
||||||
val latestPrivateHomeRelayList = parseEventOrNull<PrivateOutboxRelayListEvent>(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST)
|
val zapAmountChoices = parseOrNull<List<Long>>(PrefKeys.ZAP_AMOUNTS)?.ifEmpty { DefaultZapAmounts } ?: DefaultZapAmounts
|
||||||
val latestAppSpecificData = parseEventOrNull<AppSpecificDataEvent>(PrefKeys.LATEST_APP_SPECIFIC_DATA)
|
|
||||||
|
|
||||||
val syncedSettings =
|
val languagePreferences = parseOrNull<Map<String, String>>(PrefKeys.LANGUAGE_PREFS) ?: mapOf()
|
||||||
if (latestAppSpecificData != null) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
// previous version. Delete this when ready.
|
|
||||||
val reactionChoices = parseOrNull<List<String>>(PrefKeys.REACTION_CHOICES)?.ifEmpty { DefaultReactions } ?: DefaultReactions
|
|
||||||
val zapAmountChoices = parseOrNull<List<Long>>(PrefKeys.ZAP_AMOUNTS)?.ifEmpty { DefaultZapAmounts } ?: DefaultZapAmounts
|
|
||||||
|
|
||||||
val languagePreferences = parseOrNull<Map<String, String>>(PrefKeys.LANGUAGE_PREFS) ?: mapOf()
|
val showSensitiveContent =
|
||||||
|
if (contains(PrefKeys.SHOW_SENSITIVE_CONTENT)) {
|
||||||
|
getBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, false)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
val filterSpam = getBoolean(PrefKeys.FILTER_SPAM_FROM_STRANGERS, true)
|
||||||
|
val warnAboutReports = getBoolean(PrefKeys.WARN_ABOUT_REPORTS, true)
|
||||||
|
|
||||||
val showSensitiveContent =
|
val dontTranslateFrom = getStringSet(PrefKeys.DONT_TRANSLATE_FROM, null) ?: setOf()
|
||||||
if (contains(PrefKeys.SHOW_SENSITIVE_CONTENT)) {
|
val translateTo = getString(PrefKeys.TRANSLATE_TO, null) ?: Locale.getDefault().language
|
||||||
getBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, false)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
val filterSpam = getBoolean(PrefKeys.FILTER_SPAM_FROM_STRANGERS, true)
|
|
||||||
val warnAboutReports = getBoolean(PrefKeys.WARN_ABOUT_REPORTS, true)
|
|
||||||
|
|
||||||
val dontTranslateFrom = getStringSet(PrefKeys.DONT_TRANSLATE_FROM, null) ?: setOf()
|
AccountSyncedSettingsInternal(
|
||||||
val translateTo = getString(PrefKeys.TRANSLATE_TO, null) ?: Locale.getDefault().language
|
reactions =
|
||||||
|
AccountReactionPreferencesInternal(
|
||||||
|
reactionChoices = reactionChoices,
|
||||||
|
),
|
||||||
|
zaps =
|
||||||
|
AccountZapPreferencesInternal(
|
||||||
|
zapAmountChoices = zapAmountChoices,
|
||||||
|
defaultZapType = defaultZapType,
|
||||||
|
),
|
||||||
|
languages =
|
||||||
|
AccountLanguagePreferencesInternal(
|
||||||
|
dontTranslateFrom = dontTranslateFrom,
|
||||||
|
languagePreferences = languagePreferences,
|
||||||
|
translateTo = translateTo,
|
||||||
|
),
|
||||||
|
security =
|
||||||
|
AccountSecurityPreferencesInternal(
|
||||||
|
showSensitiveContent = showSensitiveContent,
|
||||||
|
warnAboutPostsWithReports = warnAboutReports,
|
||||||
|
filterSpamFromStrangers = filterSpam,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
AccountSyncedSettingsInternal(
|
val hideDeleteRequestDialog = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false)
|
||||||
reactions =
|
val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false)
|
||||||
AccountReactionPreferencesInternal(
|
val hideNIP17WarningDialog = getBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, false)
|
||||||
reactionChoices = reactionChoices,
|
val useProxy = getBoolean(PrefKeys.USE_PROXY, false)
|
||||||
),
|
|
||||||
zaps =
|
|
||||||
AccountZapPreferencesInternal(
|
|
||||||
zapAmountChoices = zapAmountChoices,
|
|
||||||
defaultZapType = defaultZapType,
|
|
||||||
),
|
|
||||||
languages =
|
|
||||||
AccountLanguagePreferencesInternal(
|
|
||||||
dontTranslateFrom = dontTranslateFrom,
|
|
||||||
languagePreferences = languagePreferences,
|
|
||||||
translateTo = translateTo,
|
|
||||||
),
|
|
||||||
security =
|
|
||||||
AccountSecurityPreferencesInternal(
|
|
||||||
showSensitiveContent = showSensitiveContent,
|
|
||||||
warnAboutPostsWithReports = warnAboutReports,
|
|
||||||
filterSpamFromStrangers = filterSpam,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val hideDeleteRequestDialog = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false)
|
val torSettings =
|
||||||
val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false)
|
if (useProxy) {
|
||||||
val hideNIP17WarningDialog = getBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, false)
|
// old settings, means Orbot
|
||||||
val useProxy = getBoolean(PrefKeys.USE_PROXY, false)
|
TorSettings(
|
||||||
|
TorType.EXTERNAL,
|
||||||
|
getInt(PrefKeys.PROXY_PORT, 9050),
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
parseOrNull<TorSettings>(PrefKeys.TOR_SETTINGS) ?: TorSettings()
|
||||||
|
}
|
||||||
|
|
||||||
val torSettings =
|
val lastReadPerRoute =
|
||||||
if (useProxy) {
|
parseOrNull<Map<String, Long>>(PrefKeys.LAST_READ_PER_ROUTE)?.mapValues {
|
||||||
// old settings, means Orbot
|
MutableStateFlow(it.value)
|
||||||
TorSettings(
|
} ?: mapOf()
|
||||||
TorType.EXTERNAL,
|
|
||||||
getInt(PrefKeys.PROXY_PORT, 9050),
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
parseOrNull<TorSettings>(PrefKeys.TOR_SETTINGS) ?: TorSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
val lastReadPerRoute =
|
val keyPair = KeyPair(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray())
|
||||||
parseOrNull<Map<String, Long>>(PrefKeys.LAST_READ_PER_ROUTE)?.mapValues {
|
val hasDonatedInVersion = getStringSet(PrefKeys.HAS_DONATED_IN_VERSION, null) ?: setOf()
|
||||||
MutableStateFlow(it.value)
|
|
||||||
} ?: mapOf()
|
|
||||||
|
|
||||||
val keyPair = KeyPair(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray())
|
return@with AccountSettings(
|
||||||
val hasDonatedInVersion = getStringSet(PrefKeys.HAS_DONATED_IN_VERSION, null) ?: setOf()
|
keyPair = keyPair,
|
||||||
|
transientAccount = false,
|
||||||
return@with AccountSettings(
|
externalSignerPackageName = externalSignerPackageName,
|
||||||
keyPair = keyPair,
|
localRelays = localRelays,
|
||||||
transientAccount = false,
|
localRelayServers = localRelayServers,
|
||||||
externalSignerPackageName = externalSignerPackageName,
|
defaultFileServer = defaultFileServer,
|
||||||
localRelays = localRelays,
|
defaultHomeFollowList = MutableStateFlow(defaultHomeFollowList),
|
||||||
localRelayServers = localRelayServers,
|
defaultStoriesFollowList = MutableStateFlow(defaultStoriesFollowList),
|
||||||
defaultFileServer = defaultFileServer,
|
defaultNotificationFollowList = MutableStateFlow(defaultNotificationFollowList),
|
||||||
defaultHomeFollowList = MutableStateFlow(defaultHomeFollowList),
|
defaultDiscoveryFollowList = MutableStateFlow(defaultDiscoveryFollowList),
|
||||||
defaultStoriesFollowList = MutableStateFlow(defaultStoriesFollowList),
|
zapPaymentRequest = zapPaymentRequestServer,
|
||||||
defaultNotificationFollowList = MutableStateFlow(defaultNotificationFollowList),
|
hideDeleteRequestDialog = hideDeleteRequestDialog,
|
||||||
defaultDiscoveryFollowList = MutableStateFlow(defaultDiscoveryFollowList),
|
hideBlockAlertDialog = hideBlockAlertDialog,
|
||||||
zapPaymentRequest = zapPaymentRequestServer,
|
hideNIP17WarningDialog = hideNIP17WarningDialog,
|
||||||
hideDeleteRequestDialog = hideDeleteRequestDialog,
|
backupUserMetadata = latestUserMetadata,
|
||||||
hideBlockAlertDialog = hideBlockAlertDialog,
|
backupContactList = latestContactList,
|
||||||
hideNIP17WarningDialog = hideNIP17WarningDialog,
|
backupNIP65RelayList = latestNip65RelayList,
|
||||||
backupUserMetadata = latestUserMetadata,
|
backupDMRelayList = latestDmRelayList,
|
||||||
backupContactList = latestContactList,
|
backupSearchRelayList = latestSearchRelayList,
|
||||||
backupNIP65RelayList = latestNip65RelayList,
|
backupPrivateHomeRelayList = latestPrivateHomeRelayList,
|
||||||
backupDMRelayList = latestDmRelayList,
|
backupMuteList = latestMuteList,
|
||||||
backupSearchRelayList = latestSearchRelayList,
|
backupAppSpecificData = latestAppSpecificData,
|
||||||
backupPrivateHomeRelayList = latestPrivateHomeRelayList,
|
backupSyncedSettings = syncedSettings,
|
||||||
backupMuteList = latestMuteList,
|
torSettings = TorSettingsFlow.build(torSettings),
|
||||||
backupAppSpecificData = latestAppSpecificData,
|
lastReadPerRoute = MutableStateFlow(lastReadPerRoute),
|
||||||
backupSyncedSettings = syncedSettings,
|
hasDonatedInVersion = MutableStateFlow(hasDonatedInVersion),
|
||||||
torSettings = TorSettingsFlow.build(torSettings),
|
pendingAttestations = MutableStateFlow(pendingAttestations),
|
||||||
lastReadPerRoute = MutableStateFlow(lastReadPerRoute),
|
)
|
||||||
hasDonatedInVersion = MutableStateFlow(hasDonatedInVersion),
|
}
|
||||||
pendingAttestations = MutableStateFlow(pendingAttestations),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
Log.d("LocalPreferences", "Loaded account from file $npub")
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private inline fun <reified T> SharedPreferences.parseOrNull(key: String): T? {
|
private inline fun <reified T> SharedPreferences.parseOrNull(key: String): T? {
|
||||||
|
|||||||
@@ -20,21 +20,11 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst
|
package com.vitorpamplona.amethyst
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import coil3.SingletonImageLoader
|
|
||||||
import coil3.annotation.DelicateCoilApi
|
import coil3.annotation.DelicateCoilApi
|
||||||
import coil3.gif.AnimatedImageDecoder
|
|
||||||
import coil3.gif.GifDecoder
|
|
||||||
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
|
||||||
import coil3.size.Precision
|
|
||||||
import coil3.svg.SvgDecoder
|
|
||||||
import coil3.util.DebugLogger
|
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.service.Base64Fetcher
|
|
||||||
import com.vitorpamplona.amethyst.service.BlurHashFetcher
|
|
||||||
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
|
import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
|
||||||
@@ -51,17 +41,10 @@ import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
|||||||
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
|
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
|
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrVideoDataSource
|
import com.vitorpamplona.amethyst.service.NostrVideoDataSource
|
||||||
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
|
import com.vitorpamplona.amethyst.service.eventCache.MemoryTrimmingService
|
||||||
import com.vitorpamplona.amethyst.service.ots.OkHttpBlockstreamExplorer
|
|
||||||
import com.vitorpamplona.amethyst.service.ots.OkHttpCalendarBuilder
|
|
||||||
import com.vitorpamplona.amethyst.ui.tor.TorManager
|
|
||||||
import com.vitorpamplona.amethyst.ui.tor.TorType
|
|
||||||
import com.vitorpamplona.ammolite.relays.NostrClient
|
import com.vitorpamplona.ammolite.relays.NostrClient
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
|
|
||||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.OpenTimestamps
|
|
||||||
import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes
|
import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes
|
||||||
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull
|
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -71,7 +54,6 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class ServiceManager(
|
class ServiceManager(
|
||||||
@@ -85,7 +67,7 @@ class ServiceManager(
|
|||||||
|
|
||||||
private var collectorJob: Job? = null
|
private var collectorJob: Job? = null
|
||||||
|
|
||||||
var isTrimmingMemoryMutex = AtomicBoolean(false)
|
private val trimmingService = MemoryTrimmingService()
|
||||||
|
|
||||||
private fun start(account: Account) {
|
private fun start(account: Account) {
|
||||||
this.account = account
|
this.account = account
|
||||||
@@ -94,87 +76,17 @@ class ServiceManager(
|
|||||||
|
|
||||||
@OptIn(DelicateCoilApi::class)
|
@OptIn(DelicateCoilApi::class)
|
||||||
private fun start() {
|
private fun start() {
|
||||||
Log.d("ServiceManager", "Pre Starting Relay Services $isStarted $account")
|
Log.d("ServiceManager", "-- May Start (hasStarted: $isStarted) for account $account")
|
||||||
if (isStarted && account != null) {
|
if (isStarted && account != null) {
|
||||||
|
Log.d("ServiceManager", "---- Restarting innactive relay Services with Tor: ${account?.settings?.torSettings?.torType?.value}")
|
||||||
|
client.reconnect()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Log.d("ServiceManager", "Starting Relay Services Tor: ${account?.settings?.torSettings?.torType?.value}")
|
Log.d("ServiceManager", "---- Starting Relay Services with Tor: ${account?.settings?.torSettings?.torType?.value}")
|
||||||
|
|
||||||
val myAccount = account
|
val myAccount = account
|
||||||
|
|
||||||
// Resets Proxy Use
|
Amethyst.instance.setImageLoader(myAccount?.shouldUseTorForImageDownload())
|
||||||
if (myAccount != null) {
|
|
||||||
when (myAccount.settings.torSettings.torType.value) {
|
|
||||||
TorType.INTERNAL -> {
|
|
||||||
// Tor's lib will automatically set this port.
|
|
||||||
if (TorManager.isSocksReady()) {
|
|
||||||
HttpClientManager.setDefaultProxyOnPort(TorManager.socksPort())
|
|
||||||
} else {
|
|
||||||
HttpClientManager.setProxyNotReady()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TorType.EXTERNAL -> {
|
|
||||||
val port = myAccount.settings.torSettings.externalSocksPort.value
|
|
||||||
if (port > 0) {
|
|
||||||
HttpClientManager.setDefaultProxyOnPort(port)
|
|
||||||
} else {
|
|
||||||
HttpClientManager.setProxyNotReady()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> HttpClientManager.setDefaultProxy(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
OtsResolver.ots =
|
|
||||||
OpenTimestamps(
|
|
||||||
OkHttpBlockstreamExplorer(myAccount::shouldUseTorForMoneyOperations),
|
|
||||||
OkHttpCalendarBuilder(myAccount::shouldUseTorForMoneyOperations),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
OtsResolver.ots =
|
|
||||||
OpenTimestamps(
|
|
||||||
OkHttpBlockstreamExplorer { false },
|
|
||||||
OkHttpCalendarBuilder { false },
|
|
||||||
)
|
|
||||||
|
|
||||||
HttpClientManager.setDefaultProxy(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert this into a flow
|
|
||||||
LocalCache.antiSpam.active = account
|
|
||||||
?.settings
|
|
||||||
?.syncedSettings
|
|
||||||
?.security
|
|
||||||
?.filterSpamFromStrangers ?: true
|
|
||||||
|
|
||||||
SingletonImageLoader.setUnsafe {
|
|
||||||
Amethyst.instance
|
|
||||||
.imageLoaderBuilder()
|
|
||||||
.components {
|
|
||||||
if (Build.VERSION.SDK_INT >= 28) {
|
|
||||||
add(AnimatedImageDecoder.Factory())
|
|
||||||
} else {
|
|
||||||
add(GifDecoder.Factory())
|
|
||||||
}
|
|
||||||
add(SvgDecoder.Factory())
|
|
||||||
add(Base64Fetcher.Factory)
|
|
||||||
add(BlurHashFetcher.Factory)
|
|
||||||
add(Base64Fetcher.BKeyer)
|
|
||||||
add(BlurHashFetcher.BKeyer)
|
|
||||||
add(
|
|
||||||
OkHttpNetworkFetcherFactory(
|
|
||||||
callFactory = {
|
|
||||||
myAccount?.shouldUseTorForImageDownload()?.let { HttpClientManager.getHttpClient(it) }
|
|
||||||
?: HttpClientManager.getHttpClient(false)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}.apply {
|
|
||||||
if (BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "benchmark") {
|
|
||||||
this.logger(DebugLogger())
|
|
||||||
}
|
|
||||||
}.precision(Precision.INEXACT)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (myAccount != null) {
|
if (myAccount != null) {
|
||||||
val relaySet = myAccount.connectToRelaysWithProxy.value
|
val relaySet = myAccount.connectToRelaysWithProxy.value
|
||||||
@@ -230,7 +142,7 @@ class ServiceManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun pause() {
|
private fun pause() {
|
||||||
Log.d("ServiceManager", "Pausing Relay Services")
|
Log.d("ServiceManager", "-- Pausing Relay Services")
|
||||||
|
|
||||||
collectorJob?.cancel()
|
collectorJob?.cancel()
|
||||||
collectorJob = null
|
collectorJob = null
|
||||||
@@ -262,27 +174,7 @@ class ServiceManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun trimMemory() {
|
suspend fun trimMemory() {
|
||||||
if (isTrimmingMemoryMutex.compareAndSet(false, true)) {
|
trimmingService.run(account)
|
||||||
try {
|
|
||||||
LocalCache.cleanObservers()
|
|
||||||
|
|
||||||
val accounts =
|
|
||||||
LocalPreferences.allSavedAccounts().mapNotNull { decodePublicKeyAsHexOrNull(it.npub) }.toSet()
|
|
||||||
|
|
||||||
account?.let {
|
|
||||||
LocalCache.pruneOldAndHiddenMessages(it)
|
|
||||||
NostrChatroomDataSource.clearEOSEs(it)
|
|
||||||
|
|
||||||
LocalCache.pruneHiddenMessages(it)
|
|
||||||
LocalCache.pruneContactLists(accounts)
|
|
||||||
LocalCache.pruneRepliesAndReactions(accounts)
|
|
||||||
LocalCache.prunePastVersionsOfReplaceables()
|
|
||||||
LocalCache.pruneExpiredEvents()
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
isTrimmingMemoryMutex.getAndSet(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method keeps the pause/start in a Syncronized block to
|
// This method keeps the pause/start in a Syncronized block to
|
||||||
@@ -293,6 +185,7 @@ class ServiceManager(
|
|||||||
start: Boolean = true,
|
start: Boolean = true,
|
||||||
pause: Boolean = true,
|
pause: Boolean = true,
|
||||||
) {
|
) {
|
||||||
|
Log.d("ServiceManager", "-- Force Restart (start:$start) (pause:$pause) for $account")
|
||||||
if (pause) {
|
if (pause) {
|
||||||
pause()
|
pause()
|
||||||
}
|
}
|
||||||
@@ -306,25 +199,25 @@ class ServiceManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restartIfDifferentAccount(account: Account) {
|
fun setAccountAndRestart(account: Account) {
|
||||||
if (this.account != account) {
|
forceRestart(account, true, true)
|
||||||
forceRestart(account, true, true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun forceRestart() {
|
fun forceRestart() {
|
||||||
forceRestart(null, true, true)
|
forceRestart(null, true, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun justStart() {
|
fun justStartIfItHasAccount() {
|
||||||
forceRestart(null, true, false)
|
if (account != null) {
|
||||||
|
forceRestart(null, true, false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pauseForGood() {
|
fun pauseForGood() {
|
||||||
forceRestart(null, false, true)
|
forceRestart(null, false, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pauseForGoodAndClearAccount() {
|
fun pauseAndLogOff() {
|
||||||
account = null
|
account = null
|
||||||
forceRestart(null, false, true)
|
forceRestart(null, false, true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
|||||||
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
|
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||||
|
import com.vitorpamplona.amethyst.service.ots.OtsResolverBuilder
|
||||||
import com.vitorpamplona.amethyst.service.uploads.FileHeader
|
import com.vitorpamplona.amethyst.service.uploads.FileHeader
|
||||||
import com.vitorpamplona.amethyst.tryAndWait
|
import com.vitorpamplona.amethyst.tryAndWait
|
||||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
|
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||||
@@ -102,6 +103,7 @@ import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
|||||||
import com.vitorpamplona.quartz.nip02FollowList.ReadWrite
|
import com.vitorpamplona.quartz.nip02FollowList.ReadWrite
|
||||||
import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag
|
import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag
|
||||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
|
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
|
||||||
|
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
|
||||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||||
import com.vitorpamplona.quartz.nip04Dm.messages.reply
|
import com.vitorpamplona.quartz.nip04Dm.messages.reply
|
||||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||||
@@ -1217,7 +1219,7 @@ class Account(
|
|||||||
filterSpam: Boolean,
|
filterSpam: Boolean,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (settings.updateOptOutOptions(warnReports, filterSpam)) {
|
if (settings.updateOptOutOptions(warnReports, filterSpam)) {
|
||||||
if (!settings.syncedSettings.security.filterSpamFromStrangers) {
|
if (!settings.syncedSettings.security.filterSpamFromStrangers.value) {
|
||||||
transientHiddenUsers.update {
|
transientHiddenUsers.update {
|
||||||
emptySet()
|
emptySet()
|
||||||
}
|
}
|
||||||
@@ -1769,8 +1771,10 @@ class Account(
|
|||||||
suspend fun updateAttestations() {
|
suspend fun updateAttestations() {
|
||||||
Log.d("Pending Attestations", "Updating ${settings.pendingAttestations.value.size} pending attestations")
|
Log.d("Pending Attestations", "Updating ${settings.pendingAttestations.value.size} pending attestations")
|
||||||
|
|
||||||
|
val otsResolver = otsResolver()
|
||||||
|
|
||||||
settings.pendingAttestations.value.forEach { pair ->
|
settings.pendingAttestations.value.forEach { pair ->
|
||||||
val otsState = OtsEvent.upgrade(Base64.getDecoder().decode(pair.value), pair.key)
|
val otsState = OtsEvent.upgrade(Base64.getDecoder().decode(pair.value), pair.key, otsResolver)
|
||||||
|
|
||||||
if (otsState != null) {
|
if (otsState != null) {
|
||||||
val hint = LocalCache.getNoteIfExists(pair.key)?.toEventHint<Event>()
|
val hint = LocalCache.getNoteIfExists(pair.key)?.toEventHint<Event>()
|
||||||
@@ -1804,8 +1808,9 @@ class Account(
|
|||||||
if (note.isDraft()) return
|
if (note.isDraft()) return
|
||||||
|
|
||||||
val id = note.event?.id ?: note.idHex
|
val id = note.event?.id ?: note.idHex
|
||||||
|
val otsResolver = otsResolver()
|
||||||
|
|
||||||
settings.addPendingAttestation(id, Base64.getEncoder().encodeToString(OtsEvent.stamp(id)))
|
settings.addPendingAttestation(id, Base64.getEncoder().encodeToString(OtsEvent.stamp(id, otsResolver)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun follow(user: User) {
|
fun follow(user: User) {
|
||||||
@@ -3770,6 +3775,13 @@ class Account(
|
|||||||
|
|
||||||
fun isTrustedRelay(url: String): Boolean = connectToRelays.value.any { it.url == url } || url == settings.zapPaymentRequest?.relayUri
|
fun isTrustedRelay(url: String): Boolean = connectToRelays.value.any { it.url == url } || url == settings.zapPaymentRequest?.relayUri
|
||||||
|
|
||||||
|
fun otsResolver(): OtsResolver =
|
||||||
|
OtsResolverBuilder().build(
|
||||||
|
Amethyst.instance.okHttpClients,
|
||||||
|
::shouldUseTorForMoneyOperations,
|
||||||
|
Amethyst.instance.otsBlockHeightCache,
|
||||||
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Log.d("AccountRegisterObservers", "Init")
|
Log.d("AccountRegisterObservers", "Init")
|
||||||
settings.backupContactList?.let {
|
settings.backupContactList?.let {
|
||||||
|
|||||||
@@ -122,13 +122,13 @@ class AccountSettings(
|
|||||||
var hasDonatedInVersion: MutableStateFlow<Set<String>> = MutableStateFlow(setOf<String>()),
|
var hasDonatedInVersion: MutableStateFlow<Set<String>> = MutableStateFlow(setOf<String>()),
|
||||||
val pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow<Map<HexKey, String>>(mapOf()),
|
val pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow<Map<HexKey, String>>(mapOf()),
|
||||||
) {
|
) {
|
||||||
val saveable = MutableStateFlow(AccountSettingsUpdater(this))
|
val saveable = MutableStateFlow(AccountSettingsUpdater(null))
|
||||||
val syncedSettings: AccountSyncedSettings =
|
val syncedSettings: AccountSyncedSettings =
|
||||||
backupSyncedSettings?.let { AccountSyncedSettings(it) }
|
backupSyncedSettings?.let { AccountSyncedSettings(it) }
|
||||||
?: AccountSyncedSettings(AccountSyncedSettingsInternal())
|
?: AccountSyncedSettings(AccountSyncedSettingsInternal())
|
||||||
|
|
||||||
class AccountSettingsUpdater(
|
class AccountSettingsUpdater(
|
||||||
val accountSettings: AccountSettings,
|
val accountSettings: AccountSettings?,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun saveAccountSettings() {
|
fun saveAccountSettings() {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class AccountSyncedSettings(
|
|||||||
AccountSecurityPreferences(
|
AccountSecurityPreferences(
|
||||||
MutableStateFlow(internalSettings.security.showSensitiveContent),
|
MutableStateFlow(internalSettings.security.showSensitiveContent),
|
||||||
internalSettings.security.warnAboutPostsWithReports,
|
internalSettings.security.warnAboutPostsWithReports,
|
||||||
internalSettings.security.filterSpamFromStrangers,
|
MutableStateFlow(internalSettings.security.filterSpamFromStrangers),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun toInternal(): AccountSyncedSettingsInternal =
|
fun toInternal(): AccountSyncedSettingsInternal =
|
||||||
@@ -70,7 +70,7 @@ class AccountSyncedSettings(
|
|||||||
AccountSecurityPreferencesInternal(
|
AccountSecurityPreferencesInternal(
|
||||||
security.showSensitiveContent.value,
|
security.showSensitiveContent.value,
|
||||||
security.warnAboutPostsWithReports,
|
security.warnAboutPostsWithReports,
|
||||||
security.filterSpamFromStrangers,
|
security.filterSpamFromStrangers.value,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -105,8 +105,8 @@ class AccountSyncedSettings(
|
|||||||
security.showSensitiveContent.tryEmit(syncedSettingsInternal.security.showSensitiveContent)
|
security.showSensitiveContent.tryEmit(syncedSettingsInternal.security.showSensitiveContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (security.filterSpamFromStrangers != syncedSettingsInternal.security.filterSpamFromStrangers) {
|
if (security.filterSpamFromStrangers.value != syncedSettingsInternal.security.filterSpamFromStrangers) {
|
||||||
security.filterSpamFromStrangers = syncedSettingsInternal.security.filterSpamFromStrangers
|
security.filterSpamFromStrangers.tryEmit(syncedSettingsInternal.security.filterSpamFromStrangers)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (security.warnAboutPostsWithReports != syncedSettingsInternal.security.warnAboutPostsWithReports) {
|
if (security.warnAboutPostsWithReports != syncedSettingsInternal.security.warnAboutPostsWithReports) {
|
||||||
@@ -180,7 +180,7 @@ class AccountLanguagePreferences(
|
|||||||
class AccountSecurityPreferences(
|
class AccountSecurityPreferences(
|
||||||
val showSensitiveContent: MutableStateFlow<Boolean?> = MutableStateFlow(null),
|
val showSensitiveContent: MutableStateFlow<Boolean?> = MutableStateFlow(null),
|
||||||
var warnAboutPostsWithReports: Boolean = true,
|
var warnAboutPostsWithReports: Boolean = true,
|
||||||
var filterSpamFromStrangers: Boolean = true,
|
var filterSpamFromStrangers: MutableStateFlow<Boolean> = MutableStateFlow(true),
|
||||||
) {
|
) {
|
||||||
fun updateShowSensitiveContent(show: Boolean?): Boolean {
|
fun updateShowSensitiveContent(show: Boolean?): Boolean {
|
||||||
if (showSensitiveContent.value != show) {
|
if (showSensitiveContent.value != show) {
|
||||||
@@ -197,9 +197,9 @@ class AccountSecurityPreferences(
|
|||||||
warnReports: Boolean,
|
warnReports: Boolean,
|
||||||
filterSpam: Boolean,
|
filterSpam: Boolean,
|
||||||
): Boolean =
|
): Boolean =
|
||||||
if (warnAboutPostsWithReports != warnReports || filterSpam != filterSpamFromStrangers) {
|
if (warnAboutPostsWithReports != warnReports || filterSpam != filterSpamFromStrangers.value) {
|
||||||
warnAboutPostsWithReports = warnReports
|
warnAboutPostsWithReports = warnReports
|
||||||
filterSpamFromStrangers = filterSpam
|
filterSpamFromStrangers.tryEmit(filterSpam)
|
||||||
|
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.commons.hashtags.Btc
|
|||||||
import com.vitorpamplona.amethyst.commons.hashtags.Cashu
|
import com.vitorpamplona.amethyst.commons.hashtags.Cashu
|
||||||
import com.vitorpamplona.amethyst.commons.hashtags.Coffee
|
import com.vitorpamplona.amethyst.commons.hashtags.Coffee
|
||||||
import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons
|
import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons
|
||||||
|
import com.vitorpamplona.amethyst.commons.hashtags.Flowerstr
|
||||||
import com.vitorpamplona.amethyst.commons.hashtags.Footstr
|
import com.vitorpamplona.amethyst.commons.hashtags.Footstr
|
||||||
import com.vitorpamplona.amethyst.commons.hashtags.Gamestr
|
import com.vitorpamplona.amethyst.commons.hashtags.Gamestr
|
||||||
import com.vitorpamplona.amethyst.commons.hashtags.Grownostr
|
import com.vitorpamplona.amethyst.commons.hashtags.Grownostr
|
||||||
@@ -57,7 +58,7 @@ import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
|
|||||||
fun RenderHashTagIconsPreview() {
|
fun RenderHashTagIconsPreview() {
|
||||||
ThemeComparisonColumn {
|
ThemeComparisonColumn {
|
||||||
RenderRegular(
|
RenderRegular(
|
||||||
"Testing rendering of hashtags: #Bitcoin, #nostr, #lightning, #zap, #amethyst, #cashu, #plebs, #coffee, #skullofsatoshi, #grownostr, #footstr, #tunestr, #weed, #mate, #gamestr, #gamechain",
|
"Testing rendering of hashtags: #flowerstr #Bitcoin, #nostr, #lightning, #zap, #amethyst, #cashu, #plebs, #coffee, #skullofsatoshi, #grownostr, #footstr, #tunestr, #weed, #mate, #gamestr, #gamechain",
|
||||||
EmptyTagList,
|
EmptyTagList,
|
||||||
) { word, state ->
|
) { word, state ->
|
||||||
when (word) {
|
when (word) {
|
||||||
@@ -81,6 +82,7 @@ fun checkForHashtagWithIcon(tag: String): HashtagIcon? =
|
|||||||
"skullofsatoshi" -> skull
|
"skullofsatoshi" -> skull
|
||||||
"grownostr", "gardening", "garden" -> growstr
|
"grownostr", "gardening", "garden" -> growstr
|
||||||
"footstr" -> footstr
|
"footstr" -> footstr
|
||||||
|
"flowerstr" -> flowerstr
|
||||||
"tunestr", "music", "nowplaying" -> tunestr
|
"tunestr", "music", "nowplaying" -> tunestr
|
||||||
"mate", "matechain", "matestr" -> matestr
|
"mate", "matechain", "matestr" -> matestr
|
||||||
"weed", "weedstr", "420", "cannabis", "marijuana" -> weed
|
"weed", "weedstr", "420", "cannabis", "marijuana" -> weed
|
||||||
@@ -99,6 +101,7 @@ val coffee = HashtagIcon(CustomHashTagIcons.Coffee, "Coffee", Modifier.padding(s
|
|||||||
val skull = HashtagIcon(CustomHashTagIcons.Skull, "SkullofSatoshi", Modifier.padding(start = 1.dp, bottom = 1.dp, top = 1.dp))
|
val skull = HashtagIcon(CustomHashTagIcons.Skull, "SkullofSatoshi", Modifier.padding(start = 1.dp, bottom = 1.dp, top = 1.dp))
|
||||||
val growstr = HashtagIcon(CustomHashTagIcons.Grownostr, "GrowNostr", Modifier.padding(start = 1.dp, bottom = 1.dp, top = 1.dp))
|
val growstr = HashtagIcon(CustomHashTagIcons.Grownostr, "GrowNostr", Modifier.padding(start = 1.dp, bottom = 1.dp, top = 1.dp))
|
||||||
val footstr = HashtagIcon(CustomHashTagIcons.Footstr, "Footstr", Modifier.padding(start = 2.dp, bottom = 1.dp, top = 1.dp))
|
val footstr = HashtagIcon(CustomHashTagIcons.Footstr, "Footstr", Modifier.padding(start = 2.dp, bottom = 1.dp, top = 1.dp))
|
||||||
|
val flowerstr = HashtagIcon(CustomHashTagIcons.Flowerstr, "Flowerstr", Modifier.padding(start = 2.dp, bottom = 1.dp, top = 1.dp))
|
||||||
val tunestr = HashtagIcon(CustomHashTagIcons.Tunestr, "Tunestr", Modifier.padding(start = 1.dp, bottom = 1.dp, top = 1.dp))
|
val tunestr = HashtagIcon(CustomHashTagIcons.Tunestr, "Tunestr", Modifier.padding(start = 1.dp, bottom = 1.dp, top = 1.dp))
|
||||||
val weed = HashtagIcon(CustomHashTagIcons.Weed, "Weed", Modifier.padding(start = 1.dp, bottom = 0.dp, top = 0.dp))
|
val weed = HashtagIcon(CustomHashTagIcons.Weed, "Weed", Modifier.padding(start = 1.dp, bottom = 0.dp, top = 0.dp))
|
||||||
val matestr = HashtagIcon(CustomHashTagIcons.Mate, "Mate", Modifier.padding(start = 1.dp, bottom = 0.dp, top = 0.dp))
|
val matestr = HashtagIcon(CustomHashTagIcons.Mate, "Mate", Modifier.padding(start = 1.dp, bottom = 0.dp, top = 0.dp))
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.commons.data.DeletionIndex
|
|||||||
import com.vitorpamplona.amethyst.commons.data.LargeCache
|
import com.vitorpamplona.amethyst.commons.data.LargeCache
|
||||||
import com.vitorpamplona.amethyst.model.observables.LatestByKindAndAuthor
|
import com.vitorpamplona.amethyst.model.observables.LatestByKindAndAuthor
|
||||||
import com.vitorpamplona.amethyst.model.observables.LatestByKindWithETag
|
import com.vitorpamplona.amethyst.model.observables.LatestByKindWithETag
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrAccountDataSource.account
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.ammolite.relays.BundledInsert
|
import com.vitorpamplona.ammolite.relays.BundledInsert
|
||||||
import com.vitorpamplona.ammolite.relays.Relay
|
import com.vitorpamplona.ammolite.relays.Relay
|
||||||
@@ -68,6 +69,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUsers
|
|||||||
import com.vitorpamplona.quartz.nip01Core.verify
|
import com.vitorpamplona.quartz.nip01Core.verify
|
||||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
|
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
|
||||||
|
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
|
||||||
import com.vitorpamplona.quartz.nip03Timestamp.VerificationState
|
import com.vitorpamplona.quartz.nip03Timestamp.VerificationState
|
||||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||||
@@ -938,9 +940,6 @@ object LocalCache {
|
|||||||
// Already processed this event.
|
// Already processed this event.
|
||||||
if (version.event?.id == event.id) return
|
if (version.event?.id == event.id) return
|
||||||
|
|
||||||
// makes sure the OTS has a valid certificate
|
|
||||||
if (event.cacheVerify() is VerificationState.Error) return // no valid OTS
|
|
||||||
|
|
||||||
if (version.event == null) {
|
if (version.event == null) {
|
||||||
version.loadEvent(event, author, emptyList())
|
version.loadEvent(event, author, emptyList())
|
||||||
version.liveSet?.innerOts?.invalidateData()
|
version.liveSet?.innerOts?.invalidateData()
|
||||||
@@ -1630,7 +1629,7 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val cachePath = Amethyst.instance.nip95cache()
|
val cachePath = Amethyst.instance.nip95cache
|
||||||
cachePath.mkdirs()
|
cachePath.mkdirs()
|
||||||
val file = File(cachePath, event.id)
|
val file = File(cachePath, event.id)
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
@@ -1991,7 +1990,10 @@ object LocalCache {
|
|||||||
.toImmutableList()
|
.toImmutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun findEarliestOtsForNote(note: Note): Long? {
|
suspend fun findEarliestOtsForNote(
|
||||||
|
note: Note,
|
||||||
|
resolverBuilder: () -> OtsResolver,
|
||||||
|
): Long? {
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
|
|
||||||
var minTime: Long? = null
|
var minTime: Long? = null
|
||||||
@@ -2000,7 +2002,7 @@ object LocalCache {
|
|||||||
notes.forEach { _, item ->
|
notes.forEach { _, item ->
|
||||||
val noteEvent = item.event
|
val noteEvent = item.event
|
||||||
if ((noteEvent is OtsEvent && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time))) {
|
if ((noteEvent is OtsEvent && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time))) {
|
||||||
(noteEvent.cacheVerify() as? VerificationState.Verified)?.verifiedTime?.let { stampedTime ->
|
(Amethyst.instance.otsVerifCache.cacheVerify(noteEvent, resolverBuilder) as? VerificationState.Verified)?.verifiedTime?.let { stampedTime ->
|
||||||
if (minTime == null || stampedTime < (minTime ?: Long.MAX_VALUE)) {
|
if (minTime == null || stampedTime < (minTime ?: Long.MAX_VALUE)) {
|
||||||
minTime = stampedTime
|
minTime = stampedTime
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import android.util.LruCache
|
|||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import com.vitorpamplona.amethyst.service.previews.UrlPreview
|
import com.vitorpamplona.amethyst.service.previews.UrlPreview
|
||||||
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
|
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
object UrlCachedPreviewer {
|
object UrlCachedPreviewer {
|
||||||
@@ -32,7 +33,7 @@ object UrlCachedPreviewer {
|
|||||||
|
|
||||||
suspend fun previewInfo(
|
suspend fun previewInfo(
|
||||||
url: String,
|
url: String,
|
||||||
forceProxy: Boolean,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
onReady: suspend (UrlPreviewState) -> Unit,
|
onReady: suspend (UrlPreviewState) -> Unit,
|
||||||
) {
|
) {
|
||||||
cache[url]?.let {
|
cache[url]?.let {
|
||||||
@@ -42,7 +43,7 @@ object UrlCachedPreviewer {
|
|||||||
|
|
||||||
UrlPreview().fetch(
|
UrlPreview().fetch(
|
||||||
url,
|
url,
|
||||||
forceProxy,
|
okHttpClient,
|
||||||
onComplete = { urlInfo ->
|
onComplete = { urlInfo ->
|
||||||
cache[url]?.let {
|
cache[url]?.let {
|
||||||
if (it is UrlPreviewState.Loaded || it is UrlPreviewState.Empty) {
|
if (it is UrlPreviewState.Loaded || it is UrlPreviewState.Empty) {
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
fun Application.safeCacheDir(): File {
|
||||||
|
val cacheDir = checkNotNull(cacheDir) { "cacheDir == null" }
|
||||||
|
return cacheDir.apply { mkdirs() }
|
||||||
|
}
|
||||||
@@ -28,7 +28,6 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
|||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||||
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
|
|
||||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||||
@@ -40,6 +39,7 @@ import kotlinx.serialization.cbor.ByteString
|
|||||||
import kotlinx.serialization.cbor.Cbor
|
import kotlinx.serialization.cbor.Cbor
|
||||||
import kotlinx.serialization.decodeFromByteArray
|
import kotlinx.serialization.decodeFromByteArray
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
@@ -218,7 +218,7 @@ class CashuProcessor {
|
|||||||
suspend fun melt(
|
suspend fun melt(
|
||||||
token: CashuToken,
|
token: CashuToken,
|
||||||
lud16: String,
|
lud16: String,
|
||||||
forceProxy: (url: String) -> Boolean,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
onSuccess: (String, String) -> Unit,
|
onSuccess: (String, String) -> Unit,
|
||||||
onError: (String, String) -> Unit,
|
onError: (String, String) -> Unit,
|
||||||
context: Context,
|
context: Context,
|
||||||
@@ -232,12 +232,12 @@ class CashuProcessor {
|
|||||||
// Make invoice and leave room for fees
|
// Make invoice and leave room for fees
|
||||||
milliSats = token.totalAmount * 1000,
|
milliSats = token.totalAmount * 1000,
|
||||||
message = "Calculate Fees for Cashu",
|
message = "Calculate Fees for Cashu",
|
||||||
forceProxy = forceProxy,
|
okHttpClient = okHttpClient,
|
||||||
onSuccess = { baseInvoice ->
|
onSuccess = { baseInvoice ->
|
||||||
feeCalculator(
|
feeCalculator(
|
||||||
token.mint,
|
token.mint,
|
||||||
baseInvoice,
|
baseInvoice,
|
||||||
forceProxy = forceProxy,
|
okHttpClient = okHttpClient,
|
||||||
onSuccess = { fees ->
|
onSuccess = { fees ->
|
||||||
LightningAddressResolver()
|
LightningAddressResolver()
|
||||||
.lnAddressInvoice(
|
.lnAddressInvoice(
|
||||||
@@ -245,9 +245,9 @@ class CashuProcessor {
|
|||||||
// Make invoice and leave room for fees
|
// Make invoice and leave room for fees
|
||||||
milliSats = (token.totalAmount - fees) * 1000,
|
milliSats = (token.totalAmount - fees) * 1000,
|
||||||
message = "Redeem Cashu",
|
message = "Redeem Cashu",
|
||||||
forceProxy = forceProxy,
|
okHttpClient = okHttpClient,
|
||||||
onSuccess = { invoice ->
|
onSuccess = { invoice ->
|
||||||
meltInvoice(token, invoice, fees, forceProxy, onSuccess, onError, context)
|
meltInvoice(token, invoice, fees, okHttpClient, onSuccess, onError, context)
|
||||||
},
|
},
|
||||||
onProgress = {},
|
onProgress = {},
|
||||||
onError = onError,
|
onError = onError,
|
||||||
@@ -268,7 +268,7 @@ class CashuProcessor {
|
|||||||
fun feeCalculator(
|
fun feeCalculator(
|
||||||
mintAddress: String,
|
mintAddress: String,
|
||||||
invoice: String,
|
invoice: String,
|
||||||
forceProxy: (String) -> Boolean,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
onSuccess: (Int) -> Unit,
|
onSuccess: (Int) -> Unit,
|
||||||
onError: (String, String) -> Unit,
|
onError: (String, String) -> Unit,
|
||||||
context: Context,
|
context: Context,
|
||||||
@@ -277,7 +277,7 @@ class CashuProcessor {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
val url = "$mintAddress/checkfees" // Melt cashu tokens at Mint
|
val url = "$mintAddress/checkfees" // Melt cashu tokens at Mint
|
||||||
val client = HttpClientManager.getHttpClient(forceProxy(url))
|
val client = okHttpClient(url)
|
||||||
|
|
||||||
val factory = JsonNodeFactory.instance
|
val factory = JsonNodeFactory.instance
|
||||||
|
|
||||||
@@ -334,14 +334,14 @@ class CashuProcessor {
|
|||||||
token: CashuToken,
|
token: CashuToken,
|
||||||
invoice: String,
|
invoice: String,
|
||||||
fees: Int,
|
fees: Int,
|
||||||
forceProxy: (String) -> Boolean,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
onSuccess: (String, String) -> Unit,
|
onSuccess: (String, String) -> Unit,
|
||||||
onError: (String, String) -> Unit,
|
onError: (String, String) -> Unit,
|
||||||
context: Context,
|
context: Context,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
val url = token.mint + "/melt" // Melt cashu tokens at Mint
|
val url = token.mint + "/melt" // Melt cashu tokens at Mint
|
||||||
val client = HttpClientManager.getHttpClient(forceProxy(url))
|
val client = okHttpClient(url)
|
||||||
|
|
||||||
val factory = JsonNodeFactory.instance
|
val factory = JsonNodeFactory.instance
|
||||||
|
|
||||||
|
|||||||
+27
-21
@@ -18,28 +18,34 @@
|
|||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
* 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.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.components
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
import androidx.compose.foundation.text.ClickableText
|
import android.util.Log
|
||||||
import androidx.compose.material3.LocalTextStyle
|
import kotlinx.coroutines.CancellationException
|
||||||
import androidx.compose.material3.MaterialTheme
|
import kotlinx.coroutines.delay
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
|
||||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
|
||||||
|
|
||||||
@Composable
|
suspend fun <T> retryIfException(
|
||||||
fun ClickableNoteTag(
|
debugTag: String = "RetryIfException",
|
||||||
baseNote: Note,
|
maxRetries: Int = 10,
|
||||||
accountViewModel: AccountViewModel,
|
delayMs: Long = 1000,
|
||||||
nav: INav,
|
func: suspend () -> T,
|
||||||
) {
|
) {
|
||||||
ClickableText(
|
var tentative = 0
|
||||||
text = AnnotatedString("@${baseNote.idNote().toShortenHex()}"),
|
var currentDelay = delayMs
|
||||||
onClick = { routeFor(baseNote, accountViewModel.userProfile())?.let { nav.nav(it) } },
|
while (tentative < maxRetries) {
|
||||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary),
|
try {
|
||||||
)
|
func()
|
||||||
|
|
||||||
|
// if it works, finishes.
|
||||||
|
return
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
|
Log.e(debugTag, "Tentative $tentative failed", e)
|
||||||
|
|
||||||
|
delay(currentDelay)
|
||||||
|
tentative++
|
||||||
|
currentDelay = currentDelay * 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// gives up
|
||||||
}
|
}
|
||||||
+5
-6
@@ -21,17 +21,17 @@
|
|||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.BuildConfig
|
import com.vitorpamplona.amethyst.BuildConfig
|
||||||
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
|
|
||||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05
|
import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
|
||||||
class Nip05NostrAddressVerifier {
|
class Nip05NostrAddressVerifier {
|
||||||
suspend fun fetchNip05Json(
|
suspend fun fetchNip05Json(
|
||||||
nip05: String,
|
nip05: String,
|
||||||
forceProxy: (String) -> Boolean,
|
okttpClient: (String) -> OkHttpClient,
|
||||||
onSuccess: suspend (String) -> Unit,
|
onSuccess: suspend (String) -> Unit,
|
||||||
onError: (String) -> Unit,
|
onError: (String) -> Unit,
|
||||||
) = withContext(Dispatchers.IO) {
|
) = withContext(Dispatchers.IO) {
|
||||||
@@ -52,8 +52,7 @@ class Nip05NostrAddressVerifier {
|
|||||||
.url(url)
|
.url(url)
|
||||||
.build()
|
.build()
|
||||||
// Fetchers MUST ignore any HTTP redirects given by the /.well-known/nostr.json endpoint.
|
// Fetchers MUST ignore any HTTP redirects given by the /.well-known/nostr.json endpoint.
|
||||||
HttpClientManager
|
okttpClient(url)
|
||||||
.getHttpClient(forceProxy(url))
|
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.followRedirects(false)
|
.followRedirects(false)
|
||||||
.build()
|
.build()
|
||||||
@@ -78,7 +77,7 @@ class Nip05NostrAddressVerifier {
|
|||||||
|
|
||||||
suspend fun verifyNip05(
|
suspend fun verifyNip05(
|
||||||
nip05: String,
|
nip05: String,
|
||||||
forceProxy: (String) -> Boolean,
|
okttpClient: (String) -> OkHttpClient,
|
||||||
onSuccess: suspend (String) -> Unit,
|
onSuccess: suspend (String) -> Unit,
|
||||||
onError: (String) -> Unit,
|
onError: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -87,7 +86,7 @@ class Nip05NostrAddressVerifier {
|
|||||||
|
|
||||||
fetchNip05Json(
|
fetchNip05Json(
|
||||||
nip05,
|
nip05,
|
||||||
forceProxy,
|
okttpClient,
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
|
|
||||||
|
|||||||
+10
-10
@@ -20,15 +20,16 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
|
import android.content.ContentProviderOperation.newCall
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.LruCache
|
import android.util.LruCache
|
||||||
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
|
|
||||||
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
|
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
|
||||||
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
|
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
import okhttp3.Callback
|
import okhttp3.Callback
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -60,7 +61,7 @@ object Nip11CachedRetriever {
|
|||||||
|
|
||||||
suspend fun loadRelayInfo(
|
suspend fun loadRelayInfo(
|
||||||
dirtyUrl: String,
|
dirtyUrl: String,
|
||||||
forceProxy: Boolean,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
onInfo: (Nip11RelayInformation) -> Unit,
|
onInfo: (Nip11RelayInformation) -> Unit,
|
||||||
onError: (String, Nip11Retriever.ErrorCode, String?) -> Unit,
|
onError: (String, Nip11Retriever.ErrorCode, String?) -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -75,24 +76,24 @@ object Nip11CachedRetriever {
|
|||||||
if (TimeUtils.now() - doc.time < TimeUtils.ONE_MINUTE) {
|
if (TimeUtils.now() - doc.time < TimeUtils.ONE_MINUTE) {
|
||||||
// just wait.
|
// just wait.
|
||||||
} else {
|
} else {
|
||||||
retrieve(url, dirtyUrl, forceProxy, onInfo, onError)
|
retrieve(url, dirtyUrl, okHttpClient, onInfo, onError)
|
||||||
}
|
}
|
||||||
} else if (doc is RetrieveResultError) {
|
} else if (doc is RetrieveResultError) {
|
||||||
if (TimeUtils.now() - doc.time < TimeUtils.ONE_HOUR) {
|
if (TimeUtils.now() - doc.time < TimeUtils.ONE_HOUR) {
|
||||||
onError(dirtyUrl, doc.error, null)
|
onError(dirtyUrl, doc.error, null)
|
||||||
} else {
|
} else {
|
||||||
retrieve(url, dirtyUrl, forceProxy, onInfo, onError)
|
retrieve(url, dirtyUrl, okHttpClient, onInfo, onError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
retrieve(url, dirtyUrl, forceProxy, onInfo, onError)
|
retrieve(url, dirtyUrl, okHttpClient, onInfo, onError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun retrieve(
|
private suspend fun retrieve(
|
||||||
url: String,
|
url: String,
|
||||||
dirtyUrl: String,
|
dirtyUrl: String,
|
||||||
forceProxy: Boolean,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
onInfo: (Nip11RelayInformation) -> Unit,
|
onInfo: (Nip11RelayInformation) -> Unit,
|
||||||
onError: (String, Nip11Retriever.ErrorCode, String?) -> Unit,
|
onError: (String, Nip11Retriever.ErrorCode, String?) -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -100,7 +101,7 @@ object Nip11CachedRetriever {
|
|||||||
retriever.loadRelayInfo(
|
retriever.loadRelayInfo(
|
||||||
url = url,
|
url = url,
|
||||||
dirtyUrl = dirtyUrl,
|
dirtyUrl = dirtyUrl,
|
||||||
forceProxy = forceProxy,
|
okHttpClient = okHttpClient,
|
||||||
onInfo = {
|
onInfo = {
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
relayInformationDocumentCache.put(url, RetrieveResultSuccess(it))
|
relayInformationDocumentCache.put(url, RetrieveResultSuccess(it))
|
||||||
@@ -126,7 +127,7 @@ class Nip11Retriever {
|
|||||||
suspend fun loadRelayInfo(
|
suspend fun loadRelayInfo(
|
||||||
url: String,
|
url: String,
|
||||||
dirtyUrl: String,
|
dirtyUrl: String,
|
||||||
forceProxy: Boolean,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
onInfo: (Nip11RelayInformation) -> Unit,
|
onInfo: (Nip11RelayInformation) -> Unit,
|
||||||
onError: (String, ErrorCode, String?) -> Unit,
|
onError: (String, ErrorCode, String?) -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -139,8 +140,7 @@ class Nip11Retriever {
|
|||||||
.url(url)
|
.url(url)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
HttpClientManager
|
okHttpClient(url)
|
||||||
.getHttpClient(forceProxy)
|
|
||||||
.newCall(request)
|
.newCall(request)
|
||||||
.enqueue(
|
.enqueue(
|
||||||
object : Callback {
|
object : Callback {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
@@ -39,6 +40,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
|
|||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||||
|
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
|
||||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||||
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||||
@@ -311,6 +313,11 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
|
|||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
|
|
||||||
when (event) {
|
when (event) {
|
||||||
|
is OtsEvent -> {
|
||||||
|
// verifies new OTS upon arrival
|
||||||
|
Amethyst.instance.otsVerifCache.cacheVerify(event, account::otsResolver)
|
||||||
|
}
|
||||||
|
|
||||||
is PrivateOutboxRelayListEvent -> {
|
is PrivateOutboxRelayListEvent -> {
|
||||||
val note = LocalCache.getAddressableNoteIfExists(event.addressTag())
|
val note = LocalCache.getAddressableNoteIfExists(event.addressTag())
|
||||||
val noteEvent = note?.event
|
val noteEvent = note?.event
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ import android.util.Log
|
|||||||
import android.util.LruCache
|
import android.util.LruCache
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import com.vitorpamplona.amethyst.BuildConfig
|
import com.vitorpamplona.amethyst.BuildConfig
|
||||||
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
|
|
||||||
import com.vitorpamplona.quartz.utils.RandomInstance
|
import com.vitorpamplona.quartz.utils.RandomInstance
|
||||||
import okhttp3.EventListener
|
import okhttp3.EventListener
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Protocol
|
import okhttp3.Protocol
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okio.ByteString.Companion.toByteString
|
import okio.ByteString.Companion.toByteString
|
||||||
@@ -51,7 +51,7 @@ object OnlineChecker {
|
|||||||
|
|
||||||
fun isOnline(
|
fun isOnline(
|
||||||
url: String?,
|
url: String?,
|
||||||
forceProxy: Boolean,
|
okttpClient: (String) -> OkHttpClient,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
|
|
||||||
@@ -76,8 +76,7 @@ object OnlineChecker {
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
val client =
|
val client =
|
||||||
HttpClientManager
|
okttpClient(url)
|
||||||
.getHttpClient(forceProxy)
|
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.eventListener(EventListener.NONE)
|
.eventListener(EventListener.NONE)
|
||||||
.protocols(listOf(Protocol.HTTP_1_1))
|
.protocols(listOf(Protocol.HTTP_1_1))
|
||||||
@@ -96,7 +95,7 @@ object OnlineChecker {
|
|||||||
.get()
|
.get()
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
HttpClientManager.getHttpClient(forceProxy).newCall(request).execute().use {
|
okttpClient(url).newCall(request).execute().use {
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
it.isSuccessful
|
it.isSuccessful
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import kotlin.math.round
|
import kotlin.math.round
|
||||||
|
|
||||||
class ZapPaymentHandler(
|
class ZapPaymentHandler(
|
||||||
@@ -64,7 +65,7 @@ class ZapPaymentHandler(
|
|||||||
message: String,
|
message: String,
|
||||||
context: Context,
|
context: Context,
|
||||||
showErrorIfNoLnAddress: Boolean,
|
showErrorIfNoLnAddress: Boolean,
|
||||||
forceProxy: (String) -> Boolean,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
onError: (String, String, User?) -> Unit,
|
onError: (String, String, User?) -> Unit,
|
||||||
onProgress: (percent: Float) -> Unit,
|
onProgress: (percent: Float) -> Unit,
|
||||||
onPayViaIntent: (ImmutableList<Payable>) -> Unit,
|
onPayViaIntent: (ImmutableList<Payable>) -> Unit,
|
||||||
@@ -130,7 +131,7 @@ class ZapPaymentHandler(
|
|||||||
onProgress(0.05f)
|
onProgress(0.05f)
|
||||||
}
|
}
|
||||||
|
|
||||||
assembleAllInvoices(splitZapRequestPairs, amountMilliSats, message, showErrorIfNoLnAddress, forceProxy, onError, onProgress = {
|
assembleAllInvoices(splitZapRequestPairs, amountMilliSats, message, showErrorIfNoLnAddress, okHttpClient, onError, onProgress = {
|
||||||
onProgress(it * 0.7f + 0.05f) // keeps within range.
|
onProgress(it * 0.7f + 0.05f) // keeps within range.
|
||||||
}, context) { payables ->
|
}, context) { payables ->
|
||||||
if (payables.isEmpty()) {
|
if (payables.isEmpty()) {
|
||||||
@@ -228,7 +229,7 @@ class ZapPaymentHandler(
|
|||||||
totalAmountMilliSats: Long,
|
totalAmountMilliSats: Long,
|
||||||
message: String,
|
message: String,
|
||||||
showErrorIfNoLnAddress: Boolean,
|
showErrorIfNoLnAddress: Boolean,
|
||||||
forceProxy: (String) -> Boolean,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
onError: (String, String, User?) -> Unit,
|
onError: (String, String, User?) -> Unit,
|
||||||
onProgress: (percent: Float) -> Unit,
|
onProgress: (percent: Float) -> Unit,
|
||||||
context: Context,
|
context: Context,
|
||||||
@@ -247,7 +248,7 @@ class ZapPaymentHandler(
|
|||||||
zapValue = calculateZapValue(totalAmountMilliSats, splitZapRequestPair.inputSetup.weight, totalWeight),
|
zapValue = calculateZapValue(totalAmountMilliSats, splitZapRequestPair.inputSetup.weight, totalWeight),
|
||||||
message = message,
|
message = message,
|
||||||
showErrorIfNoLnAddress = showErrorIfNoLnAddress,
|
showErrorIfNoLnAddress = showErrorIfNoLnAddress,
|
||||||
forceProxy = forceProxy,
|
okHttpClient = okHttpClient,
|
||||||
onError = onError,
|
onError = onError,
|
||||||
onProgressStep = { percentStepForThisPayment ->
|
onProgressStep = { percentStepForThisPayment ->
|
||||||
progressAllPayments += percentStepForThisPayment / requests.size
|
progressAllPayments += percentStepForThisPayment / requests.size
|
||||||
@@ -319,7 +320,7 @@ class ZapPaymentHandler(
|
|||||||
zapValue: Long,
|
zapValue: Long,
|
||||||
message: String,
|
message: String,
|
||||||
showErrorIfNoLnAddress: Boolean = true,
|
showErrorIfNoLnAddress: Boolean = true,
|
||||||
forceProxy: (String) -> Boolean,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
onError: (String, String, User?) -> Unit,
|
onError: (String, String, User?) -> Unit,
|
||||||
onProgressStep: (percent: Float) -> Unit,
|
onProgressStep: (percent: Float) -> Unit,
|
||||||
context: Context,
|
context: Context,
|
||||||
@@ -341,7 +342,7 @@ class ZapPaymentHandler(
|
|||||||
milliSats = zapValue,
|
milliSats = zapValue,
|
||||||
message = message,
|
message = message,
|
||||||
nostrRequest = nostrZapRequest,
|
nostrRequest = nostrZapRequest,
|
||||||
forceProxy = forceProxy,
|
okHttpClient = okHttpClient,
|
||||||
onError = { title, msg ->
|
onError = { title, msg ->
|
||||||
onError(title, msg, toUser)
|
onError(title, msg, toUser)
|
||||||
},
|
},
|
||||||
|
|||||||
+94
@@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.service.connectivity
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.Network
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.util.Log
|
||||||
|
import com.vitorpamplona.ammolite.service.checkNotInMainThread
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
|
||||||
|
class ConnectivityFlow(
|
||||||
|
val context: Context,
|
||||||
|
) {
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
|
val status =
|
||||||
|
callbackFlow {
|
||||||
|
trySend(ConnectivityStatus.Connecting)
|
||||||
|
|
||||||
|
val connectivityManager = context.getConnectivityManager()
|
||||||
|
|
||||||
|
Log.d("ConnectivityFlow", "Starting Connectivity Flow")
|
||||||
|
val networkCallback =
|
||||||
|
object : ConnectivityManager.NetworkCallback() {
|
||||||
|
override fun onAvailable(network: Network) {
|
||||||
|
super.onAvailable(network)
|
||||||
|
checkNotInMainThread()
|
||||||
|
Log.d("ConnectivityFlow", "onAvailable ${network.networkHandle}")
|
||||||
|
connectivityManager.getNetworkCapabilities(network)?.let {
|
||||||
|
trySend(ConnectivityStatus.Active(network.networkHandle, it.isMeteredOrMobileData()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCapabilitiesChanged(
|
||||||
|
network: Network,
|
||||||
|
networkCapabilities: NetworkCapabilities,
|
||||||
|
) {
|
||||||
|
super.onCapabilitiesChanged(network, networkCapabilities)
|
||||||
|
checkNotInMainThread()
|
||||||
|
val isMobile = networkCapabilities.isMeteredOrMobileData()
|
||||||
|
Log.d("ConnectivityFlow", "onCapabilitiesChanged ${network.networkHandle} $isMobile")
|
||||||
|
trySend(ConnectivityStatus.Active(network.networkHandle, isMobile))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectivityManager.registerDefaultNetworkCallback(networkCallback)
|
||||||
|
|
||||||
|
connectivityManager.activeNetwork?.let { network ->
|
||||||
|
connectivityManager.getNetworkCapabilities(network)?.let {
|
||||||
|
trySend(ConnectivityStatus.Active(network.networkHandle, it.isMeteredOrMobileData()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
awaitClose {
|
||||||
|
checkNotInMainThread()
|
||||||
|
Log.d("ConnectivityFlow", "Stopping Connectivity Flow")
|
||||||
|
connectivityManager.unregisterNetworkCallback(networkCallback)
|
||||||
|
trySend(ConnectivityStatus.Off)
|
||||||
|
}
|
||||||
|
}.distinctUntilChanged().debounce(200).flowOn(Dispatchers.IO)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NetworkCapabilities.isMeteredOrMobileData(): Boolean {
|
||||||
|
val metered = !hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
||||||
|
val mobileData = hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||||
|
return metered || mobileData
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.getConnectivityManager() = getSystemService(ConnectivityManager::class.java) as ConnectivityManager
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.service.connectivity
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
|
||||||
|
/**
|
||||||
|
* There should be only one instance of the Tor binding per app.
|
||||||
|
*
|
||||||
|
* Tor will connect as soon as status is listened to.
|
||||||
|
*/
|
||||||
|
class ConnectivityManager(
|
||||||
|
app: Application,
|
||||||
|
scope: CoroutineScope,
|
||||||
|
) {
|
||||||
|
val status: StateFlow<ConnectivityStatus> =
|
||||||
|
ConnectivityFlow(app).status.stateIn(
|
||||||
|
scope,
|
||||||
|
SharingStarted.WhileSubscribed(30000),
|
||||||
|
ConnectivityStatus.Off,
|
||||||
|
)
|
||||||
|
|
||||||
|
val isMobileOrNull: StateFlow<Boolean?> =
|
||||||
|
status
|
||||||
|
.map {
|
||||||
|
(status.value as? ConnectivityStatus.Active)?.isMobile
|
||||||
|
}.stateIn(
|
||||||
|
scope,
|
||||||
|
SharingStarted.WhileSubscribed(2000),
|
||||||
|
(status.value as? ConnectivityStatus.Active)?.isMobile,
|
||||||
|
)
|
||||||
|
}
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.service.connectivity
|
||||||
|
|
||||||
|
sealed class ConnectivityStatus {
|
||||||
|
data class Active(
|
||||||
|
val networkId: Long,
|
||||||
|
val isMobile: Boolean,
|
||||||
|
) : ConnectivityStatus()
|
||||||
|
|
||||||
|
object Off : ConnectivityStatus()
|
||||||
|
|
||||||
|
object Connecting : ConnectivityStatus()
|
||||||
|
}
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.service.eventCache
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.vitorpamplona.amethyst.LocalPreferences
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
|
||||||
|
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
|
class MemoryTrimmingService {
|
||||||
|
var isTrimmingMemoryMutex = AtomicBoolean(false)
|
||||||
|
|
||||||
|
private suspend fun doTrim(account: Account?) {
|
||||||
|
LocalCache.cleanObservers()
|
||||||
|
|
||||||
|
val accounts = LocalPreferences.allSavedAccounts().mapNotNull { decodePublicKeyAsHexOrNull(it.npub) }.toSet()
|
||||||
|
|
||||||
|
account?.let {
|
||||||
|
LocalCache.pruneHiddenMessages(it)
|
||||||
|
LocalCache.pruneOldAndHiddenMessages(it)
|
||||||
|
NostrChatroomDataSource.clearEOSEs(it)
|
||||||
|
|
||||||
|
LocalCache.pruneContactLists(accounts)
|
||||||
|
LocalCache.pruneRepliesAndReactions(accounts)
|
||||||
|
LocalCache.prunePastVersionsOfReplaceables()
|
||||||
|
LocalCache.pruneExpiredEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun run(account: Account?) {
|
||||||
|
if (isTrimmingMemoryMutex.compareAndSet(false, true)) {
|
||||||
|
Log.d("ServiceManager", "Trimming Memory")
|
||||||
|
try {
|
||||||
|
doTrim(account)
|
||||||
|
} finally {
|
||||||
|
isTrimmingMemoryMutex.getAndSet(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-38
@@ -18,10 +18,8 @@
|
|||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
* 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.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.service.images
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import coil3.ImageLoader
|
import coil3.ImageLoader
|
||||||
import coil3.Uri
|
import coil3.Uri
|
||||||
@@ -31,38 +29,22 @@ import coil3.fetch.FetchResult
|
|||||||
import coil3.fetch.Fetcher
|
import coil3.fetch.Fetcher
|
||||||
import coil3.fetch.ImageFetchResult
|
import coil3.fetch.ImageFetchResult
|
||||||
import coil3.key.Keyer
|
import coil3.key.Keyer
|
||||||
import coil3.request.ImageRequest
|
|
||||||
import coil3.request.Options
|
import coil3.request.Options
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser.Companion.base64contentPattern
|
import com.vitorpamplona.amethyst.commons.base64Image.Base64Image
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||||
import com.vitorpamplona.quartz.utils.sha256.sha256
|
import com.vitorpamplona.quartz.utils.sha256.sha256
|
||||||
import java.util.Base64
|
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class Base64Fetcher(
|
class Base64Fetcher(
|
||||||
private val options: Options,
|
private val options: Options,
|
||||||
private val data: Uri,
|
private val data: Uri,
|
||||||
) : Fetcher {
|
) : Fetcher {
|
||||||
override suspend fun fetch(): FetchResult {
|
override suspend fun fetch(): FetchResult =
|
||||||
checkNotInMainThread()
|
ImageFetchResult(
|
||||||
|
image = Base64Image.Companion.toBitmap(data.toString()).asImage(true),
|
||||||
val matcher = base64contentPattern.matcher(data.toString())
|
isSampled = false,
|
||||||
|
dataSource = DataSource.MEMORY,
|
||||||
if (matcher.find()) {
|
)
|
||||||
val base64String = matcher.group(2)
|
|
||||||
|
|
||||||
val byteArray = Base64.getDecoder().decode(base64String)
|
|
||||||
val bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) ?: throw Exception("Unable to load base64 $base64String")
|
|
||||||
|
|
||||||
return ImageFetchResult(
|
|
||||||
image = bitmap.asImage(true),
|
|
||||||
isSampled = false,
|
|
||||||
dataSource = DataSource.MEMORY,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
throw Exception("Unable to load base64 $data")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object Factory : Fetcher.Factory<Uri> {
|
object Factory : Fetcher.Factory<Uri> {
|
||||||
override fun create(
|
override fun create(
|
||||||
@@ -89,15 +71,3 @@ class Base64Fetcher(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object Base64Requester {
|
|
||||||
fun imageRequest(
|
|
||||||
context: Context,
|
|
||||||
message: String,
|
|
||||||
): ImageRequest =
|
|
||||||
ImageRequest
|
|
||||||
.Builder(context)
|
|
||||||
.data(message)
|
|
||||||
.fetcherFactory(Base64Fetcher.Factory)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
+7
-9
@@ -18,7 +18,7 @@
|
|||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
* 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.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.service.images
|
||||||
|
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import coil3.ImageLoader
|
import coil3.ImageLoader
|
||||||
@@ -31,18 +31,16 @@ import coil3.key.Keyer
|
|||||||
import coil3.request.Options
|
import coil3.request.Options
|
||||||
import com.vitorpamplona.amethyst.commons.blurhash.BlurHashDecoder
|
import com.vitorpamplona.amethyst.commons.blurhash.BlurHashDecoder
|
||||||
|
|
||||||
class Blurhash(
|
data class BlurhashWrapper(
|
||||||
val blurhash: String,
|
val blurhash: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class BlurHashFetcher(
|
class BlurHashFetcher(
|
||||||
private val options: Options,
|
private val options: Options,
|
||||||
private val data: Blurhash,
|
private val data: BlurhashWrapper,
|
||||||
) : Fetcher {
|
) : Fetcher {
|
||||||
override suspend fun fetch(): FetchResult {
|
override suspend fun fetch(): FetchResult {
|
||||||
checkNotInMainThread()
|
|
||||||
|
|
||||||
val hash = data.blurhash
|
val hash = data.blurhash
|
||||||
|
|
||||||
val bitmap = BlurHashDecoder.decodeKeepAspectRatio(hash, 25) ?: throw Exception("Unable to convert Blurhash $data")
|
val bitmap = BlurHashDecoder.decodeKeepAspectRatio(hash, 25) ?: throw Exception("Unable to convert Blurhash $data")
|
||||||
@@ -54,17 +52,17 @@ class BlurHashFetcher(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
object Factory : Fetcher.Factory<Blurhash> {
|
object Factory : Fetcher.Factory<BlurhashWrapper> {
|
||||||
override fun create(
|
override fun create(
|
||||||
data: Blurhash,
|
data: BlurhashWrapper,
|
||||||
options: Options,
|
options: Options,
|
||||||
imageLoader: ImageLoader,
|
imageLoader: ImageLoader,
|
||||||
): Fetcher = BlurHashFetcher(options, data)
|
): Fetcher = BlurHashFetcher(options, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
object BKeyer : Keyer<Blurhash> {
|
object BKeyer : Keyer<BlurhashWrapper> {
|
||||||
override fun key(
|
override fun key(
|
||||||
data: Blurhash,
|
data: BlurhashWrapper,
|
||||||
options: Options,
|
options: Options,
|
||||||
): String = data.blurhash
|
): String = data.blurhash
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.service.images
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import coil3.disk.DiskCache
|
||||||
|
import coil3.memory.MemoryCache
|
||||||
|
import com.vitorpamplona.amethyst.service.safeCacheDir
|
||||||
|
import okio.Path.Companion.toOkioPath
|
||||||
|
|
||||||
|
class ImageCacheFactory {
|
||||||
|
companion object {
|
||||||
|
fun newDisk(app: Application): DiskCache =
|
||||||
|
DiskCache
|
||||||
|
.Builder()
|
||||||
|
.directory(app.safeCacheDir().resolve("image_cache").toOkioPath())
|
||||||
|
.maxSizePercent(0.2)
|
||||||
|
.maximumMaxSizeBytes(1024 * 1024 * 1024) // 1GB
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun newMemory(app: Application): MemoryCache =
|
||||||
|
MemoryCache
|
||||||
|
.Builder()
|
||||||
|
.maxSizePercent(app)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.service.images
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.os.Build
|
||||||
|
import coil3.ImageLoader
|
||||||
|
import coil3.SingletonImageLoader
|
||||||
|
import coil3.disk.DiskCache
|
||||||
|
import coil3.gif.AnimatedImageDecoder
|
||||||
|
import coil3.gif.GifDecoder
|
||||||
|
import coil3.memory.MemoryCache
|
||||||
|
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||||
|
import coil3.size.Precision
|
||||||
|
import coil3.svg.SvgDecoder
|
||||||
|
import coil3.util.DebugLogger
|
||||||
|
import okhttp3.Call
|
||||||
|
|
||||||
|
class ImageLoaderSetup {
|
||||||
|
companion object {
|
||||||
|
fun setup(
|
||||||
|
app: Application,
|
||||||
|
diskCache: DiskCache,
|
||||||
|
memoryCache: MemoryCache,
|
||||||
|
isDebug: Boolean,
|
||||||
|
callFactory: () -> Call.Factory,
|
||||||
|
) {
|
||||||
|
SingletonImageLoader.setUnsafe(
|
||||||
|
ImageLoader
|
||||||
|
.Builder(app)
|
||||||
|
.diskCache { diskCache }
|
||||||
|
.memoryCache { memoryCache }
|
||||||
|
.precision(Precision.INEXACT)
|
||||||
|
.logger(if (isDebug) DebugLogger() else null)
|
||||||
|
.components {
|
||||||
|
if (Build.VERSION.SDK_INT >= 28) {
|
||||||
|
add(AnimatedImageDecoder.Factory())
|
||||||
|
} else {
|
||||||
|
add(GifDecoder.Factory())
|
||||||
|
}
|
||||||
|
add(SvgDecoder.Factory())
|
||||||
|
add(Base64Fetcher.Factory)
|
||||||
|
add(BlurHashFetcher.Factory)
|
||||||
|
add(Base64Fetcher.BKeyer)
|
||||||
|
add(BlurHashFetcher.BKeyer)
|
||||||
|
add(OkHttpNetworkFetcherFactory(callFactory))
|
||||||
|
}.build(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-8
@@ -27,10 +27,10 @@ import com.vitorpamplona.amethyst.BuildConfig
|
|||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.service.HttpStatusMessages
|
import com.vitorpamplona.amethyst.service.HttpStatusMessages
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
|
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.quartz.lightning.LnInvoiceUtil
|
import com.vitorpamplona.quartz.lightning.LnInvoiceUtil
|
||||||
import com.vitorpamplona.quartz.lightning.Lud06
|
import com.vitorpamplona.quartz.lightning.Lud06
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
@@ -55,7 +55,7 @@ class LightningAddressResolver {
|
|||||||
|
|
||||||
private fun fetchLightningAddressJson(
|
private fun fetchLightningAddressJson(
|
||||||
lnaddress: String,
|
lnaddress: String,
|
||||||
forceProxy: (url: String) -> Boolean,
|
okttpClient: (String) -> OkHttpClient,
|
||||||
onSuccess: (String) -> Unit,
|
onSuccess: (String) -> Unit,
|
||||||
onError: (String, String) -> Unit,
|
onError: (String, String) -> Unit,
|
||||||
context: Context,
|
context: Context,
|
||||||
@@ -76,7 +76,7 @@ class LightningAddressResolver {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val client = HttpClientManager.getHttpClient(forceProxy(url))
|
val client = okttpClient(url)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val request: Request =
|
val request: Request =
|
||||||
@@ -125,7 +125,7 @@ class LightningAddressResolver {
|
|||||||
milliSats: Long,
|
milliSats: Long,
|
||||||
message: String,
|
message: String,
|
||||||
nostrRequest: String? = null,
|
nostrRequest: String? = null,
|
||||||
forceProxy: (url: String) -> Boolean,
|
okttpClient: (String) -> OkHttpClient,
|
||||||
onSuccess: (String) -> Unit,
|
onSuccess: (String) -> Unit,
|
||||||
onError: (String, String) -> Unit,
|
onError: (String, String) -> Unit,
|
||||||
context: Context,
|
context: Context,
|
||||||
@@ -142,7 +142,7 @@ class LightningAddressResolver {
|
|||||||
url += "&nostr=$encodedNostrRequest"
|
url += "&nostr=$encodedNostrRequest"
|
||||||
}
|
}
|
||||||
|
|
||||||
val client = HttpClientManager.getHttpClient(forceProxy(url))
|
val client = okttpClient(url)
|
||||||
|
|
||||||
val request: Request =
|
val request: Request =
|
||||||
Request
|
Request
|
||||||
@@ -208,7 +208,7 @@ class LightningAddressResolver {
|
|||||||
milliSats: Long,
|
milliSats: Long,
|
||||||
message: String,
|
message: String,
|
||||||
nostrRequest: String? = null,
|
nostrRequest: String? = null,
|
||||||
forceProxy: (url: String) -> Boolean,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
onSuccess: (String) -> Unit,
|
onSuccess: (String) -> Unit,
|
||||||
onError: (String, String) -> Unit,
|
onError: (String, String) -> Unit,
|
||||||
onProgress: (percent: Float) -> Unit,
|
onProgress: (percent: Float) -> Unit,
|
||||||
@@ -218,7 +218,7 @@ class LightningAddressResolver {
|
|||||||
|
|
||||||
fetchLightningAddressJson(
|
fetchLightningAddressJson(
|
||||||
lnaddress,
|
lnaddress,
|
||||||
forceProxy,
|
okHttpClient,
|
||||||
onSuccess = { lnAddressJson ->
|
onSuccess = { lnAddressJson ->
|
||||||
onProgress(0.4f)
|
onProgress(0.4f)
|
||||||
|
|
||||||
@@ -259,7 +259,7 @@ class LightningAddressResolver {
|
|||||||
milliSats,
|
milliSats,
|
||||||
message,
|
message,
|
||||||
if (allowsNostr) nostrRequest else null,
|
if (allowsNostr) nostrRequest else null,
|
||||||
forceProxy,
|
okHttpClient,
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
onProgress(0.6f)
|
onProgress(0.6f)
|
||||||
|
|
||||||
|
|||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.service.logging
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.Choreographer
|
||||||
|
|
||||||
|
object ChoreographerHelper {
|
||||||
|
var lastFrameTimeNanos: Long = 0
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
Choreographer.getInstance().postFrameCallback(
|
||||||
|
object : Choreographer.FrameCallback {
|
||||||
|
override fun doFrame(frameTimeNanos: Long) {
|
||||||
|
// Last callback time
|
||||||
|
if (lastFrameTimeNanos == 0L) {
|
||||||
|
lastFrameTimeNanos = frameTimeNanos
|
||||||
|
Choreographer.getInstance().postFrameCallback(this)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val diff = (frameTimeNanos - lastFrameTimeNanos) / 1000000
|
||||||
|
// only report after 30ms because videos play at 30fps
|
||||||
|
if (diff > 35) {
|
||||||
|
// Follow the frame number
|
||||||
|
val droppedCount = (diff / 16.6).toInt()
|
||||||
|
Log.w("block-canary", "Dropped $droppedCount frames. Skipped $diff ms")
|
||||||
|
}
|
||||||
|
lastFrameTimeNanos = frameTimeNanos
|
||||||
|
Choreographer.getInstance().postFrameCallback(this)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-30
@@ -18,14 +18,13 @@
|
|||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
* 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.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst
|
package com.vitorpamplona.amethyst.service.logging
|
||||||
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.HandlerThread
|
import android.os.HandlerThread
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.Printer
|
import android.util.Printer
|
||||||
import android.view.Choreographer
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
@@ -168,31 +167,3 @@ class StackSampler(
|
|||||||
val TIME_FORMATTER: SimpleDateFormat = SimpleDateFormat("MM-dd HH:mm:ss.SSS")
|
val TIME_FORMATTER: SimpleDateFormat = SimpleDateFormat("MM-dd HH:mm:ss.SSS")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object ChoreographerHelper {
|
|
||||||
var lastFrameTimeNanos: Long = 0
|
|
||||||
|
|
||||||
fun start() {
|
|
||||||
Choreographer.getInstance().postFrameCallback(
|
|
||||||
object : Choreographer.FrameCallback {
|
|
||||||
override fun doFrame(frameTimeNanos: Long) {
|
|
||||||
// Last callback time
|
|
||||||
if (lastFrameTimeNanos == 0L) {
|
|
||||||
lastFrameTimeNanos = frameTimeNanos
|
|
||||||
Choreographer.getInstance().postFrameCallback(this)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val diff = (frameTimeNanos - lastFrameTimeNanos) / 1000000
|
|
||||||
// only report after 30ms because videos play at 30fps
|
|
||||||
if (diff > 35) {
|
|
||||||
// Follow the frame number
|
|
||||||
val droppedCount = (diff / 16.6).toInt()
|
|
||||||
Log.w("block-canary", "Dropped $droppedCount frames. Skipped $diff ms")
|
|
||||||
}
|
|
||||||
lastFrameTimeNanos = frameTimeNanos
|
|
||||||
Choreographer.getInstance().postFrameCallback(this)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.service.logging
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Looper
|
||||||
|
import android.os.StrictMode
|
||||||
|
import android.os.StrictMode.ThreadPolicy
|
||||||
|
import android.os.StrictMode.VmPolicy
|
||||||
|
|
||||||
|
class Logging {
|
||||||
|
companion object {
|
||||||
|
fun setup() {
|
||||||
|
StrictMode.setThreadPolicy(
|
||||||
|
ThreadPolicy
|
||||||
|
.Builder()
|
||||||
|
.detectAll()
|
||||||
|
.penaltyLog()
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
StrictMode.setVmPolicy(
|
||||||
|
VmPolicy
|
||||||
|
.Builder()
|
||||||
|
.detectLeakedSqlLiteObjects()
|
||||||
|
.detectActivityLeaks()
|
||||||
|
.detectLeakedClosableObjects()
|
||||||
|
.detectLeakedRegistrationObjects()
|
||||||
|
.detectFileUriExposure()
|
||||||
|
.detectCleartextNetwork()
|
||||||
|
.detectContentUriWithoutPermission()
|
||||||
|
.apply {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
detectCredentialProtectedWhileLocked()
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
detectIncorrectContextUse()
|
||||||
|
detectUnsafeIntentLaunch()
|
||||||
|
}
|
||||||
|
}.penaltyLog()
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
Looper.getMainLooper().setMessageLogging(LogMonitor())
|
||||||
|
ChoreographerHelper.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
-8
@@ -20,23 +20,39 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.service.notifications
|
package com.vitorpamplona.amethyst.service.notifications
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Context.RECEIVER_EXPORTED
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.LiveFolders.INTENT
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.core.content.ContextCompat.registerReceiver
|
||||||
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class PokeyReceiver : BroadcastReceiver() {
|
class PokeyReceiver : BroadcastReceiver() {
|
||||||
companion object {
|
companion object {
|
||||||
val POKEY_ACTION = "com.shared.NOSTR"
|
const val POKEY_ACTION = "com.shared.NOSTR"
|
||||||
val TAG = "PokeyReceiver"
|
const val TAG = "PokeyReceiver"
|
||||||
|
val INTENT = IntentFilter(POKEY_ACTION)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
fun register(app: Application) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
app.registerReceiver(this, INTENT, RECEIVER_EXPORTED)
|
||||||
|
} else {
|
||||||
|
@Suppress("UnspecifiedRegisterReceiverFlag")
|
||||||
|
app.registerReceiver(this, INTENT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unregister(app: Application) {
|
||||||
|
app.unregisterReceiver(this)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onReceive(
|
override fun onReceive(
|
||||||
context: Context,
|
context: Context,
|
||||||
@@ -48,9 +64,11 @@ class PokeyReceiver : BroadcastReceiver() {
|
|||||||
|
|
||||||
if (eventStr == null) return
|
if (eventStr == null) return
|
||||||
|
|
||||||
scope.launch(Dispatchers.IO) {
|
val app = context.applicationContext as Amethyst
|
||||||
|
|
||||||
|
app.applicationIOScope.launch {
|
||||||
try {
|
try {
|
||||||
EventNotificationConsumer(context.applicationContext).findAccountAndConsume(
|
EventNotificationConsumer(app).findAccountAndConsume(
|
||||||
Event.fromJson(eventStr),
|
Event.fromJson(eventStr),
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
+21
-28
@@ -27,20 +27,20 @@ import com.vitorpamplona.amethyst.BuildConfig
|
|||||||
import com.vitorpamplona.amethyst.LocalPreferences
|
import com.vitorpamplona.amethyst.LocalPreferences
|
||||||
import com.vitorpamplona.amethyst.launchAndWaitAll
|
import com.vitorpamplona.amethyst.launchAndWaitAll
|
||||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||||
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
|
|
||||||
import com.vitorpamplona.amethyst.tryAndWait
|
import com.vitorpamplona.amethyst.tryAndWait
|
||||||
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
||||||
import com.vitorpamplona.quartz.nip55AndroidSigner.NostrSignerExternal
|
import com.vitorpamplona.quartz.nip55AndroidSigner.NostrSignerExternal
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
class RegisterAccounts(
|
class RegisterAccounts(
|
||||||
private val accounts: List<AccountInfo>,
|
private val accounts: List<AccountInfo>,
|
||||||
|
private val client: (String) -> OkHttpClient,
|
||||||
) {
|
) {
|
||||||
val tag =
|
val tag =
|
||||||
if (BuildConfig.FLAVOR == "play") {
|
if (BuildConfig.FLAVOR == "play") {
|
||||||
@@ -133,33 +133,26 @@ class RegisterAccounts(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun postRegistrationEvent(events: List<RelayAuthEvent>) {
|
fun postRegistrationEvent(events: List<RelayAuthEvent>) {
|
||||||
try {
|
val jsonObject =
|
||||||
val jsonObject =
|
"""{
|
||||||
"""{
|
"events": [ ${events.joinToString(", ") { it.toJson() }} ]
|
||||||
"events": [ ${events.joinToString(", ") { it.toJson() }} ]
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
val mediaType = "application/json; charset=utf-8".toMediaType()
|
|
||||||
val body = jsonObject.toRequestBody(mediaType)
|
|
||||||
|
|
||||||
val request =
|
|
||||||
Request
|
|
||||||
.Builder()
|
|
||||||
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
|
||||||
.url("https://push.amethyst.social/register")
|
|
||||||
.post(body)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
// Always try via Tor for Amethyst.
|
|
||||||
val client = HttpClientManager.getHttpClient(true)
|
|
||||||
|
|
||||||
val isSucess = client.newCall(request).execute().use { it.isSuccessful }
|
|
||||||
Log.i(tag, "Server registration $isSucess")
|
|
||||||
} catch (e: java.lang.Exception) {
|
|
||||||
if (e is CancellationException) throw e
|
|
||||||
Log.e(tag, "Unable to register with push server", e)
|
|
||||||
}
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
val url = "https://push.amethyst.social/register"
|
||||||
|
val mediaType = "application/json; charset=utf-8".toMediaType()
|
||||||
|
val body = jsonObject.toRequestBody(mediaType)
|
||||||
|
|
||||||
|
val request =
|
||||||
|
Request
|
||||||
|
.Builder()
|
||||||
|
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||||
|
.url(url)
|
||||||
|
.post(body)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val isSucess = client(url).newCall(request).execute().use { it.isSuccessful }
|
||||||
|
Log.i(tag, "Server registration $isSucess")
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun go(notificationToken: String) {
|
suspend fun go(notificationToken: String) {
|
||||||
|
|||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.service.okhttp
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.Proxy
|
||||||
|
|
||||||
|
class DualHttpClientManager(
|
||||||
|
userAgent: String,
|
||||||
|
proxyPortProvider: StateFlow<Int?>,
|
||||||
|
isMobileDataProvider: StateFlow<Boolean?>,
|
||||||
|
keyCache: EncryptionKeyCache,
|
||||||
|
scope: CoroutineScope,
|
||||||
|
) {
|
||||||
|
val factory = OkHttpClientFactory(keyCache)
|
||||||
|
|
||||||
|
private val defaultHttpClient: StateFlow<OkHttpClient> =
|
||||||
|
combine(proxyPortProvider, isMobileDataProvider) { proxy, mobile ->
|
||||||
|
factory.buildHttpClient(proxy, mobile, userAgent)
|
||||||
|
}.stateIn(
|
||||||
|
scope,
|
||||||
|
SharingStarted.Lazily,
|
||||||
|
factory.buildHttpClient(proxyPortProvider.value, isMobileDataProvider.value, userAgent),
|
||||||
|
)
|
||||||
|
|
||||||
|
private val defaultHttpClientWithoutProxy: StateFlow<OkHttpClient> =
|
||||||
|
isMobileDataProvider
|
||||||
|
.map { mobile ->
|
||||||
|
factory.buildHttpClient(mobile, userAgent)
|
||||||
|
}.stateIn(
|
||||||
|
scope,
|
||||||
|
SharingStarted.Lazily,
|
||||||
|
factory.buildHttpClient(isMobileDataProvider.value, userAgent),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getCurrentProxy(): Proxy? = defaultHttpClient.value.proxy
|
||||||
|
|
||||||
|
fun getCurrentProxyPort(useProxy: Boolean): Int? =
|
||||||
|
if (useProxy) {
|
||||||
|
(getCurrentProxy()?.address() as? InetSocketAddress)?.port
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getHttpClient(useProxy: Boolean): OkHttpClient =
|
||||||
|
if (useProxy) {
|
||||||
|
defaultHttpClient.value
|
||||||
|
} else {
|
||||||
|
defaultHttpClientWithoutProxy.value
|
||||||
|
}
|
||||||
|
}
|
||||||
-137
@@ -1,137 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2024 Vitor Pamplona
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to use,
|
|
||||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
|
||||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
* subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
|
||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
||||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
package com.vitorpamplona.amethyst.service.okhttp
|
|
||||||
|
|
||||||
import android.R.attr.port
|
|
||||||
import android.util.Log
|
|
||||||
import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import java.net.InetSocketAddress
|
|
||||||
import java.net.Proxy
|
|
||||||
import java.time.Duration
|
|
||||||
|
|
||||||
object HttpClientManager {
|
|
||||||
private val rootClient =
|
|
||||||
OkHttpClient
|
|
||||||
.Builder()
|
|
||||||
.followRedirects(true)
|
|
||||||
.followSslRedirects(true)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val DEFAULT_TOR_PROXY = Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", 9050))
|
|
||||||
|
|
||||||
val DEFAULT_TIMEOUT_ON_WIFI: Duration = Duration.ofSeconds(10L)
|
|
||||||
val DEFAULT_TIMEOUT_ON_MOBILE: Duration = Duration.ofSeconds(30L)
|
|
||||||
|
|
||||||
private var defaultTimeout = DEFAULT_TIMEOUT_ON_WIFI
|
|
||||||
private var defaultHttpClient: OkHttpClient? = null
|
|
||||||
private var defaultHttpClientWithoutProxy: OkHttpClient? = null
|
|
||||||
private var userAgent: String = "Amethyst"
|
|
||||||
|
|
||||||
private var currentProxy: Proxy? = DEFAULT_TOR_PROXY
|
|
||||||
|
|
||||||
private val cache = EncryptionKeyCache()
|
|
||||||
|
|
||||||
fun setDefaultProxy(proxy: Proxy?) {
|
|
||||||
if (currentProxy != proxy) {
|
|
||||||
Log.d("HttpClient", "Changing proxy to: $proxy")
|
|
||||||
currentProxy = proxy
|
|
||||||
|
|
||||||
// recreates singleton
|
|
||||||
defaultHttpClient = buildHttpClient(currentProxy, defaultTimeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCurrentProxy(): Proxy? = currentProxy
|
|
||||||
|
|
||||||
fun setDefaultTimeout(timeout: Duration) {
|
|
||||||
Log.d("HttpClient", "Changing timeout to: $timeout")
|
|
||||||
if (defaultTimeout.seconds != timeout.seconds) {
|
|
||||||
defaultTimeout = timeout
|
|
||||||
|
|
||||||
// recreates singleton
|
|
||||||
defaultHttpClient = buildHttpClient(currentProxy, defaultTimeout)
|
|
||||||
defaultHttpClientWithoutProxy = buildHttpClient(null, defaultTimeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setDefaultUserAgent(userAgentHeader: String) {
|
|
||||||
Log.d("HttpClient", "Changing userAgent")
|
|
||||||
if (userAgent != userAgentHeader) {
|
|
||||||
userAgent = userAgentHeader
|
|
||||||
defaultHttpClient = buildHttpClient(currentProxy, defaultTimeout)
|
|
||||||
defaultHttpClientWithoutProxy = buildHttpClient(null, defaultTimeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildHttpClient(
|
|
||||||
proxy: Proxy?,
|
|
||||||
timeout: Duration,
|
|
||||||
): OkHttpClient {
|
|
||||||
val seconds = if (proxy != null) timeout.seconds * 3 else timeout.seconds
|
|
||||||
val duration = Duration.ofSeconds(seconds)
|
|
||||||
return rootClient
|
|
||||||
.newBuilder()
|
|
||||||
.proxy(proxy)
|
|
||||||
.readTimeout(duration)
|
|
||||||
.connectTimeout(duration)
|
|
||||||
.writeTimeout(duration)
|
|
||||||
.addInterceptor(DefaultContentTypeInterceptor(userAgent))
|
|
||||||
.addNetworkInterceptor(LoggingInterceptor())
|
|
||||||
.addNetworkInterceptor(EncryptedBlobInterceptor(cache))
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCurrentProxyPort(useProxy: Boolean): Int? =
|
|
||||||
if (useProxy) {
|
|
||||||
(currentProxy?.address() as? InetSocketAddress)?.port
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getHttpClient(useProxy: Boolean): OkHttpClient =
|
|
||||||
if (useProxy) {
|
|
||||||
if (defaultHttpClient == null) {
|
|
||||||
defaultHttpClient = buildHttpClient(currentProxy, defaultTimeout)
|
|
||||||
}
|
|
||||||
defaultHttpClient!!
|
|
||||||
} else {
|
|
||||||
if (defaultHttpClientWithoutProxy == null) {
|
|
||||||
defaultHttpClientWithoutProxy = buildHttpClient(null, defaultTimeout)
|
|
||||||
}
|
|
||||||
defaultHttpClientWithoutProxy!!
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setProxyNotReady() {
|
|
||||||
// this blocks all connections unless Orbot is live.
|
|
||||||
setDefaultProxy(DEFAULT_TOR_PROXY)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setDefaultProxyOnPort(port: Int) {
|
|
||||||
setDefaultProxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", port)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addCipherToCache(
|
|
||||||
url: String,
|
|
||||||
cipher: NostrCipher,
|
|
||||||
expectedMimeType: String?,
|
|
||||||
) = cache.add(url, cipher, expectedMimeType)
|
|
||||||
}
|
|
||||||
+94
@@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.service.okhttp
|
||||||
|
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.Proxy
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
class OkHttpClientFactory(
|
||||||
|
val keyCache: EncryptionKeyCache,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
// by picking a random proxy port, the connection will fail as it shouold.
|
||||||
|
const val DEFAULT_SOCKS_PORT: Int = 9050
|
||||||
|
const val DEFAULT_IS_MOBILE: Boolean = false
|
||||||
|
const val DEFAULT_TIMEOUT_ON_WIFI_SECS: Int = 10
|
||||||
|
const val DEFAULT_TIMEOUT_ON_MOBILE_SECS: Int = 30
|
||||||
|
}
|
||||||
|
|
||||||
|
private val rootClient =
|
||||||
|
OkHttpClient
|
||||||
|
.Builder()
|
||||||
|
.followRedirects(true)
|
||||||
|
.followSslRedirects(true)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun buildHttpClient(
|
||||||
|
proxy: Proxy?,
|
||||||
|
timeoutSeconds: Int,
|
||||||
|
userAgent: String,
|
||||||
|
): OkHttpClient {
|
||||||
|
val seconds = if (proxy != null) timeoutSeconds * 3 else timeoutSeconds
|
||||||
|
val duration = Duration.ofSeconds(seconds.toLong())
|
||||||
|
return rootClient
|
||||||
|
.newBuilder()
|
||||||
|
.proxy(proxy)
|
||||||
|
.readTimeout(duration)
|
||||||
|
.connectTimeout(duration)
|
||||||
|
.writeTimeout(duration)
|
||||||
|
.addInterceptor(DefaultContentTypeInterceptor(userAgent))
|
||||||
|
.addNetworkInterceptor(LoggingInterceptor())
|
||||||
|
.addNetworkInterceptor(EncryptedBlobInterceptor(keyCache))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildHttpClient(
|
||||||
|
localSocksProxyPort: Int?,
|
||||||
|
isMobile: Boolean?,
|
||||||
|
userAgent: String,
|
||||||
|
): OkHttpClient =
|
||||||
|
buildHttpClient(
|
||||||
|
buildLocalSocksProxy(localSocksProxyPort),
|
||||||
|
buildTimeout(isMobile ?: DEFAULT_IS_MOBILE),
|
||||||
|
userAgent,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun buildHttpClient(
|
||||||
|
isMobile: Boolean?,
|
||||||
|
userAgent: String,
|
||||||
|
): OkHttpClient =
|
||||||
|
buildHttpClient(
|
||||||
|
null,
|
||||||
|
buildTimeout(isMobile ?: DEFAULT_IS_MOBILE),
|
||||||
|
userAgent,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun buildTimeout(isMobile: Boolean): Int =
|
||||||
|
if (isMobile) {
|
||||||
|
DEFAULT_TIMEOUT_ON_MOBILE_SECS
|
||||||
|
} else {
|
||||||
|
DEFAULT_TIMEOUT_ON_WIFI_SECS
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildLocalSocksProxy(port: Int?) = Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", port ?: DEFAULT_SOCKS_PORT))
|
||||||
|
}
|
||||||
+13
-4
@@ -24,12 +24,14 @@ import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocket
|
|||||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocketListener
|
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocketListener
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder
|
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilderFactory
|
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilderFactory
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
|
||||||
class OkHttpWebSocket(
|
class OkHttpWebSocket(
|
||||||
val url: String,
|
val url: String,
|
||||||
val forceProxy: Boolean,
|
val forceProxy: Boolean,
|
||||||
|
val httpClient: (url: String, forceProxy: Boolean) -> OkHttpClient,
|
||||||
val out: WebSocketListener,
|
val out: WebSocketListener,
|
||||||
) : WebSocket {
|
) : WebSocket {
|
||||||
private val listener = OkHttpWebsocketListener()
|
private val listener = OkHttpWebsocketListener()
|
||||||
@@ -38,7 +40,7 @@ class OkHttpWebSocket(
|
|||||||
fun buildRequest() = Request.Builder().url(url.trim()).build()
|
fun buildRequest() = Request.Builder().url(url.trim()).build()
|
||||||
|
|
||||||
override fun connect() {
|
override fun connect() {
|
||||||
socket = HttpClientManager.getHttpClient(forceProxy).newWebSocket(buildRequest(), listener)
|
socket = httpClient(url, forceProxy).newWebSocket(buildRequest(), listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class OkHttpWebsocketListener : okhttp3.WebSocketListener() {
|
inner class OkHttpWebsocketListener : okhttp3.WebSocketListener() {
|
||||||
@@ -76,15 +78,22 @@ class OkHttpWebSocket(
|
|||||||
|
|
||||||
class Builder(
|
class Builder(
|
||||||
val forceProxy: Boolean,
|
val forceProxy: Boolean,
|
||||||
|
val httpClient: (String, Boolean) -> OkHttpClient,
|
||||||
) : WebsocketBuilder {
|
) : WebsocketBuilder {
|
||||||
|
// Called when connecting.
|
||||||
override fun build(
|
override fun build(
|
||||||
url: String,
|
url: String,
|
||||||
out: WebSocketListener,
|
out: WebSocketListener,
|
||||||
) = OkHttpWebSocket(url, forceProxy, out)
|
) = OkHttpWebSocket(url, forceProxy, httpClient, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
class BuilderFactory : WebsocketBuilderFactory {
|
class BuilderFactory(
|
||||||
override fun build(forceProxy: Boolean) = Builder(forceProxy)
|
val httpClient: (String, Boolean) -> OkHttpClient,
|
||||||
|
) : WebsocketBuilderFactory {
|
||||||
|
override fun build(
|
||||||
|
url: String,
|
||||||
|
forceProxy: Boolean,
|
||||||
|
) = Builder(forceProxy, httpClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cancel() {
|
override fun cancel() {
|
||||||
|
|||||||
+19
-33
@@ -21,21 +21,19 @@
|
|||||||
package com.vitorpamplona.amethyst.service.ots
|
package com.vitorpamplona.amethyst.service.ots
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.LruCache
|
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.vitorpamplona.amethyst.BuildConfig
|
import com.vitorpamplona.amethyst.BuildConfig
|
||||||
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
|
|
||||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.BitcoinExplorer
|
import com.vitorpamplona.quartz.nip03Timestamp.ots.BitcoinExplorer
|
||||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.BlockHeader
|
import com.vitorpamplona.quartz.nip03Timestamp.ots.BlockHeader
|
||||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.exceptions.UrlException
|
import com.vitorpamplona.quartz.nip03Timestamp.ots.exceptions.UrlException
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
|
||||||
class OkHttpBlockstreamExplorer(
|
class OkHttpBitcoinExplorer(
|
||||||
val forceProxy: (String) -> Boolean,
|
val baseAPI: String,
|
||||||
|
val client: OkHttpClient,
|
||||||
|
val cache: OtsBlockHeightCache,
|
||||||
) : BitcoinExplorer {
|
) : BitcoinExplorer {
|
||||||
private val cacheHeaders = LruCache<String, BlockHeader>(100)
|
|
||||||
private val cacheHeights = LruCache<Int, String>(100)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the block information from the block hash.
|
* Retrieve the block information from the block hash.
|
||||||
*
|
*
|
||||||
@@ -44,13 +42,11 @@ class OkHttpBlockstreamExplorer(
|
|||||||
* @throws Exception desc
|
* @throws Exception desc
|
||||||
*/
|
*/
|
||||||
override fun block(hash: String): BlockHeader {
|
override fun block(hash: String): BlockHeader {
|
||||||
cacheHeaders.get(hash)?.let {
|
cache.cacheHeaders.get(hash)?.let {
|
||||||
return it
|
return it
|
||||||
}
|
}
|
||||||
|
|
||||||
val usingTor = forceProxy(BLOCKSTREAM_API_URL)
|
val url = "$baseAPI/block/$hash"
|
||||||
val url = "${getAPI(usingTor)}/block/$hash"
|
|
||||||
val client = HttpClientManager.getHttpClient(forceProxy(url))
|
|
||||||
|
|
||||||
val request =
|
val request =
|
||||||
Request
|
Request
|
||||||
@@ -63,17 +59,15 @@ class OkHttpBlockstreamExplorer(
|
|||||||
|
|
||||||
client.newCall(request).execute().use {
|
client.newCall(request).execute().use {
|
||||||
if (it.isSuccessful) {
|
if (it.isSuccessful) {
|
||||||
|
Log.d("OkHttpBlockstreamExplorer", "$baseAPI/block/$hash")
|
||||||
|
|
||||||
val jsonObject = jacksonObjectMapper().readTree(it.body.string())
|
val jsonObject = jacksonObjectMapper().readTree(it.body.string())
|
||||||
|
|
||||||
val blockHeader =
|
val blockHeader = BlockHeader()
|
||||||
BlockHeader()
|
|
||||||
blockHeader.merkleroot = jsonObject["merkle_root"].asText()
|
blockHeader.merkleroot = jsonObject["merkle_root"].asText()
|
||||||
blockHeader.setTime(jsonObject["timestamp"].asInt().toString())
|
blockHeader.setTime(jsonObject["timestamp"].asInt().toString())
|
||||||
blockHeader.blockHash = hash
|
blockHeader.blockHash = hash
|
||||||
Log.d("OkHttpBlockstreamExplorer", "$BLOCKSTREAM_API_URL/block/$hash")
|
cache.cacheHeaders.put(hash, blockHeader)
|
||||||
|
|
||||||
cacheHeaders.put(hash, blockHeader)
|
|
||||||
|
|
||||||
return blockHeader
|
return blockHeader
|
||||||
} else {
|
} else {
|
||||||
throw UrlException(
|
throw UrlException(
|
||||||
@@ -92,13 +86,11 @@ class OkHttpBlockstreamExplorer(
|
|||||||
*/
|
*/
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun blockHash(height: Int): String {
|
override fun blockHash(height: Int): String {
|
||||||
cacheHeights[height]?.let {
|
cache.cacheHeights[height]?.let {
|
||||||
return it
|
return it
|
||||||
}
|
}
|
||||||
|
|
||||||
val usingTor = forceProxy(BLOCKSTREAM_API_URL)
|
val url = "$baseAPI/block-height/$height"
|
||||||
val url = "${getAPI(usingTor)}/block-height/$height"
|
|
||||||
val client = HttpClientManager.getHttpClient(usingTor)
|
|
||||||
|
|
||||||
val request =
|
val request =
|
||||||
Request
|
Request
|
||||||
@@ -114,25 +106,19 @@ class OkHttpBlockstreamExplorer(
|
|||||||
|
|
||||||
Log.d("OkHttpBlockstreamExplorer", "$url $blockHash")
|
Log.d("OkHttpBlockstreamExplorer", "$url $blockHash")
|
||||||
|
|
||||||
cacheHeights.put(height, blockHash)
|
cache.cacheHeights.put(height, blockHash)
|
||||||
return blockHash
|
return blockHash
|
||||||
} else {
|
} else {
|
||||||
throw UrlException(
|
throw UrlException("Couldn't open $url: " + it.message + " " + it.code)
|
||||||
"Couldn't open $url: " + it.message + " " + it.code,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val BLOCKSTREAM_API_URL = "https://blockstream.info/api"
|
// doesn't accept Tor
|
||||||
private const val MEMPOOL_API_URL = "https://mempool.space/api/"
|
const val BLOCKSTREAM_API_URL = "https://blockstream.info/api"
|
||||||
|
|
||||||
fun getAPI(usingTor: Boolean) =
|
// accepts Tor
|
||||||
if (usingTor) {
|
const val MEMPOOL_API_URL = "https://mempool.space/api/"
|
||||||
MEMPOOL_API_URL
|
|
||||||
} else {
|
|
||||||
BLOCKSTREAM_API_URL
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,6 @@
|
|||||||
package com.vitorpamplona.amethyst.service.ots
|
package com.vitorpamplona.amethyst.service.ots
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.BuildConfig
|
import com.vitorpamplona.amethyst.BuildConfig
|
||||||
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
|
|
||||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.ICalendar
|
import com.vitorpamplona.quartz.nip03Timestamp.ots.ICalendar
|
||||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.StreamDeserializationContext
|
import com.vitorpamplona.quartz.nip03Timestamp.ots.StreamDeserializationContext
|
||||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.Timestamp
|
import com.vitorpamplona.quartz.nip03Timestamp.ots.Timestamp
|
||||||
@@ -31,6 +30,7 @@ import com.vitorpamplona.quartz.nip03Timestamp.ots.exceptions.ExceededSizeExcept
|
|||||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.exceptions.UrlException
|
import com.vitorpamplona.quartz.nip03Timestamp.ots.exceptions.UrlException
|
||||||
import com.vitorpamplona.quartz.utils.Hex
|
import com.vitorpamplona.quartz.utils.Hex
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,7 +38,7 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
|||||||
*/
|
*/
|
||||||
class OkHttpCalendar(
|
class OkHttpCalendar(
|
||||||
val url: String,
|
val url: String,
|
||||||
val forceProxy: Boolean,
|
val client: OkHttpClient,
|
||||||
) : ICalendar {
|
) : ICalendar {
|
||||||
/**
|
/**
|
||||||
* Submitting a digest to remote calendar. Returns a com.eternitywall.ots.Timestamp committing to that digest.
|
* Submitting a digest to remote calendar. Returns a com.eternitywall.ots.Timestamp committing to that digest.
|
||||||
@@ -52,7 +52,6 @@ class OkHttpCalendar(
|
|||||||
@Throws(ExceededSizeException::class, UrlException::class, DeserializationException::class)
|
@Throws(ExceededSizeException::class, UrlException::class, DeserializationException::class)
|
||||||
override fun submit(digest: ByteArray): Timestamp {
|
override fun submit(digest: ByteArray): Timestamp {
|
||||||
try {
|
try {
|
||||||
val client = HttpClientManager.getHttpClient(forceProxy)
|
|
||||||
val url = "$url/digest"
|
val url = "$url/digest"
|
||||||
|
|
||||||
val mediaType = "application/x-www-form-urlencoded; charset=utf-8".toMediaType()
|
val mediaType = "application/x-www-form-urlencoded; charset=utf-8".toMediaType()
|
||||||
@@ -108,7 +107,6 @@ class OkHttpCalendar(
|
|||||||
)
|
)
|
||||||
override fun getTimestamp(commitment: ByteArray): Timestamp {
|
override fun getTimestamp(commitment: ByteArray): Timestamp {
|
||||||
try {
|
try {
|
||||||
val client = HttpClientManager.getHttpClient(forceProxy)
|
|
||||||
val url = url + "/timestamp/" + Hex.encode(commitment)
|
val url = url + "/timestamp/" + Hex.encode(commitment)
|
||||||
|
|
||||||
val request =
|
val request =
|
||||||
|
|||||||
+3
-16
@@ -21,14 +21,13 @@
|
|||||||
package com.vitorpamplona.amethyst.service.ots
|
package com.vitorpamplona.amethyst.service.ots
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.BuildConfig
|
import com.vitorpamplona.amethyst.BuildConfig
|
||||||
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
|
|
||||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.ICalendarAsyncSubmit
|
import com.vitorpamplona.quartz.nip03Timestamp.ots.ICalendarAsyncSubmit
|
||||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.StreamDeserializationContext
|
import com.vitorpamplona.quartz.nip03Timestamp.ots.StreamDeserializationContext
|
||||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.Timestamp
|
import com.vitorpamplona.quartz.nip03Timestamp.ots.Timestamp
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
import java.util.concurrent.BlockingQueue
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For making async calls to a calendar server
|
* For making async calls to a calendar server
|
||||||
@@ -36,19 +35,10 @@ import java.util.concurrent.BlockingQueue
|
|||||||
class OkHttpCalendarAsyncSubmit(
|
class OkHttpCalendarAsyncSubmit(
|
||||||
private val url: String,
|
private val url: String,
|
||||||
private val digest: ByteArray,
|
private val digest: ByteArray,
|
||||||
private val forceProxy: Boolean,
|
private val client: OkHttpClient,
|
||||||
) : ICalendarAsyncSubmit {
|
) : ICalendarAsyncSubmit {
|
||||||
private var queue: BlockingQueue<Optional<Timestamp>>? = null
|
|
||||||
|
|
||||||
fun setQueue(queue: BlockingQueue<Optional<Timestamp>>?) {
|
|
||||||
this.queue = queue
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(Exception::class)
|
|
||||||
override fun call(): Optional<Timestamp> {
|
override fun call(): Optional<Timestamp> {
|
||||||
val client = HttpClientManager.getHttpClient(forceProxy)
|
|
||||||
val url = "$url/digest"
|
val url = "$url/digest"
|
||||||
|
|
||||||
val mediaType = "application/x-www-form-urlencoded; charset=utf-8".toMediaType()
|
val mediaType = "application/x-www-form-urlencoded; charset=utf-8".toMediaType()
|
||||||
val requestBody = digest.toRequestBody(mediaType)
|
val requestBody = digest.toRequestBody(mediaType)
|
||||||
|
|
||||||
@@ -69,11 +59,8 @@ class OkHttpCalendarAsyncSubmit(
|
|||||||
it.body.bytes(),
|
it.body.bytes(),
|
||||||
)
|
)
|
||||||
val timestamp = Timestamp.deserialize(ctx, digest)
|
val timestamp = Timestamp.deserialize(ctx, digest)
|
||||||
val of = Optional.of(timestamp)
|
return Optional.of(timestamp)
|
||||||
queue!!.add(of)
|
|
||||||
return of
|
|
||||||
} else {
|
} else {
|
||||||
queue!!.add(Optional.empty())
|
|
||||||
return Optional.empty()
|
return Optional.empty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-3
@@ -23,14 +23,15 @@ package com.vitorpamplona.amethyst.service.ots
|
|||||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.CalendarBuilder
|
import com.vitorpamplona.quartz.nip03Timestamp.ots.CalendarBuilder
|
||||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.ICalendar
|
import com.vitorpamplona.quartz.nip03Timestamp.ots.ICalendar
|
||||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.ICalendarAsyncSubmit
|
import com.vitorpamplona.quartz.nip03Timestamp.ots.ICalendarAsyncSubmit
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
class OkHttpCalendarBuilder(
|
class OkHttpCalendarBuilder(
|
||||||
val forceProxy: (String) -> Boolean,
|
val clientFn: (url: String) -> OkHttpClient,
|
||||||
) : CalendarBuilder {
|
) : CalendarBuilder {
|
||||||
override fun newSyncCalendar(url: String): ICalendar = OkHttpCalendar(url, forceProxy(url))
|
override fun newSyncCalendar(url: String): ICalendar = OkHttpCalendar(url, clientFn(url))
|
||||||
|
|
||||||
override fun newAsyncCalendar(
|
override fun newAsyncCalendar(
|
||||||
url: String,
|
url: String,
|
||||||
digest: ByteArray,
|
digest: ByteArray,
|
||||||
): ICalendarAsyncSubmit = OkHttpCalendarAsyncSubmit(url, digest, forceProxy(url))
|
): ICalendarAsyncSubmit = OkHttpCalendarAsyncSubmit(url, digest, clientFn(url))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.service.ots
|
||||||
|
|
||||||
|
import android.util.LruCache
|
||||||
|
import com.vitorpamplona.quartz.nip03Timestamp.ots.BlockHeader
|
||||||
|
|
||||||
|
class OtsBlockHeightCache {
|
||||||
|
val cacheHeaders = LruCache<String, BlockHeader>(100)
|
||||||
|
val cacheHeights = LruCache<Int, String>(100)
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.service.ots
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager
|
||||||
|
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
|
||||||
|
|
||||||
|
class OtsResolverBuilder {
|
||||||
|
fun getAPI(usingTor: Boolean) =
|
||||||
|
if (usingTor) {
|
||||||
|
OkHttpBitcoinExplorer.MEMPOOL_API_URL
|
||||||
|
} else {
|
||||||
|
OkHttpBitcoinExplorer.BLOCKSTREAM_API_URL
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build(
|
||||||
|
okHttpClients: DualHttpClientManager,
|
||||||
|
shouldUseTorForUrl: (String) -> Boolean,
|
||||||
|
cache: OtsBlockHeightCache,
|
||||||
|
): OtsResolver {
|
||||||
|
val shouldUseTor = shouldUseTorForUrl(OkHttpBitcoinExplorer.MEMPOOL_API_URL)
|
||||||
|
|
||||||
|
return OtsResolver(
|
||||||
|
OkHttpBitcoinExplorer(
|
||||||
|
getAPI(shouldUseTor),
|
||||||
|
okHttpClients.getHttpClient(shouldUseTor),
|
||||||
|
cache,
|
||||||
|
),
|
||||||
|
OkHttpCalendarBuilder {
|
||||||
|
okHttpClients.getHttpClient(shouldUseTorForUrl(it))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -61,7 +61,7 @@ fun GetVideoController(
|
|||||||
// If there is a connection, don't wait.
|
// If there is a connection, don't wait.
|
||||||
if (!onlyOnePreparing.getAndSet(true)) {
|
if (!onlyOnePreparing.getAndSet(true)) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
Log.d("PlaybackService", "Preparing Video ${controllerId.id} $mediaItem.src.videoUri")
|
Log.d("PlaybackService", "Preparing Video ${controllerId.id} ${mediaItem.src.videoUri}")
|
||||||
PlaybackServiceClient.prepareController(
|
PlaybackServiceClient.prepareController(
|
||||||
controllerId,
|
controllerId,
|
||||||
mediaItem.src.videoUri,
|
mediaItem.src.videoUri,
|
||||||
|
|||||||
+1
-5
@@ -26,7 +26,6 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
|
|
||||||
import com.vitorpamplona.amethyst.service.playback.composable.mainVideo.VideoPlayerActiveMutex
|
import com.vitorpamplona.amethyst.service.playback.composable.mainVideo.VideoPlayerActiveMutex
|
||||||
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.GetMediaItem
|
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.GetMediaItem
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
@@ -64,10 +63,7 @@ fun VideoViewInner(
|
|||||||
nostrUriCallback,
|
nostrUriCallback,
|
||||||
mimeType,
|
mimeType,
|
||||||
aspectRatio,
|
aspectRatio,
|
||||||
proxyPort =
|
proxyPort = accountViewModel.proxyPortFor(videoUri),
|
||||||
HttpClientManager.getCurrentProxyPort(
|
|
||||||
accountViewModel.account.shouldUseTorForVideoDownload(videoUri),
|
|
||||||
),
|
|
||||||
) { mediaItem ->
|
) { mediaItem ->
|
||||||
GetVideoController(
|
GetVideoController(
|
||||||
mediaItem = mediaItem,
|
mediaItem = mediaItem,
|
||||||
|
|||||||
+1
-1
@@ -91,7 +91,7 @@ private fun saveMediaToGalleryInner(
|
|||||||
) {
|
) {
|
||||||
MediaSaverToDisk.saveDownloadingIfNeeded(
|
MediaSaverToDisk.saveDownloadingIfNeeded(
|
||||||
videoUri = videoUri,
|
videoUri = videoUri,
|
||||||
forceProxy = accountViewModel.account.shouldUseTorForVideoDownload(),
|
okHttpClient = accountViewModel::okHttpClientForVideo,
|
||||||
mimeType = mimeType,
|
mimeType = mimeType,
|
||||||
localContext = localContext,
|
localContext = localContext,
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
|
|||||||
-20
@@ -32,26 +32,6 @@ import kotlinx.coroutines.withContext
|
|||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
/**
|
|
||||||
* Copyright (c) 2024 Vitor Pamplona
|
|
||||||
*
|
|
||||||
* Permission 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.
|
|
||||||
*/
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
@SuppressLint("UnsafeOptInUsageError")
|
||||||
class VideoCache {
|
class VideoCache {
|
||||||
var exoPlayerCacheSize: Long = 150 * 1024 * 1024 // 150MB
|
var exoPlayerCacheSize: Long = 150 * 1024 * 1024 // 150MB
|
||||||
|
|||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.service.playback.diskCache
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import com.vitorpamplona.amethyst.service.safeCacheDir
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
|
class VideoCacheFactory {
|
||||||
|
companion object {
|
||||||
|
fun new(app: Application): VideoCache {
|
||||||
|
val newCache = VideoCache()
|
||||||
|
runBlocking {
|
||||||
|
newCache.initFileCache(
|
||||||
|
app,
|
||||||
|
app.safeCacheDir().resolve("exoplayer"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return newCache
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-2
@@ -27,15 +27,18 @@ import androidx.annotation.OptIn
|
|||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.datasource.DataSourceBitmapLoader
|
||||||
|
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
import androidx.media3.session.MediaSession
|
import androidx.media3.session.MediaSession
|
||||||
import com.google.common.util.concurrent.Futures
|
import com.google.common.util.concurrent.Futures
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
import com.vitorpamplona.amethyst.Amethyst
|
import com.vitorpamplona.amethyst.ui.MainActivity
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
class SessionListener(
|
class SessionListener(
|
||||||
val session: MediaSession,
|
val session: MediaSession,
|
||||||
@@ -51,6 +54,7 @@ class SessionListener(
|
|||||||
*/
|
*/
|
||||||
class MediaSessionPool(
|
class MediaSessionPool(
|
||||||
val exoPlayerPool: ExoPlayerPool,
|
val exoPlayerPool: ExoPlayerPool,
|
||||||
|
val okHttpClient: OkHttpClient,
|
||||||
val reset: (MediaSession) -> Unit,
|
val reset: (MediaSession) -> Unit,
|
||||||
) {
|
) {
|
||||||
val globalCallback = MediaSessionCallback(this)
|
val globalCallback = MediaSessionCallback(this)
|
||||||
@@ -79,6 +83,7 @@ class MediaSessionPool(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
fun newSession(
|
fun newSession(
|
||||||
id: String,
|
id: String,
|
||||||
context: Context,
|
context: Context,
|
||||||
@@ -87,6 +92,12 @@ class MediaSessionPool(
|
|||||||
MediaSession
|
MediaSession
|
||||||
.Builder(context, exoPlayerPool.acquirePlayer(context))
|
.Builder(context, exoPlayerPool.acquirePlayer(context))
|
||||||
.apply {
|
.apply {
|
||||||
|
setBitmapLoader(
|
||||||
|
DataSourceBitmapLoader(
|
||||||
|
DataSourceBitmapLoader.DEFAULT_EXECUTOR_SERVICE.get(),
|
||||||
|
OkHttpDataSource.Factory(okHttpClient),
|
||||||
|
),
|
||||||
|
)
|
||||||
setId(id)
|
setId(id)
|
||||||
setCallback(globalCallback)
|
setCallback(globalCallback)
|
||||||
}.build()
|
}.build()
|
||||||
@@ -180,7 +191,7 @@ class MediaSessionPool(
|
|||||||
|
|
||||||
// set up return call when clicking on the Notification bar
|
// set up return call when clicking on the Notification bar
|
||||||
mediaItems.firstOrNull()?.mediaMetadata?.extras?.getString("callbackUri")?.let {
|
mediaItems.firstOrNull()?.mediaMetadata?.extras?.getString("callbackUri")?.let {
|
||||||
mediaSession.setSessionActivity(Amethyst.Companion.instance.createIntent(it))
|
mediaSession.setSessionActivity(MainActivity.createIntent(it))
|
||||||
}
|
}
|
||||||
|
|
||||||
return Futures.immediateFuture(mediaItems)
|
return Futures.immediateFuture(mediaItems)
|
||||||
|
|||||||
+12
-6
@@ -28,7 +28,7 @@ import androidx.media3.common.util.UnstableApi
|
|||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
import androidx.media3.session.MediaSession
|
import androidx.media3.session.MediaSession
|
||||||
import androidx.media3.session.MediaSessionService
|
import androidx.media3.session.MediaSessionService
|
||||||
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia
|
import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia
|
||||||
import com.vitorpamplona.amethyst.service.playback.playerPool.ExoPlayerBuilder
|
import com.vitorpamplona.amethyst.service.playback.playerPool.ExoPlayerBuilder
|
||||||
import com.vitorpamplona.amethyst.service.playback.playerPool.ExoPlayerPool
|
import com.vitorpamplona.amethyst.service.playback.playerPool.ExoPlayerPool
|
||||||
@@ -43,6 +43,7 @@ class PlaybackService : MediaSessionService() {
|
|||||||
fun newPool(okHttp: OkHttpClient): MediaSessionPool =
|
fun newPool(okHttp: OkHttpClient): MediaSessionPool =
|
||||||
MediaSessionPool(
|
MediaSessionPool(
|
||||||
ExoPlayerPool(ExoPlayerBuilder(okHttp)),
|
ExoPlayerPool(ExoPlayerBuilder(okHttp)),
|
||||||
|
okHttpClient = okHttp,
|
||||||
reset = { session ->
|
reset = { session ->
|
||||||
(session.player as ExoPlayer).apply {
|
(session.player as ExoPlayer).apply {
|
||||||
repeatMode = Player.REPEAT_MODE_ONE
|
repeatMode = Player.REPEAT_MODE_ONE
|
||||||
@@ -59,12 +60,12 @@ class PlaybackService : MediaSessionService() {
|
|||||||
poolNoProxy?.let { return it }
|
poolNoProxy?.let { return it }
|
||||||
|
|
||||||
// creates new
|
// creates new
|
||||||
return newPool(HttpClientManager.getHttpClient(false)).also { poolNoProxy = it }
|
return newPool(Amethyst.instance.okHttpClients.getHttpClient(false)).also { poolNoProxy = it }
|
||||||
} else {
|
} else {
|
||||||
poolWithProxy?.let { pool ->
|
poolWithProxy?.let { pool ->
|
||||||
// with proxy, check if the port is the same.
|
// with proxy, check if the port is the same.
|
||||||
val okHttp = HttpClientManager.getHttpClient(true)
|
val okHttp = Amethyst.instance.okHttpClients.getHttpClient(true)
|
||||||
if (okHttp.proxy == pool.exoPlayerPool.builder.okHttp.proxy) {
|
if (okHttp.proxy != null && okHttp.proxy == pool.exoPlayerPool.builder.okHttp.proxy) {
|
||||||
return pool
|
return pool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,12 +74,17 @@ class PlaybackService : MediaSessionService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// creates brand new
|
// creates brand new
|
||||||
return newPool(HttpClientManager.getHttpClient(true)).also { poolWithProxy = it }
|
return newPool(Amethyst.instance.okHttpClients.getHttpClient(true)).also { poolWithProxy = it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
Log.d("PlaybackService", "PlaybackService.onCreate")
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
Log.d("Lifetime Event", "PlaybackService.onDestroy")
|
Log.d("PlaybackService", "PlaybackService.onDestroy")
|
||||||
|
|
||||||
poolWithProxy?.destroy()
|
poolWithProxy?.destroy()
|
||||||
poolNoProxy?.destroy()
|
poolNoProxy?.destroy()
|
||||||
|
|||||||
@@ -21,21 +21,21 @@
|
|||||||
package com.vitorpamplona.amethyst.service.previews
|
package com.vitorpamplona.amethyst.service.previews
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
|
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
|
||||||
class UrlPreview {
|
class UrlPreview {
|
||||||
suspend fun fetch(
|
suspend fun fetch(
|
||||||
url: String,
|
url: String,
|
||||||
forceProxy: Boolean,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
onComplete: suspend (urlInfo: UrlInfoItem) -> Unit,
|
onComplete: suspend (urlInfo: UrlInfoItem) -> Unit,
|
||||||
onFailed: suspend (t: Throwable) -> Unit,
|
onFailed: suspend (t: Throwable) -> Unit,
|
||||||
) = try {
|
) = try {
|
||||||
onComplete(getDocument(url, forceProxy))
|
onComplete(getDocument(url, okHttpClient))
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
if (t is CancellationException) throw t
|
if (t is CancellationException) throw t
|
||||||
onFailed(t)
|
onFailed(t)
|
||||||
@@ -43,7 +43,7 @@ class UrlPreview {
|
|||||||
|
|
||||||
suspend fun getDocument(
|
suspend fun getDocument(
|
||||||
url: String,
|
url: String,
|
||||||
forceProxy: Boolean,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
): UrlInfoItem =
|
): UrlInfoItem =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val request =
|
val request =
|
||||||
@@ -52,7 +52,7 @@ class UrlPreview {
|
|||||||
.url(url)
|
.url(url)
|
||||||
.get()
|
.get()
|
||||||
.build()
|
.build()
|
||||||
HttpClientManager.getHttpClient(forceProxy).newCall(request).execute().use {
|
okHttpClient(url).newCall(request).execute().use {
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
if (it.isSuccessful) {
|
if (it.isSuccessful) {
|
||||||
val mimeType =
|
val mimeType =
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.service.proxyPort
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
||||||
|
class ImageProxyFlow(
|
||||||
|
image: StateFlow<Boolean>,
|
||||||
|
) {
|
||||||
|
val status =
|
||||||
|
image.onEach {
|
||||||
|
Amethyst.instance.setImageLoader(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.service.proxyPort
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.ui.tor.TorServiceStatus
|
||||||
|
import com.vitorpamplona.amethyst.ui.tor.TorType
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
class ProxyPortFlow(
|
||||||
|
torType: MutableStateFlow<TorType>,
|
||||||
|
externalSocksPort: MutableStateFlow<Int>,
|
||||||
|
torServiceStatus: StateFlow<TorServiceStatus>,
|
||||||
|
) {
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
val status =
|
||||||
|
torType
|
||||||
|
.flatMapLatest { torType ->
|
||||||
|
when (torType) {
|
||||||
|
TorType.INTERNAL -> {
|
||||||
|
// subscribing to status turns Tor service on
|
||||||
|
torServiceStatus.map {
|
||||||
|
if (it is TorServiceStatus.Active) {
|
||||||
|
it.port
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TorType.EXTERNAL -> {
|
||||||
|
externalSocksPort.map { port ->
|
||||||
|
if (port > 0) {
|
||||||
|
port
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> MutableStateFlow(null)
|
||||||
|
}
|
||||||
|
}.distinctUntilChanged()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun computePort(
|
||||||
|
torType: TorType,
|
||||||
|
externalPort: Int,
|
||||||
|
status: TorServiceStatus,
|
||||||
|
): Int? =
|
||||||
|
when (torType) {
|
||||||
|
TorType.INTERNAL -> {
|
||||||
|
if (status is TorServiceStatus.Active && status.port > 0) {
|
||||||
|
status.port
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TorType.EXTERNAL -> {
|
||||||
|
if (externalPort > 0) {
|
||||||
|
externalPort
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.service.relays
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import com.vitorpamplona.amethyst.service.connectivity.ConnectivityManager
|
||||||
|
import com.vitorpamplona.amethyst.ui.tor.TorManager
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
|
||||||
|
/**
|
||||||
|
* There should be only one instance of the Tor binding per app.
|
||||||
|
*
|
||||||
|
* Tor will connect as soon as status is listened to.
|
||||||
|
*/
|
||||||
|
class RelayManager(
|
||||||
|
app: Application,
|
||||||
|
scope: CoroutineScope,
|
||||||
|
torManager: TorManager,
|
||||||
|
connManager: ConnectivityManager,
|
||||||
|
) {
|
||||||
|
val relayService =
|
||||||
|
combine(
|
||||||
|
torManager.status,
|
||||||
|
connManager.status,
|
||||||
|
) { torStatus, connManager ->
|
||||||
|
}
|
||||||
|
|
||||||
|
val status: StateFlow<RelayServiceStatus> =
|
||||||
|
RelayService(app).status.stateIn(
|
||||||
|
scope,
|
||||||
|
SharingStarted.WhileSubscribed(30000),
|
||||||
|
RelayServiceStatus.Off,
|
||||||
|
)
|
||||||
|
}
|
||||||
+22
-25
@@ -18,34 +18,31 @@
|
|||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
* 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.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.components
|
package com.vitorpamplona.amethyst.service.relays
|
||||||
|
|
||||||
import androidx.compose.foundation.text.ClickableText
|
import android.content.Context
|
||||||
import androidx.compose.material3.LocalTextStyle
|
import android.util.Log
|
||||||
import androidx.compose.material3.MaterialTheme
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import androidx.compose.runtime.Composable
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import androidx.compose.runtime.getValue
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import kotlinx.coroutines.launch
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import com.vitorpamplona.amethyst.model.User
|
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
|
||||||
|
|
||||||
@Composable
|
class RelayService(
|
||||||
fun ClickableUserTag(
|
val context: Context,
|
||||||
user: User,
|
|
||||||
nav: INav,
|
|
||||||
) {
|
) {
|
||||||
val route = remember { "User/${user.pubkeyHex}" }
|
val status =
|
||||||
|
callbackFlow {
|
||||||
|
Log.d("RelayService", "Starting Relay Services")
|
||||||
|
trySend(RelayServiceStatus.Connecting)
|
||||||
|
|
||||||
val innerUserState by user.live().metadata.observeAsState()
|
// ServiceManager
|
||||||
|
|
||||||
val userName =
|
awaitClose {
|
||||||
remember(innerUserState) { AnnotatedString("@${innerUserState?.user?.toBestDisplayName()}") }
|
Log.d("RelayService", "Stopping Relay Services")
|
||||||
|
launch {
|
||||||
ClickableText(
|
// ServiceManager.pauseAndLogOff()
|
||||||
text = userName,
|
}
|
||||||
onClick = { nav.nav(route) },
|
trySend(RelayServiceStatus.Off)
|
||||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary),
|
}
|
||||||
)
|
}.distinctUntilChanged()
|
||||||
}
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.service.relays
|
||||||
|
|
||||||
|
import com.vitorpamplona.ammolite.relays.NostrClient
|
||||||
|
|
||||||
|
sealed class RelayServiceStatus {
|
||||||
|
data class Active(
|
||||||
|
val client: NostrClient,
|
||||||
|
) : RelayServiceStatus()
|
||||||
|
|
||||||
|
object Off : RelayServiceStatus()
|
||||||
|
|
||||||
|
object Connecting : RelayServiceStatus()
|
||||||
|
}
|
||||||
@@ -26,11 +26,12 @@ import android.media.MediaDataSource
|
|||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.vitorpamplona.amethyst.commons.blurhash.toBlurhash
|
import com.vitorpamplona.amethyst.commons.blurhash.toBlurhash
|
||||||
import com.vitorpamplona.amethyst.service.Blurhash
|
import com.vitorpamplona.amethyst.service.images.BlurhashWrapper
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||||
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
|
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
|
||||||
import com.vitorpamplona.quartz.utils.sha256.sha256
|
import com.vitorpamplona.quartz.utils.sha256.sha256
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class FileHeader(
|
class FileHeader(
|
||||||
@@ -38,7 +39,7 @@ class FileHeader(
|
|||||||
val hash: String,
|
val hash: String,
|
||||||
val size: Int,
|
val size: Int,
|
||||||
val dim: DimensionTag?,
|
val dim: DimensionTag?,
|
||||||
val blurHash: Blurhash?,
|
val blurHash: BlurhashWrapper?,
|
||||||
) {
|
) {
|
||||||
class UnableToDownload(
|
class UnableToDownload(
|
||||||
val fileUrl: String,
|
val fileUrl: String,
|
||||||
@@ -49,10 +50,10 @@ class FileHeader(
|
|||||||
fileUrl: String,
|
fileUrl: String,
|
||||||
mimeType: String?,
|
mimeType: String?,
|
||||||
dimPrecomputed: DimensionTag?,
|
dimPrecomputed: DimensionTag?,
|
||||||
forceProxy: Boolean,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
): Result<FileHeader> =
|
): Result<FileHeader> =
|
||||||
try {
|
try {
|
||||||
val imageData: ImageDownloader.Blob? = ImageDownloader().waitAndGetImage(fileUrl, forceProxy)
|
val imageData: ImageDownloader.Blob? = ImageDownloader().waitAndGetImage(fileUrl, okHttpClient)
|
||||||
|
|
||||||
if (imageData != null) {
|
if (imageData != null) {
|
||||||
prepare(imageData.bytes, mimeType ?: imageData.contentType, dimPrecomputed)
|
prepare(imageData.bytes, mimeType ?: imageData.contentType, dimPrecomputed)
|
||||||
@@ -79,13 +80,13 @@ class FileHeader(
|
|||||||
val opt = BitmapFactory.Options()
|
val opt = BitmapFactory.Options()
|
||||||
opt.inPreferredConfig = Bitmap.Config.ARGB_8888
|
opt.inPreferredConfig = Bitmap.Config.ARGB_8888
|
||||||
val mBitmap = BitmapFactory.decodeByteArray(data, 0, data.size, opt)
|
val mBitmap = BitmapFactory.decodeByteArray(data, 0, data.size, opt)
|
||||||
Pair(Blurhash(mBitmap.toBlurhash()), DimensionTag(mBitmap.width, mBitmap.height))
|
Pair(BlurhashWrapper(mBitmap.toBlurhash()), DimensionTag(mBitmap.width, mBitmap.height))
|
||||||
} else if (mimeType?.startsWith("video/") == true) {
|
} else if (mimeType?.startsWith("video/") == true) {
|
||||||
val mediaMetadataRetriever = MediaMetadataRetriever()
|
val mediaMetadataRetriever = MediaMetadataRetriever()
|
||||||
mediaMetadataRetriever.setDataSource(ByteArrayMediaDataSource(data))
|
mediaMetadataRetriever.setDataSource(ByteArrayMediaDataSource(data))
|
||||||
|
|
||||||
val newDim = mediaMetadataRetriever.prepareDimFromVideo() ?: dimPrecomputed
|
val newDim = mediaMetadataRetriever.prepareDimFromVideo() ?: dimPrecomputed
|
||||||
val blurhash = mediaMetadataRetriever.getThumbnail()?.toBlurhash()?.let { Blurhash(it) }
|
val blurhash = mediaMetadataRetriever.getThumbnail()?.toBlurhash()?.let { BlurhashWrapper(it) }
|
||||||
|
|
||||||
if (newDim?.hasSize() == true) {
|
if (newDim?.hasSize() == true) {
|
||||||
Pair(blurhash, newDim)
|
Pair(blurhash, newDim)
|
||||||
|
|||||||
+10
-8
@@ -20,11 +20,11 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.service.uploads
|
package com.vitorpamplona.amethyst.service.uploads
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
|
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ class ImageDownloader {
|
|||||||
|
|
||||||
suspend fun waitAndGetImage(
|
suspend fun waitAndGetImage(
|
||||||
imageUrl: String,
|
imageUrl: String,
|
||||||
forceProxy: Boolean,
|
okHttpClient: (url: String) -> OkHttpClient,
|
||||||
): Blob? =
|
): Blob? =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
var imageData: Blob? = null
|
var imageData: Blob? = null
|
||||||
@@ -46,7 +46,7 @@ class ImageDownloader {
|
|||||||
while (imageData == null && tentatives < 15) {
|
while (imageData == null && tentatives < 15) {
|
||||||
imageData =
|
imageData =
|
||||||
try {
|
try {
|
||||||
tryGetTheImage(imageUrl, forceProxy)
|
tryGetTheImage(imageUrl, okHttpClient)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e is CancellationException) throw e
|
if (e is CancellationException) throw e
|
||||||
null
|
null
|
||||||
@@ -63,15 +63,16 @@ class ImageDownloader {
|
|||||||
|
|
||||||
private suspend fun tryGetTheImage(
|
private suspend fun tryGetTheImage(
|
||||||
imageUrl: String,
|
imageUrl: String,
|
||||||
forceProxy: Boolean,
|
okHttpClient: (url: String) -> OkHttpClient,
|
||||||
): Blob? =
|
): Blob? =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
// TODO: Migrate to OkHttp
|
// TODO: Migrate to OkHttp
|
||||||
HttpURLConnection.setFollowRedirects(true)
|
HttpURLConnection.setFollowRedirects(true)
|
||||||
var url = URL(imageUrl)
|
var url = URL(imageUrl)
|
||||||
|
var clientProxy = okHttpClient(imageUrl).proxy
|
||||||
var huc =
|
var huc =
|
||||||
if (forceProxy) {
|
if (clientProxy != null) {
|
||||||
url.openConnection(HttpClientManager.getCurrentProxy()) as HttpURLConnection
|
url.openConnection(clientProxy) as HttpURLConnection
|
||||||
} else {
|
} else {
|
||||||
url.openConnection() as HttpURLConnection
|
url.openConnection() as HttpURLConnection
|
||||||
}
|
}
|
||||||
@@ -83,9 +84,10 @@ class ImageDownloader {
|
|||||||
|
|
||||||
// open the new connnection again
|
// open the new connnection again
|
||||||
url = URL(newUrl)
|
url = URL(newUrl)
|
||||||
|
clientProxy = okHttpClient(newUrl).proxy
|
||||||
huc =
|
huc =
|
||||||
if (forceProxy) {
|
if (clientProxy != null) {
|
||||||
url.openConnection(HttpClientManager.getCurrentProxy()) as HttpURLConnection
|
url.openConnection(clientProxy) as HttpURLConnection
|
||||||
} else {
|
} else {
|
||||||
url.openConnection() as HttpURLConnection
|
url.openConnection() as HttpURLConnection
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-6
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.uploads
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.service.uploads.UploadingState.UploadingFinalState
|
import com.vitorpamplona.amethyst.service.uploads.UploadingState.UploadingFinalState
|
||||||
@@ -32,6 +33,7 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
|||||||
import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher
|
import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
sealed class UploadingState {
|
sealed class UploadingState {
|
||||||
@@ -149,7 +151,7 @@ class UploadOrchestrator {
|
|||||||
alt = alt,
|
alt = alt,
|
||||||
sensitiveContent = contentWarningReason,
|
sensitiveContent = contentWarningReason,
|
||||||
serverBaseUrl = serverBaseUrl,
|
serverBaseUrl = serverBaseUrl,
|
||||||
forceProxy = account::shouldUseTorForNIP96,
|
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) },
|
||||||
onProgress = { percent: Float ->
|
onProgress = { percent: Float ->
|
||||||
updateState(0.2 + (0.2 * percent), UploadingState.Uploading)
|
updateState(0.2 + (0.2 * percent), UploadingState.Uploading)
|
||||||
},
|
},
|
||||||
@@ -162,7 +164,7 @@ class UploadOrchestrator {
|
|||||||
localContentType = contentType,
|
localContentType = contentType,
|
||||||
originalContentType = contentTypeForResult,
|
originalContentType = contentTypeForResult,
|
||||||
originalHash = originalHash,
|
originalHash = originalHash,
|
||||||
forceProxy = account::shouldUseTorForNIP96,
|
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) },
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e is CancellationException) throw e
|
if (e is CancellationException) throw e
|
||||||
@@ -193,7 +195,7 @@ class UploadOrchestrator {
|
|||||||
alt = alt,
|
alt = alt,
|
||||||
sensitiveContent = contentWarningReason,
|
sensitiveContent = contentWarningReason,
|
||||||
serverBaseUrl = serverBaseUrl,
|
serverBaseUrl = serverBaseUrl,
|
||||||
forceProxy = account::shouldUseTorForNIP96,
|
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) },
|
||||||
httpAuth = account::createBlossomUploadAuth,
|
httpAuth = account::createBlossomUploadAuth,
|
||||||
context = context,
|
context = context,
|
||||||
)
|
)
|
||||||
@@ -201,7 +203,7 @@ class UploadOrchestrator {
|
|||||||
verifyHeader(
|
verifyHeader(
|
||||||
uploadResult = result,
|
uploadResult = result,
|
||||||
localContentType = contentType,
|
localContentType = contentType,
|
||||||
forceProxy = account::shouldUseTorForNIP96,
|
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) },
|
||||||
originalHash = originalHash,
|
originalHash = originalHash,
|
||||||
originalContentType = contentTypeForResult,
|
originalContentType = contentTypeForResult,
|
||||||
)
|
)
|
||||||
@@ -216,7 +218,7 @@ class UploadOrchestrator {
|
|||||||
localContentType: String?,
|
localContentType: String?,
|
||||||
originalContentType: String?,
|
originalContentType: String?,
|
||||||
originalHash: String?,
|
originalHash: String?,
|
||||||
forceProxy: (String) -> Boolean,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
): UploadingFinalState {
|
): UploadingFinalState {
|
||||||
if (uploadResult.url.isNullOrBlank()) {
|
if (uploadResult.url.isNullOrBlank()) {
|
||||||
return error(R.string.server_did_not_provide_a_url_after_uploading)
|
return error(R.string.server_did_not_provide_a_url_after_uploading)
|
||||||
@@ -224,7 +226,7 @@ class UploadOrchestrator {
|
|||||||
|
|
||||||
updateState(0.6, UploadingState.Downloading)
|
updateState(0.6, UploadingState.Downloading)
|
||||||
|
|
||||||
val imageData: ImageDownloader.Blob? = ImageDownloader().waitAndGetImage(uploadResult.url, forceProxy(uploadResult.url))
|
val imageData: ImageDownloader.Blob? = ImageDownloader().waitAndGetImage(uploadResult.url, okHttpClient)
|
||||||
|
|
||||||
if (imageData != null) {
|
if (imageData != null) {
|
||||||
updateState(0.8, UploadingState.Hashing)
|
updateState(0.8, UploadingState.Hashing)
|
||||||
|
|||||||
+7
-7
@@ -31,7 +31,6 @@ import com.vitorpamplona.amethyst.BuildConfig
|
|||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.service.HttpStatusMessages
|
import com.vitorpamplona.amethyst.service.HttpStatusMessages
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
|
|
||||||
import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult
|
import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult
|
||||||
import com.vitorpamplona.amethyst.service.uploads.nip96.randomChars
|
import com.vitorpamplona.amethyst.service.uploads.nip96.randomChars
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
@@ -40,6 +39,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
|||||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||||
import com.vitorpamplona.quartz.utils.sha256.sha256
|
import com.vitorpamplona.quartz.utils.sha256.sha256
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import okio.BufferedSink
|
import okio.BufferedSink
|
||||||
@@ -70,7 +70,7 @@ class BlossomUploader {
|
|||||||
alt: String?,
|
alt: String?,
|
||||||
sensitiveContent: String?,
|
sensitiveContent: String?,
|
||||||
serverBaseUrl: String,
|
serverBaseUrl: String,
|
||||||
forceProxy: (String) -> Boolean,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
httpAuth: suspend (hash: HexKey, size: Long, alt: String) -> BlossomAuthorizationEvent?,
|
httpAuth: suspend (hash: HexKey, size: Long, alt: String) -> BlossomAuthorizationEvent?,
|
||||||
context: Context,
|
context: Context,
|
||||||
): MediaUploadResult {
|
): MediaUploadResult {
|
||||||
@@ -103,7 +103,7 @@ class BlossomUploader {
|
|||||||
alt,
|
alt,
|
||||||
sensitiveContent,
|
sensitiveContent,
|
||||||
serverBaseUrl,
|
serverBaseUrl,
|
||||||
forceProxy,
|
okHttpClient,
|
||||||
httpAuth,
|
httpAuth,
|
||||||
context,
|
context,
|
||||||
)
|
)
|
||||||
@@ -123,7 +123,7 @@ class BlossomUploader {
|
|||||||
alt: String?,
|
alt: String?,
|
||||||
sensitiveContent: String?,
|
sensitiveContent: String?,
|
||||||
serverBaseUrl: String,
|
serverBaseUrl: String,
|
||||||
forceProxy: (String) -> Boolean,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
httpAuth: suspend (hash: HexKey, size: Long, alt: String) -> BlossomAuthorizationEvent?,
|
httpAuth: suspend (hash: HexKey, size: Long, alt: String) -> BlossomAuthorizationEvent?,
|
||||||
context: Context,
|
context: Context,
|
||||||
): MediaUploadResult {
|
): MediaUploadResult {
|
||||||
@@ -135,7 +135,7 @@ class BlossomUploader {
|
|||||||
|
|
||||||
val apiUrl = serverBaseUrl.removeSuffix("/") + "/upload"
|
val apiUrl = serverBaseUrl.removeSuffix("/") + "/upload"
|
||||||
|
|
||||||
val client = HttpClientManager.getHttpClient(forceProxy(apiUrl))
|
val client = okHttpClient(apiUrl)
|
||||||
val requestBuilder = Request.Builder()
|
val requestBuilder = Request.Builder()
|
||||||
|
|
||||||
val requestBody: RequestBody =
|
val requestBody: RequestBody =
|
||||||
@@ -191,7 +191,7 @@ class BlossomUploader {
|
|||||||
hash: String,
|
hash: String,
|
||||||
contentType: String?,
|
contentType: String?,
|
||||||
serverBaseUrl: String,
|
serverBaseUrl: String,
|
||||||
forceProxy: (String) -> Boolean,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
httpAuth: (hash: HexKey, alt: String) -> BlossomAuthorizationEvent?,
|
httpAuth: (hash: HexKey, alt: String) -> BlossomAuthorizationEvent?,
|
||||||
context: Context,
|
context: Context,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
@@ -213,7 +213,7 @@ class BlossomUploader {
|
|||||||
.delete()
|
.delete()
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
HttpClientManager.getHttpClient(forceProxy(apiUrl)).newCall(request).execute().use { response ->
|
okHttpClient(apiUrl).newCall(request).execute().use { response ->
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.service.uploads.nip95
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import com.vitorpamplona.amethyst.service.safeCacheDir
|
||||||
|
|
||||||
|
class Nip95CacheFactory {
|
||||||
|
companion object {
|
||||||
|
fun new(app: Application) = app.safeCacheDir().resolve("NIP95")
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-13
@@ -31,7 +31,6 @@ import com.vitorpamplona.amethyst.BuildConfig
|
|||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.service.HttpStatusMessages
|
import com.vitorpamplona.amethyst.service.HttpStatusMessages
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
|
|
||||||
import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult
|
import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningTag
|
import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningTag
|
||||||
@@ -44,6 +43,7 @@ import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import okio.BufferedSink
|
import okio.BufferedSink
|
||||||
@@ -62,7 +62,7 @@ class Nip96Uploader {
|
|||||||
alt: String?,
|
alt: String?,
|
||||||
sensitiveContent: String?,
|
sensitiveContent: String?,
|
||||||
serverBaseUrl: String,
|
serverBaseUrl: String,
|
||||||
forceProxy: (String) -> Boolean,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
onProgress: (percentage: Float) -> Unit,
|
onProgress: (percentage: Float) -> Unit,
|
||||||
httpAuth: suspend (String, String, ByteArray?) -> HTTPAuthorizationEvent?,
|
httpAuth: suspend (String, String, ByteArray?) -> HTTPAuthorizationEvent?,
|
||||||
context: Context,
|
context: Context,
|
||||||
@@ -72,8 +72,8 @@ class Nip96Uploader {
|
|||||||
size,
|
size,
|
||||||
alt,
|
alt,
|
||||||
sensitiveContent,
|
sensitiveContent,
|
||||||
ServerInfoRetriever().loadInfo(serverBaseUrl, forceProxy(serverBaseUrl)),
|
ServerInfoRetriever().loadInfo(serverBaseUrl, okHttpClient),
|
||||||
forceProxy,
|
okHttpClient,
|
||||||
onProgress,
|
onProgress,
|
||||||
httpAuth,
|
httpAuth,
|
||||||
context,
|
context,
|
||||||
@@ -95,7 +95,7 @@ class Nip96Uploader {
|
|||||||
alt: String?,
|
alt: String?,
|
||||||
sensitiveContent: String?,
|
sensitiveContent: String?,
|
||||||
server: ServerInfo,
|
server: ServerInfo,
|
||||||
forceProxy: (String) -> Boolean,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
onProgress: (percentage: Float) -> Unit,
|
onProgress: (percentage: Float) -> Unit,
|
||||||
httpAuth: suspend (String, String, ByteArray?) -> HTTPAuthorizationEvent?,
|
httpAuth: suspend (String, String, ByteArray?) -> HTTPAuthorizationEvent?,
|
||||||
context: Context,
|
context: Context,
|
||||||
@@ -117,7 +117,7 @@ class Nip96Uploader {
|
|||||||
alt,
|
alt,
|
||||||
sensitiveContent,
|
sensitiveContent,
|
||||||
server,
|
server,
|
||||||
forceProxy,
|
okHttpClient,
|
||||||
onProgress,
|
onProgress,
|
||||||
httpAuth,
|
httpAuth,
|
||||||
context,
|
context,
|
||||||
@@ -131,7 +131,7 @@ class Nip96Uploader {
|
|||||||
alt: String?,
|
alt: String?,
|
||||||
sensitiveContent: String?,
|
sensitiveContent: String?,
|
||||||
server: ServerInfo,
|
server: ServerInfo,
|
||||||
forceProxy: (String) -> Boolean,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
onProgress: (percentage: Float) -> Unit,
|
onProgress: (percentage: Float) -> Unit,
|
||||||
httpAuth: suspend (String, String, ByteArray?) -> HTTPAuthorizationEvent?,
|
httpAuth: suspend (String, String, ByteArray?) -> HTTPAuthorizationEvent?,
|
||||||
context: Context,
|
context: Context,
|
||||||
@@ -141,7 +141,7 @@ class Nip96Uploader {
|
|||||||
val fileName = randomChars()
|
val fileName = randomChars()
|
||||||
val extension = contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
|
val extension = contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
|
||||||
|
|
||||||
val client = HttpClientManager.getHttpClient(forceProxy(server.apiUrl))
|
val client = okHttpClient(server.apiUrl)
|
||||||
val requestBuilder = Request.Builder()
|
val requestBuilder = Request.Builder()
|
||||||
|
|
||||||
val requestBody: RequestBody =
|
val requestBody: RequestBody =
|
||||||
@@ -182,7 +182,7 @@ class Nip96Uploader {
|
|||||||
response.body.use { body ->
|
response.body.use { body ->
|
||||||
val result = UploadResult.parse(body.string())
|
val result = UploadResult.parse(body.string())
|
||||||
if (!result.processingUrl.isNullOrBlank()) {
|
if (!result.processingUrl.isNullOrBlank()) {
|
||||||
return waitProcessing(result, server, forceProxy, onProgress)
|
return waitProcessing(result, server, okHttpClient, onProgress)
|
||||||
} else if (result.status == "success") {
|
} else if (result.status == "success") {
|
||||||
val event = result.nip94Event
|
val event = result.nip94Event
|
||||||
if (event != null) {
|
if (event != null) {
|
||||||
@@ -263,14 +263,14 @@ class Nip96Uploader {
|
|||||||
hash: String,
|
hash: String,
|
||||||
contentType: String?,
|
contentType: String?,
|
||||||
server: ServerInfo,
|
server: ServerInfo,
|
||||||
forceProxy: (String) -> Boolean,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
httpAuth: (String, String, ByteArray?) -> HTTPAuthorizationEvent?,
|
httpAuth: (String, String, ByteArray?) -> HTTPAuthorizationEvent?,
|
||||||
context: Context,
|
context: Context,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val extension =
|
val extension =
|
||||||
contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
|
contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
|
||||||
|
|
||||||
val client = HttpClientManager.getHttpClient(forceProxy(server.apiUrl))
|
val client = okHttpClient(server.apiUrl)
|
||||||
|
|
||||||
val requestBuilder = Request.Builder()
|
val requestBuilder = Request.Builder()
|
||||||
|
|
||||||
@@ -303,7 +303,7 @@ class Nip96Uploader {
|
|||||||
private suspend fun waitProcessing(
|
private suspend fun waitProcessing(
|
||||||
result: UploadResult,
|
result: UploadResult,
|
||||||
server: ServerInfo,
|
server: ServerInfo,
|
||||||
forceProxy: (String) -> Boolean,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
onProgress: (percentage: Float) -> Unit,
|
onProgress: (percentage: Float) -> Unit,
|
||||||
): MediaUploadResult {
|
): MediaUploadResult {
|
||||||
var currentResult = result
|
var currentResult = result
|
||||||
@@ -320,7 +320,7 @@ class Nip96Uploader {
|
|||||||
.url(procUrl)
|
.url(procUrl)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val client = HttpClientManager.getHttpClient(forceProxy(procUrl))
|
val client = okHttpClient(procUrl)
|
||||||
client.newCall(request).execute().use {
|
client.newCall(request).execute().use {
|
||||||
if (it.isSuccessful) {
|
if (it.isSuccessful) {
|
||||||
it.body.use { currentResult = UploadResult.parse(it.string()) }
|
it.body.use { currentResult = UploadResult.parse(it.string()) }
|
||||||
|
|||||||
+6
-3
@@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.service.uploads.nip96
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
|
|
||||||
import com.vitorpamplona.quartz.nip96FileStorage.info.ServerInfo
|
import com.vitorpamplona.quartz.nip96FileStorage.info.ServerInfo
|
||||||
import com.vitorpamplona.quartz.nip96FileStorage.info.ServerInfoParser
|
import com.vitorpamplona.quartz.nip96FileStorage.info.ServerInfoParser
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
|
||||||
class ServerInfoRetriever {
|
class ServerInfoRetriever {
|
||||||
@@ -33,7 +33,7 @@ class ServerInfoRetriever {
|
|||||||
|
|
||||||
suspend fun loadInfo(
|
suspend fun loadInfo(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
forceProxy: Boolean,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
): ServerInfo {
|
): ServerInfo {
|
||||||
val request: Request =
|
val request: Request =
|
||||||
Request
|
Request
|
||||||
@@ -42,7 +42,10 @@ class ServerInfoRetriever {
|
|||||||
.url(parser.assembleUrl(baseUrl))
|
.url(parser.assembleUrl(baseUrl))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
HttpClientManager.getHttpClient(forceProxy).newCall(request).execute().use { response ->
|
println("AABBCC $baseUrl Request ${parser.assembleUrl(baseUrl)}")
|
||||||
|
|
||||||
|
okHttpClient(baseUrl).newCall(request).execute().use { response ->
|
||||||
|
println("AABBCC $baseUrl Response")
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
response.use {
|
response.use {
|
||||||
val body = it.body.string()
|
val body = it.body.string()
|
||||||
|
|||||||
@@ -20,9 +20,8 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui
|
package com.vitorpamplona.amethyst.ui
|
||||||
|
|
||||||
import android.net.ConnectivityManager
|
import android.app.PendingIntent
|
||||||
import android.net.Network
|
import android.content.Intent
|
||||||
import android.net.NetworkCapabilities
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -31,20 +30,18 @@ import androidx.activity.enableEdgeToEdge
|
|||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.Amethyst
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.LocalPreferences
|
|
||||||
import com.vitorpamplona.amethyst.debugState
|
import com.vitorpamplona.amethyst.debugState
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService
|
import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService
|
||||||
import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils
|
|
||||||
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
|
|
||||||
import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETTING
|
import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETTING
|
||||||
import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia
|
import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||||
import com.vitorpamplona.amethyst.ui.screen.AccountScreen
|
import com.vitorpamplona.amethyst.ui.screen.AccountScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.prepareSharedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
|
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
|
||||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||||
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
||||||
@@ -65,27 +62,19 @@ import kotlinx.coroutines.DelicateCoroutinesApi
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.net.URLEncoder
|
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
import java.util.Timer
|
|
||||||
import kotlin.concurrent.schedule
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
val isOnMobileDataState = mutableStateOf(false)
|
|
||||||
private val isOnWifiDataState = mutableStateOf(false)
|
|
||||||
|
|
||||||
private var shouldPauseService = true
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
Log.d("Lifetime Event", "MainActivity.onCreate")
|
Log.d("ActivityLifecycle", "MainActivity.onCreate $this")
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val sharedPreferencesViewModel = prepareSharedViewModel(act = this)
|
StringResSetup()
|
||||||
|
|
||||||
|
val sharedPreferencesViewModel = prepareSharedViewModel()
|
||||||
AmethystTheme(sharedPreferencesViewModel) {
|
AmethystTheme(sharedPreferencesViewModel) {
|
||||||
val accountStateViewModel: AccountStateViewModel = viewModel()
|
val accountStateViewModel: AccountStateViewModel = viewModel()
|
||||||
|
|
||||||
@@ -98,74 +87,26 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun prepareToLaunchSigner() {
|
|
||||||
shouldPauseService = false
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
val locales = this.applicationContext.resources.configuration.locales
|
Log.d("ActivityLifecycle", "MainActivity.onResume $this")
|
||||||
if (!locales.isEmpty) {
|
|
||||||
checkLanguage(locales.get(0).language)
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d("Lifetime Event", "MainActivity.onResume")
|
|
||||||
|
|
||||||
// starts muted every time
|
// starts muted every time
|
||||||
DEFAULT_MUTED_SETTING.value = true
|
DEFAULT_MUTED_SETTING.value = true
|
||||||
|
|
||||||
// Keep connection alive if it's calling the signer app
|
|
||||||
Log.d("shouldPauseService", "shouldPauseService onResume: $shouldPauseService")
|
|
||||||
if (shouldPauseService) {
|
|
||||||
GlobalScope.launch(Dispatchers.IO) { Amethyst.instance.serviceManager.justStart() }
|
|
||||||
}
|
|
||||||
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
|
||||||
PushNotificationUtils.init(LocalPreferences.allSavedAccounts())
|
|
||||||
}
|
|
||||||
|
|
||||||
val connectivityManager =
|
|
||||||
(getSystemService(ConnectivityManager::class.java) as ConnectivityManager)
|
|
||||||
connectivityManager.registerDefaultNetworkCallback(networkCallback)
|
|
||||||
connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)?.let {
|
|
||||||
updateNetworkCapabilities(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
// resets state until next External Signer Call
|
|
||||||
Timer().schedule(350) { shouldPauseService = true }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
Log.d("Lifetime Event", "MainActivity.onPause")
|
Log.d("ActivityLifecycle", "MainActivity.onPause $this")
|
||||||
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) { LanguageTranslatorService.clear() }
|
||||||
LanguageTranslatorService.clear()
|
|
||||||
}
|
|
||||||
Amethyst.instance.serviceManager.cleanObservers()
|
|
||||||
|
|
||||||
// if (BuildConfig.DEBUG) {
|
|
||||||
GlobalScope.launch(Dispatchers.IO) { debugState(this@MainActivity) }
|
GlobalScope.launch(Dispatchers.IO) { debugState(this@MainActivity) }
|
||||||
// }
|
|
||||||
|
|
||||||
Log.d("shouldPauseService", "shouldPauseService onPause: $shouldPauseService")
|
|
||||||
if (shouldPauseService) {
|
|
||||||
GlobalScope.launch(Dispatchers.IO) { Amethyst.instance.serviceManager.pauseForGood() }
|
|
||||||
}
|
|
||||||
|
|
||||||
(getSystemService(ConnectivityManager::class.java) as ConnectivityManager)
|
|
||||||
.unregisterNetworkCallback(networkCallback)
|
|
||||||
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
|
|
||||||
Log.d("Lifetime Event", "MainActivity.onStart")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
|
|
||||||
@@ -174,117 +115,61 @@ class MainActivity : AppCompatActivity() {
|
|||||||
// serviceManager.trimMemory()
|
// serviceManager.trimMemory()
|
||||||
// }
|
// }
|
||||||
|
|
||||||
Log.d("Lifetime Event", "MainActivity.onStop")
|
Log.d("ActivityLifecycle", "MainActivity.onStop $this")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
Log.d("Lifetime Event", "MainActivity.onDestroy")
|
Log.d("ActivityLifecycle", "MainActivity.onDestroy $this")
|
||||||
|
|
||||||
BackgroundMedia.removeBackgroundControllerAndReleaseIt()
|
BackgroundMedia.removeBackgroundControllerAndReleaseIt()
|
||||||
|
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateNetworkCapabilities(networkCapabilities: NetworkCapabilities): Boolean {
|
companion object {
|
||||||
val unmetered = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
fun createIntent(callbackUri: String): PendingIntent =
|
||||||
|
PendingIntent.getActivity(
|
||||||
val isOnMobileData = !unmetered || networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|
Amethyst.instance,
|
||||||
val isOnWifi = unmetered && networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
0,
|
||||||
|
Intent(Intent.ACTION_VIEW, callbackUri.toUri(), Amethyst.instance, MainActivity::class.java),
|
||||||
var changedNetwork = false
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
||||||
|
)
|
||||||
if (isOnMobileDataState.value != isOnMobileData) {
|
|
||||||
isOnMobileDataState.value = isOnMobileData
|
|
||||||
|
|
||||||
changedNetwork = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isOnWifiDataState.value != isOnWifi) {
|
|
||||||
isOnWifiDataState.value = isOnWifi
|
|
||||||
|
|
||||||
changedNetwork = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changedNetwork) {
|
|
||||||
if (isOnMobileData) {
|
|
||||||
HttpClientManager.setDefaultTimeout(HttpClientManager.DEFAULT_TIMEOUT_ON_MOBILE)
|
|
||||||
} else {
|
|
||||||
HttpClientManager.setDefaultTimeout(HttpClientManager.DEFAULT_TIMEOUT_ON_WIFI)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return changedNetwork
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
|
||||||
private val networkCallback =
|
|
||||||
object : ConnectivityManager.NetworkCallback() {
|
|
||||||
var lastNetwork: Network? = null
|
|
||||||
|
|
||||||
override fun onAvailable(network: Network) {
|
|
||||||
super.onAvailable(network)
|
|
||||||
|
|
||||||
Log.d("ServiceManager NetworkCallback", "onAvailable: $shouldPauseService")
|
|
||||||
if (shouldPauseService && lastNetwork != null && lastNetwork != network) {
|
|
||||||
GlobalScope.launch(Dispatchers.IO) { Amethyst.instance.serviceManager.forceRestart() }
|
|
||||||
}
|
|
||||||
|
|
||||||
lastNetwork = network
|
|
||||||
}
|
|
||||||
|
|
||||||
// Network capabilities have changed for the network
|
|
||||||
override fun onCapabilitiesChanged(
|
|
||||||
network: Network,
|
|
||||||
networkCapabilities: NetworkCapabilities,
|
|
||||||
) {
|
|
||||||
super.onCapabilitiesChanged(network, networkCapabilities)
|
|
||||||
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
|
||||||
Log.d(
|
|
||||||
"ServiceManager NetworkCallback",
|
|
||||||
"onCapabilitiesChanged: ${network.networkHandle} hasMobileData ${isOnMobileDataState.value} hasWifi ${isOnWifiDataState.value}",
|
|
||||||
)
|
|
||||||
if (updateNetworkCapabilities(networkCapabilities) && shouldPauseService) {
|
|
||||||
Amethyst.instance.serviceManager.forceRestart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun uriToRoute(uri: String?): String? =
|
fun uriToRoute(uri: String?): Route? =
|
||||||
if (uri?.startsWith("notifications", true) == true || uri?.startsWith("nostr:notifications", true) == true) {
|
if (uri?.startsWith("notifications", true) == true || uri?.startsWith("nostr:notifications", true) == true) {
|
||||||
Route.Notification.route.replace("{scrollToTop}", "true")
|
Route.Notification
|
||||||
} else {
|
} else {
|
||||||
if (uri?.startsWith("hashtag?id=") == true || uri?.startsWith("nostr:hashtag?id=") == true) {
|
if (uri?.startsWith("hashtag?id=") == true || uri?.startsWith("nostr:hashtag?id=") == true) {
|
||||||
Route.Hashtag.route.replace("{id}", uri.removePrefix("nostr:").removePrefix("hashtag?id="))
|
Route.Hashtag(uri.removePrefix("nostr:").removePrefix("hashtag?id="))
|
||||||
} else {
|
} else {
|
||||||
val nip19 = Nip19Parser.uriToRoute(uri)?.entity
|
val nip19 = Nip19Parser.uriToRoute(uri)?.entity
|
||||||
when (nip19) {
|
when (nip19) {
|
||||||
is NPub -> "User/${nip19.hex}"
|
is NPub -> Route.Profile(nip19.hex)
|
||||||
is NProfile -> "User/${nip19.hex}"
|
is NProfile -> Route.Profile(nip19.hex)
|
||||||
is Note -> "Note/${nip19.hex}"
|
is Note -> Route.Note(nip19.hex)
|
||||||
is NEvent -> {
|
is NEvent -> {
|
||||||
if (nip19.kind == PrivateDmEvent.KIND) {
|
if (nip19.kind == PrivateDmEvent.KIND) {
|
||||||
nip19.author?.let { "RoomByAuthor/$it" }
|
nip19.author?.let { Route.RoomByAuthor(it) }
|
||||||
} else if (
|
} else if (
|
||||||
nip19.kind == ChannelMessageEvent.KIND ||
|
nip19.kind == ChannelMessageEvent.KIND ||
|
||||||
nip19.kind == ChannelCreateEvent.KIND ||
|
nip19.kind == ChannelCreateEvent.KIND ||
|
||||||
nip19.kind == ChannelMetadataEvent.KIND
|
nip19.kind == ChannelMetadataEvent.KIND
|
||||||
) {
|
) {
|
||||||
"Channel/${nip19.hex}"
|
Route.Channel(nip19.hex)
|
||||||
} else {
|
} else {
|
||||||
"Event/${nip19.hex}"
|
Route.EventRedirect(nip19.hex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is NAddress -> {
|
is NAddress -> {
|
||||||
if (nip19.kind == CommunityDefinitionEvent.KIND) {
|
if (nip19.kind == CommunityDefinitionEvent.KIND) {
|
||||||
"Community/${nip19.aTag()}"
|
Route.Community(nip19.aTag())
|
||||||
} else if (nip19.kind == LiveActivitiesEvent.KIND) {
|
} else if (nip19.kind == LiveActivitiesEvent.KIND) {
|
||||||
"Channel/${nip19.aTag()}"
|
Route.Channel(nip19.aTag())
|
||||||
} else {
|
} else {
|
||||||
"Event/${nip19.aTag()}"
|
Route.EventRedirect(nip19.aTag())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,7 +177,7 @@ fun uriToRoute(uri: String?): String? =
|
|||||||
if (LocalCache.getNoteIfExists(nip19.event.id) == null) {
|
if (LocalCache.getNoteIfExists(nip19.event.id) == null) {
|
||||||
LocalCache.verifyAndConsume(nip19.event, null)
|
LocalCache.verifyAndConsume(nip19.event, null)
|
||||||
}
|
}
|
||||||
"Event/${nip19.event.id}"
|
Route.EventRedirect(nip19.event.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> null
|
else -> null
|
||||||
@@ -301,8 +186,7 @@ fun uriToRoute(uri: String?): String? =
|
|||||||
?: try {
|
?: try {
|
||||||
uri?.let {
|
uri?.let {
|
||||||
Nip47WalletConnect.parse(it)
|
Nip47WalletConnect.parse(it)
|
||||||
val encodedUri = URLEncoder.encode(it, StandardCharsets.UTF_8.toString())
|
Route.Nip47NWCSetup(it)
|
||||||
Route.NIP47Setup.base + "?nip47=" + encodedUri
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e is CancellationException) throw e
|
if (e is CancellationException) throw e
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import android.util.LruCache
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache for stringResource because it seems to be > 1ms function in some phones
|
* Cache for stringResource because it seems to be > 1ms function in some phones
|
||||||
@@ -43,6 +44,19 @@ fun checkLanguage(currentLanguage: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StringResSetup() {
|
||||||
|
val config = LocalConfiguration.current
|
||||||
|
if (!config.locales.isEmpty) {
|
||||||
|
val language = config.locales.get(0).language
|
||||||
|
LifecycleResumeEffect(language) {
|
||||||
|
checkLanguage(language)
|
||||||
|
|
||||||
|
onPauseOrDispose { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun stringRes(id: Int): String = resourceCache.get(id) ?: stringResource(id).also { resourceCache.put(id, it) }
|
fun stringRes(id: Int): String = resourceCache.get(id) ?: stringResource(id).also { resourceCache.put(id, it) }
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -76,6 +76,7 @@ import com.vitorpamplona.amethyst.model.User
|
|||||||
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||||
import com.vitorpamplona.amethyst.ui.note.SearchIcon
|
import com.vitorpamplona.amethyst.ui.note.SearchIcon
|
||||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||||
@@ -349,7 +350,7 @@ private fun RenderSearchResults(
|
|||||||
key = { _, item -> "u" + item.pubkeyHex },
|
key = { _, item -> "u" + item.pubkeyHex },
|
||||||
) { _, item ->
|
) { _, item ->
|
||||||
UserComposeForChat(item, accountViewModel) {
|
UserComposeForChat(item, accountViewModel) {
|
||||||
accountViewModel.createChatRoomFor(item) { nav.nav("Room/$it") }
|
accountViewModel.createChatRoomFor(item) { nav.nav(Route.Room(it)) }
|
||||||
|
|
||||||
searchBarViewModel.clear()
|
searchBarViewModel.clear()
|
||||||
}
|
}
|
||||||
@@ -368,7 +369,7 @@ private fun RenderSearchResults(
|
|||||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||||
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
||||||
) {
|
) {
|
||||||
nav.nav("Channel/${item.idHex}")
|
nav.nav(Route.Channel(item.idHex))
|
||||||
searchBarViewModel.clear()
|
searchBarViewModel.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,10 +32,10 @@ import androidx.annotation.RequiresApi
|
|||||||
import androidx.core.net.toFile
|
import androidx.core.net.toFile
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.vitorpamplona.amethyst.BuildConfig
|
import com.vitorpamplona.amethyst.BuildConfig
|
||||||
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
|
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
import okhttp3.Callback
|
import okhttp3.Callback
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okio.BufferedSource
|
import okio.BufferedSource
|
||||||
@@ -49,7 +49,7 @@ import java.util.UUID
|
|||||||
object MediaSaverToDisk {
|
object MediaSaverToDisk {
|
||||||
fun saveDownloadingIfNeeded(
|
fun saveDownloadingIfNeeded(
|
||||||
videoUri: String?,
|
videoUri: String?,
|
||||||
forceProxy: Boolean,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
mimeType: String?,
|
mimeType: String?,
|
||||||
localContext: Context,
|
localContext: Context,
|
||||||
onSuccess: () -> Any?,
|
onSuccess: () -> Any?,
|
||||||
@@ -69,7 +69,7 @@ object MediaSaverToDisk {
|
|||||||
downloadAndSave(
|
downloadAndSave(
|
||||||
url = videoUri,
|
url = videoUri,
|
||||||
mimeType = mimeType,
|
mimeType = mimeType,
|
||||||
forceProxy = forceProxy,
|
okHttpClient = okHttpClient,
|
||||||
context = localContext,
|
context = localContext,
|
||||||
onSuccess = onSuccess,
|
onSuccess = onSuccess,
|
||||||
onError = onError,
|
onError = onError,
|
||||||
@@ -85,12 +85,12 @@ object MediaSaverToDisk {
|
|||||||
fun downloadAndSave(
|
fun downloadAndSave(
|
||||||
url: String,
|
url: String,
|
||||||
mimeType: String?,
|
mimeType: String?,
|
||||||
forceProxy: Boolean,
|
okHttpClient: (String) -> OkHttpClient,
|
||||||
context: Context,
|
context: Context,
|
||||||
onSuccess: () -> Any?,
|
onSuccess: () -> Any?,
|
||||||
onError: (Throwable) -> Any?,
|
onError: (Throwable) -> Any?,
|
||||||
) {
|
) {
|
||||||
val client = HttpClientManager.getHttpClient(forceProxy)
|
val client = okHttpClient(url)
|
||||||
|
|
||||||
val request =
|
val request =
|
||||||
Request
|
Request
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.actions
|
package com.vitorpamplona.amethyst.ui.actions
|
||||||
|
|
||||||
|
import android.R.attr.category
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
@@ -239,17 +240,27 @@ open class NewPostViewModel :
|
|||||||
|
|
||||||
fun user(): User? = account?.userProfile()
|
fun user(): User? = account?.userProfile()
|
||||||
|
|
||||||
|
open fun init(accountVM: AccountViewModel) {
|
||||||
|
this.accountViewModel = accountVM
|
||||||
|
this.account = accountVM.account
|
||||||
|
this.canAddInvoice = hasLnAddress()
|
||||||
|
this.canAddZapRaiser = hasLnAddress()
|
||||||
|
|
||||||
|
this.userSuggestions?.reset()
|
||||||
|
this.userSuggestions = UserSuggestionState(accountVM)
|
||||||
|
|
||||||
|
this.emojiSuggestions?.reset()
|
||||||
|
this.emojiSuggestions = EmojiSuggestionState(accountVM)
|
||||||
|
}
|
||||||
|
|
||||||
open fun load(
|
open fun load(
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
replyingTo: Note?,
|
replyingTo: Note?,
|
||||||
quote: Note?,
|
quote: Note?,
|
||||||
fork: Note?,
|
fork: Note?,
|
||||||
version: Note?,
|
version: Note?,
|
||||||
draft: Note?,
|
draft: Note?,
|
||||||
) {
|
) {
|
||||||
this.accountViewModel = accountViewModel
|
val accountViewModel = accountViewModel ?: return
|
||||||
this.account = accountViewModel.account
|
|
||||||
|
|
||||||
val noteEvent = draft?.event
|
val noteEvent = draft?.event
|
||||||
val noteAuthor = draft?.author
|
val noteAuthor = draft?.author
|
||||||
|
|
||||||
@@ -1181,7 +1192,7 @@ open class NewPostViewModel :
|
|||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
iMetaAttachments.downloadAndPrepare(
|
iMetaAttachments.downloadAndPrepare(
|
||||||
item.url.url,
|
item.url.url,
|
||||||
accountViewModel?.account?.shouldUseTorForImageDownload() ?: false,
|
{ Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload() ?: false) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -26,6 +26,8 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import coil3.util.CoilUtils.result
|
||||||
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.service.uploads.CompressorQuality
|
import com.vitorpamplona.amethyst.service.uploads.CompressorQuality
|
||||||
@@ -185,7 +187,7 @@ class NewUserMetadataViewModel : ViewModel() {
|
|||||||
alt = null,
|
alt = null,
|
||||||
sensitiveContent = null,
|
sensitiveContent = null,
|
||||||
serverBaseUrl = account.settings.defaultFileServer.baseUrl,
|
serverBaseUrl = account.settings.defaultFileServer.baseUrl,
|
||||||
forceProxy = account::shouldUseTorForNIP96,
|
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) },
|
||||||
onProgress = {},
|
onProgress = {},
|
||||||
httpAuth = account::createHTTPAuthorization,
|
httpAuth = account::createHTTPAuthorization,
|
||||||
context = context,
|
context = context,
|
||||||
@@ -198,7 +200,7 @@ class NewUserMetadataViewModel : ViewModel() {
|
|||||||
alt = null,
|
alt = null,
|
||||||
sensitiveContent = null,
|
sensitiveContent = null,
|
||||||
serverBaseUrl = account.settings.defaultFileServer.baseUrl,
|
serverBaseUrl = account.settings.defaultFileServer.baseUrl,
|
||||||
forceProxy = account::shouldUseTorForNIP96,
|
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) },
|
||||||
httpAuth = account::createBlossomUploadAuth,
|
httpAuth = account::createBlossomUploadAuth,
|
||||||
context = context,
|
context = context,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -53,18 +53,15 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.content.ContextCompat.startActivity
|
import androidx.core.content.ContextCompat.startActivity
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.commons.hashtags.Cashu
|
import com.vitorpamplona.amethyst.commons.hashtags.Cashu
|
||||||
import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons
|
import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons
|
||||||
import com.vitorpamplona.amethyst.model.ThemeType
|
|
||||||
import com.vitorpamplona.amethyst.service.CachedCashuProcessor
|
import com.vitorpamplona.amethyst.service.CachedCashuProcessor
|
||||||
import com.vitorpamplona.amethyst.service.CashuToken
|
import com.vitorpamplona.amethyst.service.CashuToken
|
||||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||||
import com.vitorpamplona.amethyst.ui.note.CopyIcon
|
import com.vitorpamplona.amethyst.ui.note.CopyIcon
|
||||||
import com.vitorpamplona.amethyst.ui.note.OpenInNewIcon
|
import com.vitorpamplona.amethyst.ui.note.OpenInNewIcon
|
||||||
import com.vitorpamplona.amethyst.ui.note.ZapIcon
|
import com.vitorpamplona.amethyst.ui.note.ZapIcon
|
||||||
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.CashuCardBorders
|
import com.vitorpamplona.amethyst.ui.theme.CashuCardBorders
|
||||||
@@ -124,11 +121,6 @@ fun CashuPreview(
|
|||||||
@Composable
|
@Composable
|
||||||
@Preview()
|
@Preview()
|
||||||
fun CashuPreviewPreview() {
|
fun CashuPreviewPreview() {
|
||||||
val sharedPreferencesViewModel: SharedPreferencesViewModel = viewModel()
|
|
||||||
|
|
||||||
sharedPreferencesViewModel.init()
|
|
||||||
sharedPreferencesViewModel.updateTheme(ThemeType.DARK)
|
|
||||||
|
|
||||||
ThemeComparisonColumn {
|
ThemeComparisonColumn {
|
||||||
CashuPreviewNew(
|
CashuPreviewNew(
|
||||||
token = CashuToken("token", "mint", 32400, listOf()),
|
token = CashuToken("token", "mint", 32400, listOf()),
|
||||||
|
|||||||
@@ -23,13 +23,8 @@ package com.vitorpamplona.amethyst.ui.components
|
|||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.compose.foundation.text.ClickableText
|
|
||||||
import androidx.compose.material3.LocalTextStyle
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -37,10 +32,9 @@ fun ClickableEmail(email: String) {
|
|||||||
val stripped = email.replaceFirst("mailto:", "")
|
val stripped = email.replaceFirst("mailto:", "")
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
ClickableText(
|
ClickableTextPrimary(
|
||||||
text = remember { AnnotatedString(stripped) },
|
text = stripped,
|
||||||
onClick = { runCatching { context.sendMail(stripped) } },
|
onClick = { runCatching { context.sendMail(stripped) } },
|
||||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,22 +23,16 @@ package com.vitorpamplona.amethyst.ui.components
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.foundation.text.ClickableText
|
|
||||||
import androidx.compose.material3.LocalTextStyle
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ClickablePhone(phone: String) {
|
fun ClickablePhone(phone: String) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
ClickableText(
|
ClickableTextPrimary(
|
||||||
text = remember { AnnotatedString(phone) },
|
text = phone,
|
||||||
onClick = { runCatching { context.dial(phone) } },
|
onClick = { context.dial(phone) },
|
||||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.components
|
package com.vitorpamplona.amethyst.ui.components
|
||||||
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.text.InlineTextContent
|
import androidx.compose.foundation.text.InlineTextContent
|
||||||
@@ -40,18 +39,18 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.takeOrElse
|
import androidx.compose.ui.graphics.takeOrElse
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.LinkAnnotation
|
||||||
import androidx.compose.ui.text.Placeholder
|
import androidx.compose.ui.text.Placeholder
|
||||||
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.TextLayoutResult
|
import androidx.compose.ui.text.TextLinkStyles
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.text.withLink
|
||||||
import androidx.compose.ui.text.withStyle
|
import androidx.compose.ui.text.withStyle
|
||||||
import androidx.compose.ui.unit.TextUnit
|
import androidx.compose.ui.unit.TextUnit
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -60,6 +59,8 @@ import coil3.compose.AsyncImage
|
|||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||||
import com.vitorpamplona.amethyst.ui.note.LoadChannel
|
import com.vitorpamplona.amethyst.ui.note.LoadChannel
|
||||||
import com.vitorpamplona.amethyst.ui.note.njumpLink
|
import com.vitorpamplona.amethyst.ui.note.njumpLink
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
@@ -200,7 +201,7 @@ private fun DisplayNoteLink(
|
|||||||
CreateClickableText(
|
CreateClickableText(
|
||||||
clickablePart = noteIdDisplayNote,
|
clickablePart = noteIdDisplayNote,
|
||||||
suffix = addedCharts,
|
suffix = addedCharts,
|
||||||
route = remember(noteState) { "Channel/$hex" },
|
route = remember(noteState) { Route.Channel(hex) },
|
||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
} else if (note.event is PrivateDmEvent || kind == PrivateDmEvent.KIND) {
|
} else if (note.event is PrivateDmEvent || kind == PrivateDmEvent.KIND) {
|
||||||
@@ -208,7 +209,7 @@ private fun DisplayNoteLink(
|
|||||||
clickablePart = noteIdDisplayNote,
|
clickablePart = noteIdDisplayNote,
|
||||||
suffix = addedCharts,
|
suffix = addedCharts,
|
||||||
route =
|
route =
|
||||||
remember(noteState) { (note.author?.pubkeyHex ?: hex).let { "RoomByAuthor/$it" } },
|
remember(noteState) { (note.author?.pubkeyHex ?: hex).let { Route.RoomByAuthor(it) } },
|
||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
} else if (channelHex != null) {
|
} else if (channelHex != null) {
|
||||||
@@ -222,7 +223,7 @@ private fun DisplayNoteLink(
|
|||||||
CreateClickableText(
|
CreateClickableText(
|
||||||
clickablePart = channelDisplayName,
|
clickablePart = channelDisplayName,
|
||||||
suffix = addedCharts,
|
suffix = addedCharts,
|
||||||
route = remember(noteState) { "Channel/${baseChannel.idHex}" },
|
route = remember(noteState) { Route.Channel(baseChannel.idHex) },
|
||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -230,7 +231,7 @@ private fun DisplayNoteLink(
|
|||||||
CreateClickableText(
|
CreateClickableText(
|
||||||
clickablePart = noteIdDisplayNote,
|
clickablePart = noteIdDisplayNote,
|
||||||
suffix = addedCharts,
|
suffix = addedCharts,
|
||||||
route = remember(noteState) { "Event/$hex" },
|
route = remember(noteState) { Route.EventRedirect(hex) },
|
||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -255,7 +256,7 @@ private fun DisplayAddress(
|
|||||||
noteBase?.let {
|
noteBase?.let {
|
||||||
val noteState by it.live().metadata.observeAsState()
|
val noteState by it.live().metadata.observeAsState()
|
||||||
|
|
||||||
val route = remember(noteState) { "Note/${nip19.aTag()}" }
|
val route = remember(noteState) { Route.Note(nip19.aTag()) }
|
||||||
val displayName = remember(noteState) { "@${noteState?.note?.idDisplayNote()}" }
|
val displayName = remember(noteState) { "@${noteState?.note?.idDisplayNote()}" }
|
||||||
|
|
||||||
CreateClickableText(
|
CreateClickableText(
|
||||||
@@ -329,7 +330,7 @@ public fun RenderUserAsClickableText(
|
|||||||
clickablePart = userState?.bestName() ?: ("@" + baseUser.pubkeyDisplayHex()),
|
clickablePart = userState?.bestName() ?: ("@" + baseUser.pubkeyDisplayHex()),
|
||||||
suffix = additionalChars?.ifBlank { null },
|
suffix = additionalChars?.ifBlank { null },
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
route = "User/${baseUser.pubkeyHex}",
|
route = remember(baseUser) { routeFor(baseUser) },
|
||||||
nav = nav,
|
nav = nav,
|
||||||
tags = userState?.tags ?: EmptyTagList,
|
tags = userState?.tags ?: EmptyTagList,
|
||||||
)
|
)
|
||||||
@@ -343,7 +344,7 @@ fun CreateClickableText(
|
|||||||
overrideColor: Color? = null,
|
overrideColor: Color? = null,
|
||||||
fontWeight: FontWeight? = null,
|
fontWeight: FontWeight? = null,
|
||||||
fontSize: TextUnit = TextUnit.Unspecified,
|
fontSize: TextUnit = TextUnit.Unspecified,
|
||||||
route: String,
|
route: Route,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
CreateClickableText(
|
CreateClickableText(
|
||||||
@@ -364,7 +365,7 @@ fun CreateClickableText(
|
|||||||
overrideColor: Color? = null,
|
overrideColor: Color? = null,
|
||||||
fontWeight: FontWeight? = null,
|
fontWeight: FontWeight? = null,
|
||||||
fontSize: TextUnit = TextUnit.Unspecified,
|
fontSize: TextUnit = TextUnit.Unspecified,
|
||||||
onClick: (Int) -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val primaryColor = MaterialTheme.colorScheme.primary
|
val primaryColor = MaterialTheme.colorScheme.primary
|
||||||
val onBackgroundColor = MaterialTheme.colorScheme.onBackground
|
val onBackgroundColor = MaterialTheme.colorScheme.onBackground
|
||||||
@@ -378,61 +379,34 @@ fun CreateClickableText(
|
|||||||
fontWeight = fontWeight,
|
fontWeight = fontWeight,
|
||||||
)
|
)
|
||||||
|
|
||||||
val nonClickablePartStyle =
|
|
||||||
SpanStyle(
|
|
||||||
fontSize = fontSize,
|
|
||||||
color = overrideColor ?: onBackgroundColor,
|
|
||||||
fontWeight = fontWeight,
|
|
||||||
)
|
|
||||||
|
|
||||||
buildAnnotatedString {
|
buildAnnotatedString {
|
||||||
withStyle(clickablePartStyle) { append(clickablePart) }
|
withLink(
|
||||||
if (!suffix.isNullOrBlank()) {
|
LinkAnnotation.Clickable(
|
||||||
withStyle(nonClickablePartStyle) { append(suffix) }
|
tag = "clickable",
|
||||||
|
styles = TextLinkStyles(clickablePartStyle),
|
||||||
|
) {
|
||||||
|
onClick()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
append(clickablePart)
|
||||||
}
|
}
|
||||||
}
|
if (!suffix.isNullOrBlank()) {
|
||||||
}
|
val nonClickablePartStyle =
|
||||||
|
SpanStyle(
|
||||||
|
fontSize = fontSize,
|
||||||
|
color = overrideColor ?: onBackgroundColor,
|
||||||
|
fontWeight = fontWeight,
|
||||||
|
)
|
||||||
|
|
||||||
ClickableText(
|
withStyle(nonClickablePartStyle) { append(suffix) }
|
||||||
text = text,
|
|
||||||
maxLines = maxLines,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
onClick = onClick,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ClickableText(
|
|
||||||
text: AnnotatedString,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
style: TextStyle = LocalTextStyle.current,
|
|
||||||
softWrap: Boolean = true,
|
|
||||||
overflow: TextOverflow = TextOverflow.Clip,
|
|
||||||
maxLines: Int = Int.MAX_VALUE,
|
|
||||||
onTextLayout: (TextLayoutResult) -> Unit = {},
|
|
||||||
onClick: (Int) -> Unit,
|
|
||||||
) {
|
|
||||||
val layoutResult = remember { mutableStateOf<TextLayoutResult?>(null) }
|
|
||||||
val pressIndicator =
|
|
||||||
Modifier.pointerInput(onClick) {
|
|
||||||
detectTapGestures { pos ->
|
|
||||||
layoutResult.value?.let { layoutResult ->
|
|
||||||
onClick(layoutResult.getOffsetForPosition(pos))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
modifier = modifier.then(pressIndicator),
|
|
||||||
style = style,
|
|
||||||
softWrap = softWrap,
|
|
||||||
overflow = overflow,
|
|
||||||
maxLines = maxLines,
|
maxLines = maxLines,
|
||||||
onTextLayout = {
|
overflow = TextOverflow.Ellipsis,
|
||||||
layoutResult.value = it
|
|
||||||
onTextLayout(it)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -608,21 +582,29 @@ fun CreateClickableTextWithEmoji(
|
|||||||
maxLines: Int = Int.MAX_VALUE,
|
maxLines: Int = Int.MAX_VALUE,
|
||||||
tags: ImmutableListOfLists<String>?,
|
tags: ImmutableListOfLists<String>?,
|
||||||
style: TextStyle,
|
style: TextStyle,
|
||||||
onClick: (Int) -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
CustomEmojiChecker(
|
CustomEmojiChecker(
|
||||||
text = clickablePart,
|
text = clickablePart,
|
||||||
tags = tags,
|
tags = tags,
|
||||||
onRegularText = {
|
onRegularText = {
|
||||||
ClickableText(
|
Text(
|
||||||
text = AnnotatedString(clickablePart),
|
text =
|
||||||
|
buildAnnotatedString {
|
||||||
|
withLink(
|
||||||
|
LinkAnnotation.Clickable("me") {
|
||||||
|
onClick()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
append(clickablePart)
|
||||||
|
}
|
||||||
|
},
|
||||||
style = style,
|
style = style,
|
||||||
maxLines = maxLines,
|
maxLines = maxLines,
|
||||||
onClick = onClick,
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onEmojiText = {
|
onEmojiText = {
|
||||||
ClickableInLineIconRenderer(it, maxLines, style.toSpanStyle()) { onClick(it) }
|
ClickableInLineIconRenderer(it, maxLines, style.toSpanStyle(), onClick = onClick)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -635,7 +617,7 @@ fun CreateClickableTextWithEmoji(
|
|||||||
overrideColor: Color? = null,
|
overrideColor: Color? = null,
|
||||||
fontWeight: FontWeight = FontWeight.Normal,
|
fontWeight: FontWeight = FontWeight.Normal,
|
||||||
fontSize: TextUnit = TextUnit.Unspecified,
|
fontSize: TextUnit = TextUnit.Unspecified,
|
||||||
route: String,
|
route: Route,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
tags: ImmutableListOfLists<String>?,
|
tags: ImmutableListOfLists<String>?,
|
||||||
) {
|
) {
|
||||||
@@ -680,15 +662,11 @@ fun ClickableInLineIconRenderer(
|
|||||||
style: SpanStyle,
|
style: SpanStyle,
|
||||||
suffix: String? = null,
|
suffix: String? = null,
|
||||||
nonClickableStype: SpanStyle? = null,
|
nonClickableStype: SpanStyle? = null,
|
||||||
onClick: (Int) -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val placeholderSize =
|
val placeholderSize =
|
||||||
remember(style) {
|
remember(style) {
|
||||||
if (style.fontSize == TextUnit.Unspecified) {
|
if (style.fontSize == TextUnit.Unspecified) 22.sp else style.fontSize.times(1.1f)
|
||||||
22.sp
|
|
||||||
} else {
|
|
||||||
style.fontSize.times(1.1f)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val inlineContent =
|
val inlineContent =
|
||||||
@@ -722,8 +700,10 @@ fun ClickableInLineIconRenderer(
|
|||||||
val annotatedText =
|
val annotatedText =
|
||||||
buildAnnotatedString {
|
buildAnnotatedString {
|
||||||
wordsInOrder.forEachIndexed { idx, value ->
|
wordsInOrder.forEachIndexed { idx, value ->
|
||||||
withStyle(
|
withLink(
|
||||||
style,
|
LinkAnnotation.Clickable("link", TextLinkStyles(style)) {
|
||||||
|
onClick()
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
if (value is CustomEmoji.TextType) {
|
if (value is CustomEmoji.TextType) {
|
||||||
append(value.text)
|
append(value.text)
|
||||||
@@ -740,20 +720,10 @@ fun ClickableInLineIconRenderer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val layoutResult = remember { mutableStateOf<TextLayoutResult?>(null) }
|
|
||||||
val pressIndicator =
|
|
||||||
Modifier.pointerInput(onClick) {
|
|
||||||
detectTapGestures { pos ->
|
|
||||||
layoutResult.value?.let { layoutResult -> onClick(layoutResult.getOffsetForPosition(pos)) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = annotatedText,
|
text = annotatedText,
|
||||||
modifier = pressIndicator,
|
|
||||||
inlineContent = inlineContent,
|
inlineContent = inlineContent,
|
||||||
maxLines = maxLines,
|
maxLines = maxLines,
|
||||||
onTextLayout = { layoutResult.value = it },
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -768,11 +738,7 @@ fun InLineIconRenderer(
|
|||||||
) {
|
) {
|
||||||
val placeholderSize =
|
val placeholderSize =
|
||||||
remember(fontSize) {
|
remember(fontSize) {
|
||||||
if (fontSize == TextUnit.Unspecified) {
|
if (fontSize == TextUnit.Unspecified) 22.sp else fontSize.times(1.1f)
|
||||||
22.sp
|
|
||||||
} else {
|
|
||||||
fontSize.times(1.1f)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val inlineContent =
|
val inlineContent =
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.AnnotatedString.Builder
|
||||||
|
import androidx.compose.ui.text.LinkAnnotation
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.TextLinkStyles
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.text.withLink
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ClickableTextPrimary(
|
||||||
|
text: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
style: TextStyle = LocalTextStyle.current,
|
||||||
|
softWrap: Boolean = true,
|
||||||
|
overflow: TextOverflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines: Int = Int.MAX_VALUE,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
ClickableTextColor(
|
||||||
|
text,
|
||||||
|
modifier,
|
||||||
|
style,
|
||||||
|
softWrap,
|
||||||
|
overflow,
|
||||||
|
maxLines,
|
||||||
|
MaterialTheme.colorScheme.primary,
|
||||||
|
onClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ClickableTextColor(
|
||||||
|
text: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
style: TextStyle = LocalTextStyle.current,
|
||||||
|
softWrap: Boolean = true,
|
||||||
|
overflow: TextOverflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines: Int = Int.MAX_VALUE,
|
||||||
|
linkColor: Color = MaterialTheme.colorScheme.primary,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text =
|
||||||
|
remember(text) {
|
||||||
|
buildAnnotatedString {
|
||||||
|
appendLink(text, linkColor, onClick)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
style = style,
|
||||||
|
softWrap = softWrap,
|
||||||
|
overflow = overflow,
|
||||||
|
maxLines = maxLines,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ClickableTextNormal(
|
||||||
|
text: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
style: TextStyle = LocalTextStyle.current,
|
||||||
|
softWrap: Boolean = true,
|
||||||
|
overflow: TextOverflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines: Int = Int.MAX_VALUE,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text =
|
||||||
|
remember(text) {
|
||||||
|
buildAnnotatedString {
|
||||||
|
appendLink(text, onClick)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
style = style,
|
||||||
|
softWrap = softWrap,
|
||||||
|
overflow = overflow,
|
||||||
|
maxLines = maxLines,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun Builder.appendLink(
|
||||||
|
text: String,
|
||||||
|
color: Color,
|
||||||
|
crossinline onClick: () -> Unit,
|
||||||
|
) = withLink(
|
||||||
|
LinkAnnotation.Clickable(
|
||||||
|
"clickable",
|
||||||
|
TextLinkStyles(SpanStyle(color)),
|
||||||
|
) {
|
||||||
|
onClick()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
append(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun Builder.appendLink(
|
||||||
|
text: String,
|
||||||
|
crossinline onClick: () -> Unit,
|
||||||
|
) = withLink(
|
||||||
|
LinkAnnotation.Clickable("clickable") {
|
||||||
|
onClick()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
append(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun buildLinkString(
|
||||||
|
text: String,
|
||||||
|
crossinline onClick: () -> Unit,
|
||||||
|
): AnnotatedString =
|
||||||
|
buildAnnotatedString {
|
||||||
|
withLink(
|
||||||
|
LinkAnnotation.Clickable("link") {
|
||||||
|
onClick()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
append(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,13 +20,10 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.components
|
package com.vitorpamplona.amethyst.ui.components
|
||||||
|
|
||||||
import androidx.compose.foundation.text.ClickableText
|
import android.R.attr.maxLines
|
||||||
import androidx.compose.material3.LocalTextStyle
|
import android.R.attr.onClick
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -36,10 +33,8 @@ fun ClickableUrl(
|
|||||||
) {
|
) {
|
||||||
val uri = LocalUriHandler.current
|
val uri = LocalUriHandler.current
|
||||||
|
|
||||||
val text = remember(urlText) { AnnotatedString(urlText) }
|
ClickableTextPrimary(
|
||||||
|
text = urlText,
|
||||||
ClickableText(
|
|
||||||
text = text,
|
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -48,6 +43,5 @@ fun ClickableUrl(
|
|||||||
uri.openUri(doubleCheckedUrl)
|
uri.openUri(doubleCheckedUrl)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-8
@@ -20,9 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.components
|
package com.vitorpamplona.amethyst.ui.components
|
||||||
|
|
||||||
import androidx.compose.foundation.text.ClickableText
|
|
||||||
import androidx.compose.material3.LocalTextStyle
|
import androidx.compose.material3.LocalTextStyle
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -31,7 +29,6 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.text.style.TextDirection
|
import androidx.compose.ui.text.style.TextDirection
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||||
@@ -70,8 +67,6 @@ fun MayBeWithdrawal(
|
|||||||
fun ClickableWithdrawal(withdrawalString: String) {
|
fun ClickableWithdrawal(withdrawalString: String) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val withdraw = remember(withdrawalString) { AnnotatedString("$withdrawalString ") }
|
|
||||||
|
|
||||||
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) }
|
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
if (showErrorMessageDialog != null) {
|
if (showErrorMessageDialog != null) {
|
||||||
@@ -82,9 +77,8 @@ fun ClickableWithdrawal(withdrawalString: String) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ClickableText(
|
ClickableTextPrimary(
|
||||||
text = withdraw,
|
text = "$withdrawalString ",
|
||||||
onClick = { payViaIntent(withdrawalString, context, { }) { showErrorMessageDialog = it } },
|
onClick = { payViaIntent(withdrawalString, context, { }) { showErrorMessageDialog = it } },
|
||||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
|||||||
import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown
|
import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
|
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.invoice.MayBeInvoicePreview
|
import com.vitorpamplona.amethyst.ui.note.creators.invoice.MayBeInvoicePreview
|
||||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||||
@@ -183,7 +185,7 @@ fun RenderRegularPreview() {
|
|||||||
word.segmentText.substring(0, 10),
|
word.segmentText.substring(0, 10),
|
||||||
"",
|
"",
|
||||||
1,
|
1,
|
||||||
route = "",
|
route = Route.EventRedirect(word.segmentText),
|
||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -650,7 +652,7 @@ fun HashTag(
|
|||||||
modifier =
|
modifier =
|
||||||
remember {
|
remember {
|
||||||
Modifier.clickable {
|
Modifier.clickable {
|
||||||
nav.nav("Hashtag/${segment.hashtag}")
|
nav.nav(Route.Hashtag(segment.hashtag))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
inlineContent =
|
inlineContent =
|
||||||
@@ -760,7 +762,10 @@ private fun DisplayNoteFromTag(
|
|||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
ClickableNoteTag(baseNote, accountViewModel, nav)
|
ClickableTextPrimary(
|
||||||
|
text = "@${baseNote.idNote().toShortenHex()}",
|
||||||
|
onClick = { routeFor(baseNote, accountViewModel.userProfile())?.let { nav.nav(it) } },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
addedChars?.ifBlank { null }?.let { Text(text = it) }
|
addedChars?.ifBlank { null }?.let { Text(text = it) }
|
||||||
@@ -779,7 +784,7 @@ private fun DisplayUserFromTag(
|
|||||||
CreateClickableTextWithEmoji(
|
CreateClickableTextWithEmoji(
|
||||||
clickablePart = remember(meta) { it?.bestName() ?: baseUser.pubkeyDisplayHex() },
|
clickablePart = remember(meta) { it?.bestName() ?: baseUser.pubkeyDisplayHex() },
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
route = "User/${baseUser.pubkeyHex}",
|
route = remember(baseUser) { routeFor(baseUser) },
|
||||||
nav = nav,
|
nav = nav,
|
||||||
tags = it?.tags,
|
tags = it?.tags,
|
||||||
)
|
)
|
||||||
|
|||||||
+7
-8
@@ -308,17 +308,16 @@ private fun saveMediaToGallery(
|
|||||||
val failure = if (isImage) R.string.failed_to_save_the_image else R.string.failed_to_save_the_video
|
val failure = if (isImage) R.string.failed_to_save_the_image else R.string.failed_to_save_the_video
|
||||||
|
|
||||||
if (content is MediaUrlContent) {
|
if (content is MediaUrlContent) {
|
||||||
val useTor =
|
|
||||||
if (isImage) {
|
|
||||||
accountViewModel.account.shouldUseTorForImageDownload()
|
|
||||||
} else {
|
|
||||||
accountViewModel.account.shouldUseTorForVideoDownload()
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaSaverToDisk.downloadAndSave(
|
MediaSaverToDisk.downloadAndSave(
|
||||||
content.url,
|
content.url,
|
||||||
mimeType = content.mimeType,
|
mimeType = content.mimeType,
|
||||||
forceProxy = useTor,
|
okHttpClient = {
|
||||||
|
if (isImage) {
|
||||||
|
accountViewModel.okHttpClientForImage(it)
|
||||||
|
} else {
|
||||||
|
accountViewModel.okHttpClientForVideo(it)
|
||||||
|
}
|
||||||
|
},
|
||||||
localContext,
|
localContext,
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
accountViewModel.toastManager.toast(success, success)
|
accountViewModel.toastManager.toast(success, success)
|
||||||
|
|||||||
+3
-3
@@ -79,7 +79,7 @@ import com.vitorpamplona.amethyst.commons.richtext.MediaUrlContent
|
|||||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
|
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
|
||||||
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
|
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
|
||||||
import com.vitorpamplona.amethyst.service.Blurhash
|
import com.vitorpamplona.amethyst.service.images.BlurhashWrapper
|
||||||
import com.vitorpamplona.amethyst.service.playback.composable.VideoView
|
import com.vitorpamplona.amethyst.service.playback.composable.VideoView
|
||||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||||
import com.vitorpamplona.amethyst.ui.actions.InformationDialog
|
import com.vitorpamplona.amethyst.ui.actions.InformationDialog
|
||||||
@@ -637,7 +637,7 @@ fun DisplayBlurHash(
|
|||||||
if (blurhash == null) return
|
if (blurhash == null) return
|
||||||
|
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = Blurhash(blurhash),
|
model = BlurhashWrapper(blurhash),
|
||||||
contentDescription = description,
|
contentDescription = description,
|
||||||
contentScale = contentScale,
|
contentScale = contentScale,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
@@ -739,7 +739,7 @@ fun ShareImageAction(
|
|||||||
private suspend fun verifyHash(content: MediaUrlContent): Boolean? {
|
private suspend fun verifyHash(content: MediaUrlContent): Boolean? {
|
||||||
if (content.hash == null) return null
|
if (content.hash == null) return null
|
||||||
|
|
||||||
Amethyst.instance.coilCache.openSnapshot(content.url)?.use { snapshot ->
|
Amethyst.instance.diskCache.openSnapshot(content.url)?.use { snapshot ->
|
||||||
val hash = sha256(snapshot.data.toFile().readBytes()).toHexKey()
|
val hash = sha256(snapshot.data.toFile().readBytes()).toHexKey()
|
||||||
|
|
||||||
Log.d("Image Hash Verification", "$hash == ${content.hash}")
|
Log.d("Image Hash Verification", "$hash == ${content.hash}")
|
||||||
|
|||||||
+9
-10
@@ -27,7 +27,6 @@ import androidx.compose.foundation.pager.rememberPagerState
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
private val savedScrollStates = mutableMapOf<String, ScrollState>()
|
private val savedScrollStates = mutableMapOf<String, ScrollState>()
|
||||||
@@ -40,17 +39,17 @@ private data class ScrollState(
|
|||||||
object ScrollStateKeys {
|
object ScrollStateKeys {
|
||||||
const val NOTIFICATION_SCREEN = "NotificationsFeed"
|
const val NOTIFICATION_SCREEN = "NotificationsFeed"
|
||||||
const val VIDEO_SCREEN = "VideoFeed"
|
const val VIDEO_SCREEN = "VideoFeed"
|
||||||
val HOME_FOLLOWS = Route.Home.base + "FollowsFeed"
|
const val HOME_FOLLOWS = "HomeFollowsFeed"
|
||||||
val HOME_REPLIES = Route.Home.base + "FollowsRepliesFeed"
|
const val HOME_REPLIES = "HomeFollowsRepliesFeed"
|
||||||
val PROFILE_GALLERY = Route.Home.base + "ProfileGalleryFeed"
|
const val PROFILE_GALLERY = "ProfileGalleryFeed"
|
||||||
|
|
||||||
val DRAFTS = Route.Home.base + "DraftsFeed"
|
const val DRAFTS = "DraftsFeed"
|
||||||
|
|
||||||
val DISCOVER_CONTENT = Route.Home.base + "DiscoverContentFeed"
|
const val DISCOVER_CONTENT = "DiscoverDiscoverContentFeed"
|
||||||
val DISCOVER_MARKETPLACE = Route.Home.base + "MarketplaceFeed"
|
const val DISCOVER_MARKETPLACE = "DiscoverMarketplaceFeed"
|
||||||
val DISCOVER_LIVE = Route.Home.base + "LiveFeed"
|
const val DISCOVER_LIVE = "DiscoverLiveFeed"
|
||||||
val DISCOVER_COMMUNITY = Route.Home.base + "CommunitiesFeed"
|
const val DISCOVER_COMMUNITY = "DiscoverCommunitiesFeed"
|
||||||
val DISCOVER_CHATS = Route.Home.base + "ChatsFeed"
|
const val DISCOVER_CHATS = "DiscoverChatsFeed"
|
||||||
}
|
}
|
||||||
|
|
||||||
object PagerStateKeys {
|
object PagerStateKeys {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import androidx.compose.foundation.layout.RowScope
|
|||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.material3.BottomAppBarDefaults.windowInsets
|
import androidx.compose.material3.BottomAppBarDefaults.windowInsets
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
@@ -50,20 +51,23 @@ import androidx.compose.ui.platform.LocalView
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size0dp
|
import com.vitorpamplona.amethyst.ui.theme.Size0dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size10Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size10Modifier
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size24dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
val bottomNavigationItems =
|
val bottomNavigationItems =
|
||||||
persistentListOf(
|
persistentListOf(
|
||||||
Route.Home,
|
BottomBarRoute(Route.Home, R.drawable.ic_home, R.string.route_home, Modifier.size(Size25dp), Modifier.size(Size24dp)),
|
||||||
Route.Message,
|
BottomBarRoute(Route.Message, R.drawable.ic_dm, R.string.route_messages),
|
||||||
Route.Video,
|
BottomBarRoute(Route.Video, R.drawable.ic_video, R.string.route_video),
|
||||||
Route.Discover,
|
BottomBarRoute(Route.Discover, R.drawable.ic_sensors, R.string.route_discover),
|
||||||
Route.Notification,
|
BottomBarRoute(Route.Notification, R.drawable.ic_notifications, R.string.route_notifications),
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class Keyboard {
|
enum class Keyboard {
|
||||||
@@ -119,7 +123,7 @@ fun IfKeyboardClosed(inner: @Composable () -> Unit) {
|
|||||||
fun AppBottomBar(
|
fun AppBottomBar(
|
||||||
selectedRoute: Route?,
|
selectedRoute: Route?,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (Route, Boolean) -> Unit,
|
nav: (Route) -> Unit,
|
||||||
) {
|
) {
|
||||||
IfKeyboardClosed { RenderBottomMenu(selectedRoute, accountViewModel, nav) }
|
IfKeyboardClosed { RenderBottomMenu(selectedRoute, accountViewModel, nav) }
|
||||||
}
|
}
|
||||||
@@ -128,7 +132,7 @@ fun AppBottomBar(
|
|||||||
private fun RenderBottomMenu(
|
private fun RenderBottomMenu(
|
||||||
selectedRoute: Route?,
|
selectedRoute: Route?,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (Route, Boolean) -> Unit,
|
nav: (Route) -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
@@ -146,7 +150,7 @@ private fun RenderBottomMenu(
|
|||||||
tonalElevation = Size0dp,
|
tonalElevation = Size0dp,
|
||||||
) {
|
) {
|
||||||
bottomNavigationItems.forEach { item ->
|
bottomNavigationItems.forEach { item ->
|
||||||
HasNewItemsIcon(item == selectedRoute, item, accountViewModel, nav)
|
HasNewItemsIcon(item.route == selectedRoute, item, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,28 +159,28 @@ private fun RenderBottomMenu(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun RowScope.HasNewItemsIcon(
|
private fun RowScope.HasNewItemsIcon(
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
route: Route,
|
bottomNav: BottomBarRoute,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (Route, Boolean) -> Unit,
|
nav: (Route) -> Unit,
|
||||||
) {
|
) {
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
alwaysShowLabel = false,
|
alwaysShowLabel = false,
|
||||||
icon = {
|
icon = {
|
||||||
NotifiableIcon(
|
NotifiableIcon(
|
||||||
selected,
|
selected,
|
||||||
route,
|
bottomNav,
|
||||||
accountViewModel,
|
accountViewModel,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
selected = selected,
|
selected = selected,
|
||||||
onClick = { nav(route, selected) },
|
onClick = { nav(bottomNav.route) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NotifiableIcon(
|
private fun NotifiableIcon(
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
route: Route,
|
route: BottomBarRoute,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
) {
|
) {
|
||||||
Box(route.notifSize) {
|
Box(route.notifSize) {
|
||||||
@@ -187,7 +191,7 @@ private fun NotifiableIcon(
|
|||||||
tint = if (selected) MaterialTheme.colorScheme.primary else Color.Unspecified,
|
tint = if (selected) MaterialTheme.colorScheme.primary else Color.Unspecified,
|
||||||
)
|
)
|
||||||
|
|
||||||
AddNotifIconIfNeeded(route, accountViewModel, Modifier.align(Alignment.TopEnd))
|
AddNotifIconIfNeeded(route.route, accountViewModel, Modifier.align(Alignment.TopEnd))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.core.util.Consumer
|
import androidx.core.util.Consumer
|
||||||
import androidx.navigation.NavBackStackEntry
|
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
@@ -87,24 +86,6 @@ import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.net.URLDecoder
|
|
||||||
|
|
||||||
fun NavBackStackEntry.id(): String? = arguments?.getString("id")
|
|
||||||
|
|
||||||
fun NavBackStackEntry.message(): String? =
|
|
||||||
arguments?.getString("message")?.let {
|
|
||||||
URLDecoder.decode(it, "utf-8")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun NavBackStackEntry.replyId(): String? =
|
|
||||||
arguments?.getString("replyId")?.let {
|
|
||||||
URLDecoder.decode(it, "utf-8")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun NavBackStackEntry.draftId(): String? =
|
|
||||||
arguments?.getString("draftId")?.let {
|
|
||||||
URLDecoder.decode(it, "utf-8")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppNavigation(
|
fun AppNavigation(
|
||||||
@@ -117,34 +98,15 @@ fun AppNavigation(
|
|||||||
AccountSwitcherAndLeftDrawerLayout(accountViewModel, accountStateViewModel, nav) {
|
AccountSwitcherAndLeftDrawerLayout(accountViewModel, accountStateViewModel, nav) {
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = nav.controller,
|
navController = nav.controller,
|
||||||
startDestination = Route.Home.route,
|
startDestination = Route.Home,
|
||||||
enterTransition = { fadeIn(animationSpec = tween(200)) },
|
enterTransition = { fadeIn(animationSpec = tween(200)) },
|
||||||
exitTransition = { fadeOut(animationSpec = tween(200)) },
|
exitTransition = { fadeOut(animationSpec = tween(200)) },
|
||||||
) {
|
) {
|
||||||
composable(Route.Home.route) { HomeScreen(accountViewModel, nav) }
|
composable<Route.Home> { HomeScreen(accountViewModel, nav) }
|
||||||
composable(Route.Message.route) { MessagesScreen(accountViewModel, nav) }
|
composable<Route.Message> { MessagesScreen(accountViewModel, nav) }
|
||||||
composable(Route.Video.route) { VideoScreen(accountViewModel, nav) }
|
composable<Route.Video> { VideoScreen(accountViewModel, nav) }
|
||||||
composable(Route.Discover.route) { DiscoverScreen(accountViewModel, nav) }
|
composable<Route.Discover> { DiscoverScreen(accountViewModel, nav) }
|
||||||
composable(Route.Notification.route) { NotificationScreen(sharedPreferencesViewModel, accountViewModel, nav) }
|
composable<Route.Notification> { NotificationScreen(sharedPreferencesViewModel, accountViewModel, nav) }
|
||||||
composable(Route.EditProfile.route) { NewUserMetadataScreen(nav, accountViewModel) }
|
|
||||||
|
|
||||||
composable(Route.Search.route) { SearchScreen(accountViewModel, nav) }
|
|
||||||
|
|
||||||
composable(
|
|
||||||
Route.BlockedUsers.route,
|
|
||||||
enterTransition = { slideInHorizontallyFromEnd },
|
|
||||||
exitTransition = { scaleOut },
|
|
||||||
popEnterTransition = { scaleIn },
|
|
||||||
popExitTransition = { slideOutHorizontallyToEnd },
|
|
||||||
) { SecurityFiltersScreen(accountViewModel, nav) }
|
|
||||||
|
|
||||||
composable(
|
|
||||||
Route.Bookmarks.route,
|
|
||||||
enterTransition = { slideInHorizontallyFromEnd },
|
|
||||||
exitTransition = { scaleOut },
|
|
||||||
popEnterTransition = { scaleIn },
|
|
||||||
popExitTransition = { slideOutHorizontallyToEnd },
|
|
||||||
) { BookmarkListScreen(accountViewModel, nav) }
|
|
||||||
|
|
||||||
composable(
|
composable(
|
||||||
Route.Lists.route,
|
Route.Lists.route,
|
||||||
@@ -154,245 +116,42 @@ fun AppNavigation(
|
|||||||
popExitTransition = { slideOutHorizontallyToEnd },
|
popExitTransition = { slideOutHorizontallyToEnd },
|
||||||
) { ListsScreen(accountViewModel, nav) }
|
) { ListsScreen(accountViewModel, nav) }
|
||||||
|
|
||||||
composable(
|
|
||||||
Route.Drafts.route,
|
|
||||||
enterTransition = { slideInHorizontallyFromEnd },
|
|
||||||
exitTransition = { scaleOut },
|
|
||||||
popEnterTransition = { scaleIn },
|
|
||||||
popExitTransition = { slideOutHorizontallyToEnd },
|
|
||||||
) { DraftListScreen(accountViewModel, nav) }
|
|
||||||
|
|
||||||
composable(
|
composable<Route.EditProfile> { NewUserMetadataScreen(nav, accountViewModel) }
|
||||||
Route.ContentDiscovery.route,
|
composable<Route.Search> { SearchScreen(accountViewModel, nav) }
|
||||||
Route.ContentDiscovery.arguments,
|
|
||||||
enterTransition = { slideInHorizontallyFromEnd },
|
|
||||||
exitTransition = { scaleOut },
|
|
||||||
popEnterTransition = { scaleIn },
|
|
||||||
popExitTransition = { slideOutHorizontallyToEnd },
|
|
||||||
) {
|
|
||||||
DvmContentDiscoveryScreen(it.id(), accountViewModel, nav)
|
|
||||||
}
|
|
||||||
|
|
||||||
composable(
|
composableFromEnd<Route.SecurityFilters> { SecurityFiltersScreen(accountViewModel, nav) }
|
||||||
Route.Profile.route,
|
composableFromEnd<Route.Bookmarks> { BookmarkListScreen(accountViewModel, nav) }
|
||||||
Route.Profile.arguments,
|
composableFromEnd<Route.Drafts> { DraftListScreen(accountViewModel, nav) }
|
||||||
enterTransition = { slideInHorizontallyFromEnd },
|
composableFromEnd<Route.Settings> { SettingsScreen(sharedPreferencesViewModel, accountViewModel, nav) }
|
||||||
exitTransition = { scaleOut },
|
composableFromBottomArgs<Route.Nip47NWCSetup> { NIP47SetupScreen(accountViewModel, nav, it.nip47) }
|
||||||
popEnterTransition = { scaleIn },
|
composableFromEndArgs<Route.EditRelays> { AllRelayListScreen(it.toAdd, accountViewModel, nav) }
|
||||||
popExitTransition = { slideOutHorizontallyToEnd },
|
|
||||||
) {
|
|
||||||
ProfileScreen(it.id(), accountViewModel, nav)
|
|
||||||
}
|
|
||||||
|
|
||||||
composable(
|
composableFromEndArgs<Route.ContentDiscovery> { DvmContentDiscoveryScreen(it.id, accountViewModel, nav) }
|
||||||
Route.Note.route,
|
composableFromEndArgs<Route.Profile> { ProfileScreen(it.id, accountViewModel, nav) }
|
||||||
Route.Note.arguments,
|
composableFromEndArgs<Route.Note> { ThreadScreen(it.id, accountViewModel, nav) }
|
||||||
enterTransition = { slideInHorizontallyFromEnd },
|
composableFromEndArgs<Route.Hashtag> { HashtagScreen(it.id, accountViewModel, nav) }
|
||||||
exitTransition = { scaleOut },
|
composableFromEndArgs<Route.Geohash> { GeoHashScreen(it.id, accountViewModel, nav) }
|
||||||
popEnterTransition = { scaleIn },
|
composableFromEndArgs<Route.Community> { CommunityScreen(it.id, accountViewModel, nav) }
|
||||||
popExitTransition = { slideOutHorizontallyToEnd },
|
composableFromEndArgs<Route.Room> { ChatroomScreen(it.id.toString(), it.message, it.replyId, it.draftId, accountViewModel, nav) }
|
||||||
) {
|
composableFromEndArgs<Route.RoomByAuthor> { ChatroomByAuthorScreen(it.id, null, accountViewModel, nav) }
|
||||||
ThreadScreen(it.id(), accountViewModel, nav)
|
composableFromEndArgs<Route.Channel> { ChannelScreen(it.id, accountViewModel, nav) }
|
||||||
}
|
|
||||||
|
|
||||||
composable(
|
composableFromBottomArgs<Route.ChannelMetadataEdit> { ChannelMetadataScreen(it.id, accountViewModel, nav) }
|
||||||
Route.Hashtag.route,
|
composableFromBottomArgs<Route.NewGroupDM> { NewGroupDMScreen(it.message, it.attachment, accountViewModel, nav) }
|
||||||
Route.Hashtag.arguments,
|
|
||||||
enterTransition = { slideInHorizontallyFromEnd },
|
|
||||||
exitTransition = { scaleOut },
|
|
||||||
popEnterTransition = { scaleIn },
|
|
||||||
popExitTransition = { slideOutHorizontallyToEnd },
|
|
||||||
) {
|
|
||||||
HashtagScreen(it.id(), accountViewModel, nav)
|
|
||||||
}
|
|
||||||
|
|
||||||
composable(
|
composableArgs<Route.EventRedirect> { LoadRedirectScreen(it.id, accountViewModel, nav) }
|
||||||
Route.Geohash.route,
|
|
||||||
Route.Geohash.arguments,
|
|
||||||
enterTransition = { slideInHorizontallyFromEnd },
|
|
||||||
exitTransition = { scaleOut },
|
|
||||||
popEnterTransition = { scaleIn },
|
|
||||||
popExitTransition = { slideOutHorizontallyToEnd },
|
|
||||||
) {
|
|
||||||
GeoHashScreen(it.id(), accountViewModel, nav)
|
|
||||||
}
|
|
||||||
|
|
||||||
composable(
|
|
||||||
Route.Community.route,
|
|
||||||
Route.Community.arguments,
|
|
||||||
enterTransition = { slideInHorizontallyFromEnd },
|
|
||||||
exitTransition = { scaleOut },
|
|
||||||
popEnterTransition = { scaleIn },
|
|
||||||
popExitTransition = { slideOutHorizontallyToEnd },
|
|
||||||
) {
|
|
||||||
CommunityScreen(it.id(), accountViewModel, nav)
|
|
||||||
}
|
|
||||||
|
|
||||||
composable(
|
|
||||||
Route.Room.route,
|
|
||||||
Route.Room.arguments,
|
|
||||||
enterTransition = { slideInHorizontallyFromEnd },
|
|
||||||
exitTransition = { scaleOut },
|
|
||||||
popEnterTransition = { scaleIn },
|
|
||||||
popExitTransition = { slideOutHorizontallyToEnd },
|
|
||||||
) {
|
|
||||||
ChatroomScreen(
|
|
||||||
roomId = it.id(),
|
|
||||||
draftMessage = it.message(),
|
|
||||||
replyToNote = it.replyId(),
|
|
||||||
editFromDraft = it.draftId(),
|
|
||||||
accountViewModel = accountViewModel,
|
|
||||||
nav = nav,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
composable(
|
|
||||||
Route.RoomByAuthor.route,
|
|
||||||
Route.RoomByAuthor.arguments,
|
|
||||||
enterTransition = { slideInHorizontallyFromEnd },
|
|
||||||
exitTransition = { scaleOut },
|
|
||||||
popEnterTransition = { scaleIn },
|
|
||||||
popExitTransition = { slideOutHorizontallyToEnd },
|
|
||||||
) {
|
|
||||||
ChatroomByAuthorScreen(it.id(), null, accountViewModel, nav)
|
|
||||||
}
|
|
||||||
|
|
||||||
composable(
|
|
||||||
Route.Channel.route,
|
|
||||||
Route.Channel.arguments,
|
|
||||||
enterTransition = { slideInHorizontallyFromEnd },
|
|
||||||
exitTransition = { scaleOut },
|
|
||||||
popEnterTransition = { scaleIn },
|
|
||||||
popExitTransition = { slideOutHorizontallyToEnd },
|
|
||||||
) {
|
|
||||||
ChannelScreen(
|
|
||||||
channelId = it.id(),
|
|
||||||
accountViewModel = accountViewModel,
|
|
||||||
nav = nav,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
composable(
|
|
||||||
Route.ChannelMetadataEdit.route,
|
|
||||||
Route.ChannelMetadataEdit.arguments,
|
|
||||||
enterTransition = { slideInVerticallyFromBottom },
|
|
||||||
exitTransition = { scaleOut },
|
|
||||||
popEnterTransition = { scaleIn },
|
|
||||||
popExitTransition = { slideOutVerticallyToBottom },
|
|
||||||
content = {
|
|
||||||
ChannelMetadataScreen(
|
|
||||||
channelId = it.id(),
|
|
||||||
accountViewModel = accountViewModel,
|
|
||||||
nav = nav,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
composable(
|
|
||||||
Route.NewGroupDM.route,
|
|
||||||
Route.NewGroupDM.arguments,
|
|
||||||
enterTransition = { slideInVerticallyFromBottom },
|
|
||||||
exitTransition = { scaleOut },
|
|
||||||
popEnterTransition = { scaleIn },
|
|
||||||
popExitTransition = { slideOutVerticallyToBottom },
|
|
||||||
content = {
|
|
||||||
val draftMessage = it.message()?.ifBlank { null }
|
|
||||||
val attachment =
|
|
||||||
it.arguments
|
|
||||||
?.getString("attachment")
|
|
||||||
?.ifBlank { null }
|
|
||||||
?.toUri()
|
|
||||||
|
|
||||||
NewGroupDMScreen(
|
|
||||||
draftMessage,
|
|
||||||
attachment,
|
|
||||||
accountViewModel = accountViewModel,
|
|
||||||
nav = nav,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
composable(
|
|
||||||
Route.Event.route,
|
|
||||||
Route.Event.arguments,
|
|
||||||
) {
|
|
||||||
LoadRedirectScreen(
|
|
||||||
eventId = it.id(),
|
|
||||||
accountViewModel = accountViewModel,
|
|
||||||
nav = nav,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
composable(
|
|
||||||
Route.Settings.route,
|
|
||||||
Route.Settings.arguments,
|
|
||||||
enterTransition = { slideInHorizontallyFromEnd },
|
|
||||||
exitTransition = { scaleOut },
|
|
||||||
popEnterTransition = { scaleIn },
|
|
||||||
popExitTransition = { slideOutHorizontallyToEnd },
|
|
||||||
) {
|
|
||||||
SettingsScreen(
|
|
||||||
sharedPreferencesViewModel,
|
|
||||||
accountViewModel,
|
|
||||||
nav,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
composable(
|
|
||||||
Route.NIP47Setup.route,
|
|
||||||
Route.NIP47Setup.arguments,
|
|
||||||
enterTransition = { slideInVerticallyFromBottom },
|
|
||||||
exitTransition = { scaleOut },
|
|
||||||
popEnterTransition = { scaleIn },
|
|
||||||
popExitTransition = { slideOutVerticallyToBottom },
|
|
||||||
) {
|
|
||||||
val nip47 = it.arguments?.getString("nip47")
|
|
||||||
|
|
||||||
NIP47SetupScreen(accountViewModel, nav, nip47)
|
|
||||||
}
|
|
||||||
|
|
||||||
composable(
|
|
||||||
Route.EditRelays.route,
|
|
||||||
content = {
|
|
||||||
val relayToAdd = it.arguments?.getString("toAdd")
|
|
||||||
|
|
||||||
AllRelayListScreen(
|
|
||||||
relayToAdd = relayToAdd,
|
|
||||||
accountViewModel = accountViewModel,
|
|
||||||
nav = nav,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
composable(
|
|
||||||
Route.NewPost.route,
|
|
||||||
Route.NewPost.arguments,
|
|
||||||
enterTransition = { slideInVerticallyFromBottom },
|
|
||||||
exitTransition = { scaleOut },
|
|
||||||
popEnterTransition = { scaleIn },
|
|
||||||
popExitTransition = { slideOutVerticallyToBottom },
|
|
||||||
) {
|
|
||||||
val draftMessage = it.message()?.ifBlank { null }
|
|
||||||
val attachment =
|
|
||||||
it.arguments?.getString("attachment")?.ifBlank { null }?.let {
|
|
||||||
Uri.parse(it)
|
|
||||||
}
|
|
||||||
val baseReplyTo = it.arguments?.getString("baseReplyTo")
|
|
||||||
val quote = it.arguments?.getString("quote")
|
|
||||||
val fork = it.arguments?.getString("fork")
|
|
||||||
val version = it.arguments?.getString("version")
|
|
||||||
val draft = it.arguments?.getString("draft")
|
|
||||||
val enableGeolocation = it.arguments?.getBoolean("enableGeolocation") == true
|
|
||||||
|
|
||||||
|
composableFromBottomArgs<Route.NewPost> {
|
||||||
NewPostScreen(
|
NewPostScreen(
|
||||||
message = draftMessage,
|
message = it.message,
|
||||||
attachment = attachment,
|
attachment = it.attachment?.ifBlank { null }?.toUri(),
|
||||||
baseReplyTo = baseReplyTo?.let { hex -> accountViewModel.getNoteIfExists(hex) },
|
baseReplyTo = it.baseReplyTo?.let { hex -> accountViewModel.getNoteIfExists(hex) },
|
||||||
quote = quote?.let { hex -> accountViewModel.getNoteIfExists(hex) },
|
quote = it.quote?.let { hex -> accountViewModel.getNoteIfExists(hex) },
|
||||||
fork = fork?.let { hex -> accountViewModel.getNoteIfExists(hex) },
|
fork = it.fork?.let { hex -> accountViewModel.getNoteIfExists(hex) },
|
||||||
version = version?.let { hex -> accountViewModel.getNoteIfExists(hex) },
|
version = it.version?.let { hex -> accountViewModel.getNoteIfExists(hex) },
|
||||||
draft = draft?.let { hex -> accountViewModel.getNoteIfExists(hex) },
|
draft = it.draft?.let { hex -> accountViewModel.getNoteIfExists(hex) },
|
||||||
enableGeolocation = enableGeolocation,
|
enableGeolocation = it.enableGeolocation,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
@@ -425,7 +184,7 @@ private fun NavigateIfIntentRequested(
|
|||||||
if (activity.intent.action == Intent.ACTION_SEND) {
|
if (activity.intent.action == Intent.ACTION_SEND) {
|
||||||
// avoids restarting the new Post screen when the intent is for the screen.
|
// avoids restarting the new Post screen when the intent is for the screen.
|
||||||
// Microsoft's swift key sends Gifs as new actions
|
// Microsoft's swift key sends Gifs as new actions
|
||||||
if (isBaseRoute(nav.controller, Route.NewPost.base)) return
|
if (isBaseRoute<Route.NewPost>(nav.controller)) return
|
||||||
|
|
||||||
// saves the intent to avoid processing again
|
// saves the intent to avoid processing again
|
||||||
var message by remember {
|
var message by remember {
|
||||||
@@ -442,7 +201,7 @@ private fun NavigateIfIntentRequested(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
nav.newStack(buildNewPostRoute(draftMessage = message, attachment = media))
|
nav.newStack(Route.NewPost(message = message, attachment = media.toString()))
|
||||||
|
|
||||||
media = null
|
media = null
|
||||||
message = null
|
message = null
|
||||||
@@ -504,13 +263,13 @@ private fun NavigateIfIntentRequested(
|
|||||||
if (intent.action == Intent.ACTION_SEND) {
|
if (intent.action == Intent.ACTION_SEND) {
|
||||||
// avoids restarting the new Post screen when the intent is for the screen.
|
// avoids restarting the new Post screen when the intent is for the screen.
|
||||||
// Microsoft's swift key sends Gifs as new actions
|
// Microsoft's swift key sends Gifs as new actions
|
||||||
if (!isBaseRoute(nav.controller, Route.NewPost.base)) {
|
if (!isBaseRoute<Route.NewPost>(nav.controller)) {
|
||||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
||||||
nav.newStack(buildNewPostRoute(draftMessage = it))
|
nav.newStack(Route.NewPost(message = it))
|
||||||
}
|
}
|
||||||
|
|
||||||
(intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)?.let {
|
(intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)?.let {
|
||||||
nav.newStack(buildNewPostRoute(attachment = it))
|
nav.newStack(Route.NewPost(attachment = it.toString()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -561,8 +320,8 @@ private fun NavigateIfIntentRequested(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun isSameRoute(
|
private fun isSameRoute(
|
||||||
currentRoute: String?,
|
currentRoute: Route?,
|
||||||
newRoute: String,
|
newRoute: Route,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (currentRoute == null) return false
|
if (currentRoute == null) return false
|
||||||
|
|
||||||
@@ -570,9 +329,11 @@ private fun isSameRoute(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newRoute.startsWith("Event/") && currentRoute.contains("/")) {
|
if (newRoute is Route.EventRedirect) {
|
||||||
if (newRoute.split("/")[1] == currentRoute.split("/")[1]) {
|
return when (currentRoute) {
|
||||||
return true
|
is Route.Note -> newRoute.id == currentRoute.id
|
||||||
|
is Route.Channel -> newRoute.id == currentRoute.id
|
||||||
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ fun GenericMainTopBar(
|
|||||||
LoggedInUserPictureDrawer(accountViewModel, nav::openDrawer)
|
LoggedInUserPictureDrawer(accountViewModel, nav::openDrawer)
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = { nav.nav(Route.Search.route) }) {
|
IconButton(onClick = { nav.nav(Route.Search) }) {
|
||||||
SearchIcon(modifier = Size22Modifier, MaterialTheme.colorScheme.placeholderText)
|
SearchIcon(modifier = Size22Modifier, MaterialTheme.colorScheme.placeholderText)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -72,13 +72,15 @@ import androidx.compose.ui.layout.ContentScale
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.LinkAnnotation
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.TextLinkStyles
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.text.withStyle
|
import androidx.compose.ui.text.withLink
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
@@ -89,7 +91,6 @@ import com.vitorpamplona.amethyst.model.Account
|
|||||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.MediaServersListView
|
import com.vitorpamplona.amethyst.ui.actions.mediaServers.MediaServersListView
|
||||||
import com.vitorpamplona.amethyst.ui.components.ClickableText
|
|
||||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||||
import com.vitorpamplona.amethyst.ui.note.LoadStatuses
|
import com.vitorpamplona.amethyst.ui.note.LoadStatuses
|
||||||
@@ -232,7 +233,7 @@ fun ProfileContentTemplate(
|
|||||||
.width(100.dp)
|
.width(100.dp)
|
||||||
.height(100.dp)
|
.height(100.dp)
|
||||||
.clip(shape = CircleShape)
|
.clip(shape = CircleShape)
|
||||||
.border(3.dp, MaterialTheme.colorScheme.background, CircleShape)
|
.border(3.dp, MaterialTheme.colorScheme.onBackground, CircleShape)
|
||||||
.clickable(onClick = onClick),
|
.clickable(onClick = onClick),
|
||||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||||
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
||||||
@@ -422,7 +423,7 @@ fun WatchFollower(
|
|||||||
.observeAsState()
|
.observeAsState()
|
||||||
|
|
||||||
LaunchedEffect(key1 = accountUserFollowersState) {
|
LaunchedEffect(key1 = accountUserFollowersState) {
|
||||||
onReady(baseAccountUser.followerCount().toString() ?: "--")
|
onReady(baseAccountUser.followerCount().toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,8 +434,6 @@ fun ListContent(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
val route = remember(accountViewModel) { "User/${accountViewModel.userProfile().pubkeyHex}" }
|
|
||||||
|
|
||||||
var editMediaServers by remember { mutableStateOf(false) }
|
var editMediaServers by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
var backupDialogOpen by remember { mutableStateOf(false) }
|
var backupDialogOpen by remember { mutableStateOf(false) }
|
||||||
@@ -445,11 +444,11 @@ fun ListContent(
|
|||||||
|
|
||||||
Column(modifier) {
|
Column(modifier) {
|
||||||
NavigationRow(
|
NavigationRow(
|
||||||
title = stringRes(R.string.profile),
|
title = R.string.profile,
|
||||||
icon = Route.Profile.icon,
|
icon = R.drawable.ic_profile,
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
route = route,
|
route = remember { Route.Profile(accountViewModel.userProfile().pubkeyHex) },
|
||||||
)
|
)
|
||||||
|
|
||||||
NavigationRow(
|
NavigationRow(
|
||||||
@@ -461,31 +460,31 @@ fun ListContent(
|
|||||||
)
|
)
|
||||||
|
|
||||||
NavigationRow(
|
NavigationRow(
|
||||||
title = stringRes(R.string.bookmarks),
|
title = R.string.bookmarks,
|
||||||
icon = Route.Bookmarks.icon,
|
icon = R.drawable.ic_bookmarks,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
route = Route.Bookmarks.route,
|
route = Route.Bookmarks,
|
||||||
)
|
)
|
||||||
|
|
||||||
NavigationRow(
|
NavigationRow(
|
||||||
title = stringRes(R.string.drafts),
|
title = R.string.drafts,
|
||||||
icon = Route.Drafts.icon,
|
icon = R.drawable.ic_topics,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
route = Route.Drafts.route,
|
route = Route.Drafts,
|
||||||
)
|
)
|
||||||
|
|
||||||
IconRowRelays(
|
IconRowRelays(
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
onClick = {
|
onClick = {
|
||||||
nav.closeDrawer()
|
nav.closeDrawer()
|
||||||
nav.nav(Route.EditRelays.base)
|
nav.nav(Route.EditRelays())
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
IconRow(
|
IconRow(
|
||||||
title = stringRes(R.string.media_servers),
|
title = R.string.media_servers,
|
||||||
icon = Icons.Outlined.CloudUpload,
|
icon = Icons.Outlined.CloudUpload,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -495,15 +494,15 @@ fun ListContent(
|
|||||||
)
|
)
|
||||||
|
|
||||||
NavigationRow(
|
NavigationRow(
|
||||||
title = stringRes(R.string.security_filters),
|
title = R.string.security_filters,
|
||||||
icon = Route.BlockedUsers.icon,
|
icon = R.drawable.ic_security,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
route = Route.BlockedUsers.route,
|
route = Route.SecurityFilters,
|
||||||
)
|
)
|
||||||
|
|
||||||
IconRow(
|
IconRow(
|
||||||
title = stringRes(R.string.privacy_options),
|
title = R.string.privacy_options,
|
||||||
icon = R.drawable.ic_tor,
|
icon = R.drawable.ic_tor,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -514,7 +513,7 @@ fun ListContent(
|
|||||||
|
|
||||||
accountViewModel.account.settings.keyPair.privKey?.let {
|
accountViewModel.account.settings.keyPair.privKey?.let {
|
||||||
IconRow(
|
IconRow(
|
||||||
title = stringRes(R.string.backup_keys),
|
title = R.string.backup_keys,
|
||||||
icon = R.drawable.ic_key,
|
icon = R.drawable.ic_key,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -525,17 +524,17 @@ fun ListContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
NavigationRow(
|
NavigationRow(
|
||||||
title = stringRes(R.string.preferences),
|
title = R.string.preferences,
|
||||||
icon = Route.Settings.icon,
|
icon = R.drawable.ic_settings,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
route = Route.Settings.route,
|
route = Route.Settings,
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
IconRow(
|
IconRow(
|
||||||
title = stringRes(R.string.drawer_accounts),
|
title = R.string.drawer_accounts,
|
||||||
icon = Icons.Outlined.GroupAdd,
|
icon = Icons.Outlined.GroupAdd,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
onClick = openSheet,
|
onClick = openSheet,
|
||||||
@@ -602,11 +601,11 @@ private fun RenderRelayStatus(relayPool: RelayPoolStatus) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NavigationRow(
|
fun NavigationRow(
|
||||||
title: String,
|
title: Int,
|
||||||
icon: Int,
|
icon: Int,
|
||||||
tint: Color,
|
tint: Color,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
route: String,
|
route: Route,
|
||||||
) {
|
) {
|
||||||
IconRow(
|
IconRow(
|
||||||
title,
|
title,
|
||||||
@@ -621,7 +620,7 @@ fun NavigationRow(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun IconRow(
|
fun IconRow(
|
||||||
title: String,
|
title: Int,
|
||||||
icon: Int,
|
icon: Int,
|
||||||
tint: Color,
|
tint: Color,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
@@ -640,13 +639,13 @@ fun IconRow(
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(icon),
|
painter = painterResource(icon),
|
||||||
null,
|
contentDescription = stringRes(title),
|
||||||
modifier = Size22Modifier,
|
modifier = Size22Modifier,
|
||||||
tint = tint,
|
tint = tint,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
modifier = IconRowTextModifier,
|
modifier = IconRowTextModifier,
|
||||||
text = title,
|
text = stringRes(title),
|
||||||
fontSize = Font18SP,
|
fontSize = Font18SP,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -655,7 +654,7 @@ fun IconRow(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun IconRow(
|
fun IconRow(
|
||||||
title: String,
|
title: Int,
|
||||||
icon: ImageVector,
|
icon: ImageVector,
|
||||||
tint: Color,
|
tint: Color,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
@@ -665,7 +664,7 @@ fun IconRow(
|
|||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(
|
.clickable(
|
||||||
onClickLabel = title,
|
onClickLabel = stringRes(title),
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
@@ -675,13 +674,13 @@ fun IconRow(
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
null,
|
contentDescription = stringRes(title),
|
||||||
modifier = Size22Modifier,
|
modifier = Size22Modifier,
|
||||||
tint = tint,
|
tint = tint,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
modifier = IconRowTextModifier,
|
modifier = IconRowTextModifier,
|
||||||
text = title,
|
text = stringRes(title),
|
||||||
fontSize = Font18SP,
|
fontSize = Font18SP,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -749,23 +748,33 @@ fun BottomContent(
|
|||||||
.padding(horizontal = 15.dp),
|
.padding(horizontal = 15.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
ClickableText(
|
val string =
|
||||||
text =
|
remember {
|
||||||
buildAnnotatedString {
|
buildAnnotatedString {
|
||||||
withStyle(
|
withLink(
|
||||||
SpanStyle(
|
LinkAnnotation.Clickable(
|
||||||
fontSize = 12.sp,
|
"clickable",
|
||||||
fontWeight = FontWeight.Bold,
|
TextLinkStyles(
|
||||||
),
|
SpanStyle(
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
nav.nav(Route.Note(BuildConfig.RELEASE_NOTES_ID))
|
||||||
|
nav.closeDrawer()
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
append("v" + BuildConfig.VERSION_NAME + "-" + BuildConfig.FLAVOR.uppercase())
|
append("v12" + BuildConfig.VERSION_NAME + "-" + BuildConfig.FLAVOR.uppercase())
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
onClick = {
|
}
|
||||||
nav.nav("Note/${BuildConfig.RELEASE_NOTES_ID}")
|
|
||||||
nav.closeDrawer()
|
Text(
|
||||||
},
|
text = string,
|
||||||
modifier = Modifier.padding(start = 16.dp),
|
modifier = Modifier.padding(start = 16.dp),
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = 1,
|
||||||
)
|
)
|
||||||
Box(modifier = Modifier.weight(1F))
|
Box(modifier = Modifier.weight(1F))
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.navigation
|
package com.vitorpamplona.amethyst.ui.navigation
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import androidx.compose.material3.DrawerState
|
import androidx.compose.material3.DrawerState
|
||||||
import androidx.compose.material3.DrawerValue
|
import androidx.compose.material3.DrawerValue
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -31,6 +32,7 @@ import androidx.navigation.compose.rememberNavController
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberNav(): Nav {
|
fun rememberNav(): Nav {
|
||||||
@@ -52,17 +54,17 @@ fun rememberExtendedNav(
|
|||||||
interface INav {
|
interface INav {
|
||||||
val drawerState: DrawerState
|
val drawerState: DrawerState
|
||||||
|
|
||||||
fun nav(route: String)
|
fun nav(route: Route)
|
||||||
|
|
||||||
fun nav(computeRoute: suspend () -> String)
|
fun nav(computeRoute: suspend () -> Route)
|
||||||
|
|
||||||
fun newStack(route: String)
|
fun newStack(route: Route)
|
||||||
|
|
||||||
fun popBack()
|
fun popBack()
|
||||||
|
|
||||||
fun popUpTo(
|
fun <T : Route> popUpTo(
|
||||||
route: String,
|
route: Route,
|
||||||
upTo: String,
|
klass: KClass<T>,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun closeDrawer()
|
fun closeDrawer()
|
||||||
@@ -85,7 +87,7 @@ class Nav(
|
|||||||
scope.launch { drawerState.open() }
|
scope.launch { drawerState.open() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun nav(route: String) {
|
override fun nav(route: Route) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
if (getRouteWithArguments(controller) != route) {
|
if (getRouteWithArguments(controller) != route) {
|
||||||
controller.navigate(route)
|
controller.navigate(route)
|
||||||
@@ -93,7 +95,7 @@ class Nav(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun nav(computeRoute: suspend () -> String) {
|
override fun nav(computeRoute: suspend () -> Route) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val route = computeRoute()
|
val route = computeRoute()
|
||||||
if (getRouteWithArguments(controller) != route) {
|
if (getRouteWithArguments(controller) != route) {
|
||||||
@@ -102,10 +104,10 @@ class Nav(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun newStack(route: String) {
|
override fun newStack(route: Route) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
controller.navigate(route) {
|
controller.navigate(route) {
|
||||||
popUpTo(Route.Home.route)
|
popUpTo(Route.Home)
|
||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,12 +119,15 @@ class Nav(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popUpTo(
|
@SuppressLint("RestrictedApi")
|
||||||
route: String,
|
override fun <T : Route> popUpTo(
|
||||||
upTo: String,
|
route: Route,
|
||||||
|
upToClass: KClass<T>,
|
||||||
) {
|
) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
controller.navigate(route) { popUpTo(upTo) { inclusive = true } }
|
controller.navigate(route) {
|
||||||
|
popUpTo<T>(upToClass) { inclusive = true }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,34 +137,25 @@ object EmptyNav : INav {
|
|||||||
override val drawerState = DrawerState(DrawerValue.Closed)
|
override val drawerState = DrawerState(DrawerValue.Closed)
|
||||||
|
|
||||||
override fun closeDrawer() {
|
override fun closeDrawer() {
|
||||||
runBlocking {
|
runBlocking { drawerState.close() }
|
||||||
drawerState.close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun openDrawer() {
|
override fun openDrawer() {
|
||||||
runBlocking {
|
runBlocking { drawerState.open() }
|
||||||
drawerState.open()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun nav(route: String) {
|
override fun nav(route: Route) {}
|
||||||
}
|
|
||||||
|
|
||||||
override fun nav(computeRoute: suspend () -> String) {
|
override fun nav(computeRoute: suspend () -> Route) {}
|
||||||
}
|
|
||||||
|
|
||||||
override fun newStack(route: String) {
|
override fun newStack(route: Route) {}
|
||||||
}
|
|
||||||
|
|
||||||
override fun popBack() {
|
override fun popBack() {}
|
||||||
}
|
|
||||||
|
|
||||||
override fun popUpTo(
|
override fun <T : Route> popUpTo(
|
||||||
route: String,
|
route: Route,
|
||||||
upTo: String,
|
upToClass: KClass<T>,
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun INav.onNavigate(runOnNavigate: () -> Unit): INav = ObservableNavigate(this, runOnNavigate)
|
fun INav.onNavigate(runOnNavigate: () -> Unit): INav = ObservableNavigate(this, runOnNavigate)
|
||||||
@@ -170,17 +166,25 @@ class ObservableNavigate(
|
|||||||
) : INav {
|
) : INav {
|
||||||
override val drawerState: DrawerState = nav.drawerState
|
override val drawerState: DrawerState = nav.drawerState
|
||||||
|
|
||||||
override fun nav(route: String) {
|
override fun closeDrawer() {
|
||||||
|
nav.closeDrawer()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun openDrawer() {
|
||||||
|
nav.openDrawer()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun nav(route: Route) {
|
||||||
onNavigate()
|
onNavigate()
|
||||||
nav.nav(route)
|
nav.nav(route)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun nav(computeRoute: suspend () -> String) {
|
override fun nav(computeRoute: suspend () -> Route) {
|
||||||
onNavigate()
|
onNavigate()
|
||||||
nav.nav(computeRoute)
|
nav.nav(computeRoute)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun newStack(route: String) {
|
override fun newStack(route: Route) {
|
||||||
onNavigate()
|
onNavigate()
|
||||||
nav.newStack(route)
|
nav.newStack(route)
|
||||||
}
|
}
|
||||||
@@ -190,19 +194,11 @@ class ObservableNavigate(
|
|||||||
nav.popBack()
|
nav.popBack()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popUpTo(
|
override fun <T : Route> popUpTo(
|
||||||
route: String,
|
route: Route,
|
||||||
upTo: String,
|
upToClass: KClass<T>,
|
||||||
) {
|
) {
|
||||||
onNavigate()
|
onNavigate()
|
||||||
nav.popUpTo(route, upTo)
|
nav.popUpTo(route, upToClass)
|
||||||
}
|
|
||||||
|
|
||||||
override fun closeDrawer() {
|
|
||||||
nav.closeDrawer()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun openDrawer() {
|
|
||||||
nav.openDrawer()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.navigation
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContentScope
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.navigation.NavBackStackEntry
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.toRoute
|
||||||
|
|
||||||
|
inline fun <reified T : Any> NavGraphBuilder.composableFromEnd(noinline content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit) {
|
||||||
|
composable<T>(
|
||||||
|
enterTransition = { slideInHorizontallyFromEnd },
|
||||||
|
exitTransition = { scaleOut },
|
||||||
|
popEnterTransition = { scaleIn },
|
||||||
|
popExitTransition = { slideOutHorizontallyToEnd },
|
||||||
|
content = content,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T : Any> NavGraphBuilder.composableFromEndArgs(noinline content: @Composable AnimatedContentScope.(T) -> Unit) {
|
||||||
|
composableFromEnd<T> {
|
||||||
|
content(it.toRoute<T>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T : Any> NavGraphBuilder.composableFromBottom(noinline content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit) {
|
||||||
|
composable<T>(
|
||||||
|
enterTransition = { slideInVerticallyFromBottom },
|
||||||
|
exitTransition = { scaleOut },
|
||||||
|
popEnterTransition = { scaleIn },
|
||||||
|
popExitTransition = { slideOutVerticallyToBottom },
|
||||||
|
content = content,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T : Any> NavGraphBuilder.composableFromBottomArgs(noinline content: @Composable AnimatedContentScope.(T) -> Unit) {
|
||||||
|
composableFromBottom<T> {
|
||||||
|
content(it.toRoute())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T : Any> NavGraphBuilder.composableArgs(noinline content: @Composable AnimatedContentScope.(T) -> Unit) {
|
||||||
|
composable<T> {
|
||||||
|
content(it.toRoute())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,13 +38,12 @@ import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessa
|
|||||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||||
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
||||||
import java.net.URLEncoder
|
|
||||||
|
|
||||||
fun routeFor(
|
fun routeFor(
|
||||||
note: Note,
|
note: Note,
|
||||||
loggedIn: User,
|
loggedIn: User,
|
||||||
): String? {
|
): Route? {
|
||||||
val noteEvent = note.event ?: return "Note/${URLEncoder.encode(note.idHex, "utf-8")}"
|
val noteEvent = note.event ?: return Route.Note(note.idHex)
|
||||||
|
|
||||||
return routeFor(noteEvent, loggedIn)
|
return routeFor(noteEvent, loggedIn)
|
||||||
}
|
}
|
||||||
@@ -52,57 +51,57 @@ fun routeFor(
|
|||||||
fun routeFor(
|
fun routeFor(
|
||||||
noteEvent: Event,
|
noteEvent: Event,
|
||||||
loggedIn: User,
|
loggedIn: User,
|
||||||
): String? {
|
): Route? {
|
||||||
if (noteEvent is DraftEvent) {
|
if (noteEvent is DraftEvent) {
|
||||||
val innerEvent = noteEvent.preCachedDraft(loggedIn.pubkeyHex)
|
val innerEvent = noteEvent.preCachedDraft(loggedIn.pubkeyHex)
|
||||||
|
|
||||||
if (innerEvent is IsInPublicChatChannel) {
|
if (innerEvent is IsInPublicChatChannel) {
|
||||||
innerEvent.channelId()?.let {
|
innerEvent.channelId()?.let {
|
||||||
return "Channel/$it"
|
return Route.Channel(it)
|
||||||
}
|
}
|
||||||
} else if (innerEvent is LiveActivitiesEvent) {
|
} else if (innerEvent is LiveActivitiesEvent) {
|
||||||
innerEvent.aTag().toTag().let {
|
innerEvent.aTag().toTag().let {
|
||||||
return "Channel/${URLEncoder.encode(it, "utf-8")}"
|
return Route.Channel(it)
|
||||||
}
|
}
|
||||||
} else if (innerEvent is LiveActivitiesChatMessageEvent) {
|
} else if (innerEvent is LiveActivitiesChatMessageEvent) {
|
||||||
innerEvent.activity()?.toTag()?.let {
|
innerEvent.activity()?.toTag()?.let {
|
||||||
return "Channel/${URLEncoder.encode(it, "utf-8")}"
|
return Route.Channel(it)
|
||||||
}
|
}
|
||||||
} else if (innerEvent is ChatroomKeyable) {
|
} else if (innerEvent is ChatroomKeyable) {
|
||||||
val room = innerEvent.chatroomKey(loggedIn.pubkeyHex)
|
val room = innerEvent.chatroomKey(loggedIn.pubkeyHex)
|
||||||
loggedIn.createChatroom(room)
|
loggedIn.createChatroom(room)
|
||||||
return "Room/${room.hashCode()}"
|
return Route.Room(room.hashCode())
|
||||||
} else if (innerEvent is AddressableEvent) {
|
} else if (innerEvent is AddressableEvent) {
|
||||||
return "Note/${URLEncoder.encode(noteEvent.aTag().toTag(), "utf-8")}"
|
return Route.Note(noteEvent.aTag().toTag())
|
||||||
} else {
|
} else {
|
||||||
return "Note/${URLEncoder.encode(noteEvent.id, "utf-8")}"
|
return Route.Note(noteEvent.id)
|
||||||
}
|
}
|
||||||
} else if (noteEvent is AppDefinitionEvent) {
|
} else if (noteEvent is AppDefinitionEvent) {
|
||||||
return "ContentDiscovery/${noteEvent.id}"
|
return Route.ContentDiscovery(noteEvent.id)
|
||||||
} else if (noteEvent is IsInPublicChatChannel) {
|
} else if (noteEvent is IsInPublicChatChannel) {
|
||||||
noteEvent.channelId()?.let {
|
noteEvent.channelId()?.let {
|
||||||
return "Channel/$it"
|
return Route.Channel(it)
|
||||||
}
|
}
|
||||||
} else if (noteEvent is ChannelCreateEvent) {
|
} else if (noteEvent is ChannelCreateEvent) {
|
||||||
return "Channel/${noteEvent.id}"
|
return Route.Channel(noteEvent.id)
|
||||||
} else if (noteEvent is LiveActivitiesEvent) {
|
} else if (noteEvent is LiveActivitiesEvent) {
|
||||||
noteEvent.aTag().toTag().let {
|
noteEvent.aTag().toTag().let {
|
||||||
return "Channel/${URLEncoder.encode(it, "utf-8")}"
|
return Route.Channel(it)
|
||||||
}
|
}
|
||||||
} else if (noteEvent is LiveActivitiesChatMessageEvent) {
|
} else if (noteEvent is LiveActivitiesChatMessageEvent) {
|
||||||
noteEvent.activity()?.toTag()?.let {
|
noteEvent.activity()?.toTag()?.let {
|
||||||
return "Channel/${URLEncoder.encode(it, "utf-8")}"
|
return Route.Channel(it)
|
||||||
}
|
}
|
||||||
} else if (noteEvent is ChatroomKeyable) {
|
} else if (noteEvent is ChatroomKeyable) {
|
||||||
val room = noteEvent.chatroomKey(loggedIn.pubkeyHex)
|
val room = noteEvent.chatroomKey(loggedIn.pubkeyHex)
|
||||||
loggedIn.createChatroom(room)
|
loggedIn.createChatroom(room)
|
||||||
return "Room/${room.hashCode()}"
|
return Route.Room(room.hashCode())
|
||||||
} else if (noteEvent is CommunityDefinitionEvent) {
|
} else if (noteEvent is CommunityDefinitionEvent) {
|
||||||
return "Community/${URLEncoder.encode(noteEvent.aTag().toTag(), "utf-8")}"
|
return Route.Community(noteEvent.aTag().toTag())
|
||||||
} else if (noteEvent is AddressableEvent) {
|
} else if (noteEvent is AddressableEvent) {
|
||||||
return "Note/${URLEncoder.encode(noteEvent.aTag().toTag(), "utf-8")}"
|
return Route.Note(noteEvent.aTag().toTag())
|
||||||
} else {
|
} else {
|
||||||
return "Note/${URLEncoder.encode(noteEvent.id, "utf-8")}"
|
return Route.Note(noteEvent.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
@@ -112,14 +111,14 @@ fun routeToMessage(
|
|||||||
user: HexKey,
|
user: HexKey,
|
||||||
draftMessage: String?,
|
draftMessage: String?,
|
||||||
replyId: HexKey? = null,
|
replyId: HexKey? = null,
|
||||||
quoteId: HexKey? = null,
|
draftId: HexKey? = null,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
): String =
|
): Route =
|
||||||
routeToMessage(
|
routeToMessage(
|
||||||
setOf(user),
|
setOf(user),
|
||||||
draftMessage,
|
draftMessage,
|
||||||
replyId,
|
replyId,
|
||||||
quoteId,
|
draftId,
|
||||||
accountViewModel,
|
accountViewModel,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -127,13 +126,13 @@ fun routeToMessage(
|
|||||||
users: Set<HexKey>,
|
users: Set<HexKey>,
|
||||||
draftMessage: String?,
|
draftMessage: String?,
|
||||||
replyId: HexKey? = null,
|
replyId: HexKey? = null,
|
||||||
quoteId: HexKey? = null,
|
draftId: HexKey? = null,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
) = routeToMessage(
|
) = routeToMessage(
|
||||||
ChatroomKey(users),
|
ChatroomKey(users),
|
||||||
draftMessage,
|
draftMessage,
|
||||||
replyId,
|
replyId,
|
||||||
quoteId,
|
draftId,
|
||||||
accountViewModel,
|
accountViewModel,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -141,44 +140,24 @@ fun routeToMessage(
|
|||||||
room: ChatroomKey,
|
room: ChatroomKey,
|
||||||
draftMessage: String?,
|
draftMessage: String?,
|
||||||
replyId: HexKey? = null,
|
replyId: HexKey? = null,
|
||||||
quoteId: HexKey? = null,
|
draftId: HexKey? = null,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
): String {
|
): Route {
|
||||||
accountViewModel.account.userProfile().createChatroom(room)
|
accountViewModel.account.userProfile().createChatroom(room)
|
||||||
|
|
||||||
val params =
|
return Route.Room(room.hashCode(), draftMessage, replyId, draftId)
|
||||||
listOfNotNull(
|
|
||||||
draftMessage?.let {
|
|
||||||
"message=${URLEncoder.encode(it, "utf-8")}"
|
|
||||||
},
|
|
||||||
replyId?.let {
|
|
||||||
"replyId=${URLEncoder.encode(it, "utf-8")}"
|
|
||||||
},
|
|
||||||
quoteId?.let {
|
|
||||||
"quoteId=${URLEncoder.encode(it, "utf-8")}"
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return buildString {
|
|
||||||
append("Room/")
|
|
||||||
append(room.hashCode().toString())
|
|
||||||
if (params.isNotEmpty()) {
|
|
||||||
append("?")
|
|
||||||
append(params.joinToString("&"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun routeToMessage(
|
fun routeToMessage(
|
||||||
user: User,
|
user: User,
|
||||||
draftMessage: String?,
|
draftMessage: String?,
|
||||||
replyId: HexKey? = null,
|
replyId: HexKey? = null,
|
||||||
quoteId: HexKey? = null,
|
draftId: HexKey? = null,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
): String = routeToMessage(user.pubkeyHex, draftMessage, replyId, quoteId, accountViewModel)
|
): Route = routeToMessage(user.pubkeyHex, draftMessage, replyId, draftId, accountViewModel)
|
||||||
|
|
||||||
fun routeFor(note: Channel): String = "Channel/${note.idHex}"
|
fun routeFor(note: Channel): Route = Route.Channel(note.idHex)
|
||||||
|
|
||||||
fun routeFor(user: User): String = "User/${user.pubkeyHex}"
|
fun routeFor(user: User): Route.Profile = Route.Profile(user.pubkeyHex)
|
||||||
|
|
||||||
fun authorRouteFor(note: Note): String = "User/${note.author?.pubkeyHex}"
|
fun authorRouteFor(note: Note): Route.Profile? = note.author?.pubkeyHex?.let { Route.Profile(it) }
|
||||||
|
|||||||
@@ -20,124 +20,46 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.navigation
|
package com.vitorpamplona.amethyst.ui.navigation
|
||||||
|
|
||||||
import android.R.attr.type
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.runtime.Immutable
|
|
||||||
import androidx.compose.runtime.State
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.navigation.NamedNavArgument
|
import androidx.navigation.NavDestination.Companion.hasRoute
|
||||||
import androidx.navigation.NavBackStackEntry
|
|
||||||
import androidx.navigation.NavDestination
|
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.NavType
|
import androidx.navigation.toRoute
|
||||||
import androidx.navigation.navArgument
|
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size23dp
|
import com.vitorpamplona.amethyst.ui.theme.Size23dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size24dp
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlin.String
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
|
||||||
import java.net.URLEncoder
|
|
||||||
|
|
||||||
@Immutable
|
class BottomBarRoute(
|
||||||
sealed class Route(
|
val route: Route,
|
||||||
val route: String,
|
|
||||||
val base: String = route.substringBefore("?"),
|
|
||||||
val icon: Int,
|
val icon: Int,
|
||||||
|
val contentDescriptor: Int = R.string.route,
|
||||||
val notifSize: Modifier = Modifier.size(Size23dp),
|
val notifSize: Modifier = Modifier.size(Size23dp),
|
||||||
val iconSize: Modifier = Modifier.size(Size20dp),
|
val iconSize: Modifier = Modifier.size(Size20dp),
|
||||||
val contentDescriptor: Int = R.string.route,
|
)
|
||||||
val arguments: ImmutableList<NamedNavArgument> = persistentListOf(),
|
|
||||||
) {
|
|
||||||
object Home :
|
|
||||||
Route(
|
|
||||||
route = "Home",
|
|
||||||
icon = R.drawable.ic_home,
|
|
||||||
notifSize = Modifier.size(Size25dp),
|
|
||||||
iconSize = Modifier.size(Size24dp),
|
|
||||||
contentDescriptor = R.string.route_home,
|
|
||||||
)
|
|
||||||
|
|
||||||
object Global :
|
sealed class Route {
|
||||||
Route(
|
@Serializable object Home : Route()
|
||||||
route = "Global",
|
|
||||||
icon = R.drawable.ic_globe,
|
|
||||||
contentDescriptor = R.string.route_global,
|
|
||||||
)
|
|
||||||
|
|
||||||
object Search :
|
@Serializable object Message : Route()
|
||||||
Route(
|
|
||||||
route = "Search",
|
|
||||||
icon = R.drawable.ic_moments,
|
|
||||||
contentDescriptor = R.string.route_search,
|
|
||||||
)
|
|
||||||
|
|
||||||
object Video :
|
@Serializable object Video : Route()
|
||||||
Route(
|
|
||||||
route = "Video",
|
|
||||||
icon = R.drawable.ic_video,
|
|
||||||
contentDescriptor = R.string.route_video,
|
|
||||||
)
|
|
||||||
|
|
||||||
object Discover :
|
@Serializable object Discover : Route()
|
||||||
Route(
|
|
||||||
route = "Discover",
|
|
||||||
icon = R.drawable.ic_sensors,
|
|
||||||
contentDescriptor = R.string.route_discover,
|
|
||||||
)
|
|
||||||
|
|
||||||
object Notification :
|
@Serializable object Notification : Route()
|
||||||
Route(
|
|
||||||
route = "Notification",
|
|
||||||
icon = R.drawable.ic_notifications,
|
|
||||||
contentDescriptor = R.string.route_notifications,
|
|
||||||
)
|
|
||||||
|
|
||||||
object Message :
|
@Serializable object Search : Route()
|
||||||
Route(
|
|
||||||
route = "Message",
|
|
||||||
icon = R.drawable.ic_dm,
|
|
||||||
contentDescriptor = R.string.route_messages,
|
|
||||||
)
|
|
||||||
|
|
||||||
object NewGroupDM :
|
@Serializable object SecurityFilters : Route()
|
||||||
Route(
|
|
||||||
route = "NewGroupDM?message={message}&attachment={attachment}",
|
|
||||||
icon = R.drawable.ic_dm,
|
|
||||||
contentDescriptor = R.string.route_messages,
|
|
||||||
arguments =
|
|
||||||
listOf(
|
|
||||||
navArgument("message") {
|
|
||||||
type = NavType.StringType
|
|
||||||
nullable = true
|
|
||||||
defaultValue = null
|
|
||||||
},
|
|
||||||
navArgument("attachment") {
|
|
||||||
type = NavType.StringType
|
|
||||||
nullable = true
|
|
||||||
defaultValue = null
|
|
||||||
},
|
|
||||||
).toImmutableList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
object BlockedUsers :
|
@Serializable object Bookmarks : Route()
|
||||||
Route(
|
|
||||||
route = "BlockedUsers",
|
|
||||||
icon = R.drawable.ic_security,
|
|
||||||
contentDescriptor = R.string.route_security_filters,
|
|
||||||
)
|
|
||||||
|
|
||||||
object Bookmarks :
|
@Serializable object Drafts : Route()
|
||||||
Route(
|
|
||||||
route = "Bookmarks",
|
|
||||||
icon = R.drawable.ic_bookmarks,
|
|
||||||
contentDescriptor = R.string.route_home,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
@Serializable object Settings : Route()
|
||||||
object Lists :
|
object Lists :
|
||||||
Route(
|
Route(
|
||||||
route = "Lists",
|
route = "Lists",
|
||||||
@@ -145,249 +67,119 @@ sealed class Route(
|
|||||||
contentDescriptor = R.string.my_lists,
|
contentDescriptor = R.string.my_lists,
|
||||||
)
|
)
|
||||||
|
|
||||||
object ContentDiscovery :
|
@Serializable object EditProfile : Route()
|
||||||
Route(
|
|
||||||
icon = R.drawable.ic_bookmarks,
|
|
||||||
contentDescriptor = R.string.discover_content,
|
|
||||||
route = "ContentDiscovery/{id}",
|
|
||||||
arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
object Drafts :
|
@Serializable data class EditRelays(
|
||||||
Route(
|
val toAdd: String? = null,
|
||||||
route = "Drafts",
|
) : Route()
|
||||||
icon = R.drawable.ic_topics,
|
|
||||||
contentDescriptor = R.string.drafts,
|
|
||||||
)
|
|
||||||
|
|
||||||
object Profile :
|
@Serializable data class Nip47NWCSetup(
|
||||||
Route(
|
val nip47: String? = null,
|
||||||
route = "User/{id}",
|
) : Route()
|
||||||
icon = R.drawable.ic_profile,
|
|
||||||
arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
object Note :
|
@Serializable data class Profile(
|
||||||
Route(
|
val id: String,
|
||||||
route = "Note/{id}",
|
) : Route()
|
||||||
icon = R.drawable.ic_moments,
|
|
||||||
arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
object Hashtag :
|
@Serializable data class ContentDiscovery(
|
||||||
Route(
|
val id: String,
|
||||||
route = "Hashtag/{id}",
|
) : Route()
|
||||||
icon = R.drawable.ic_moments,
|
|
||||||
arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
object Geohash :
|
@Serializable data class Note(
|
||||||
Route(
|
val id: String,
|
||||||
route = "Geohash/{id}",
|
) : Route()
|
||||||
icon = R.drawable.ic_moments,
|
|
||||||
arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
object Community :
|
@Serializable data class Hashtag(
|
||||||
Route(
|
val id: String,
|
||||||
route = "Community/{id}",
|
) : Route()
|
||||||
icon = R.drawable.ic_moments,
|
|
||||||
arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
object Room :
|
@Serializable data class Geohash(
|
||||||
Route(
|
val id: String,
|
||||||
route = "Room/{id}?message={message}&replyId={replyId}&draftId={draftId}",
|
) : Route()
|
||||||
icon = R.drawable.ic_moments,
|
|
||||||
arguments =
|
|
||||||
listOf(
|
|
||||||
navArgument("id") { type = NavType.StringType },
|
|
||||||
navArgument("message") {
|
|
||||||
type = NavType.StringType
|
|
||||||
nullable = true
|
|
||||||
defaultValue = null
|
|
||||||
},
|
|
||||||
navArgument("replyId") {
|
|
||||||
type = NavType.StringType
|
|
||||||
nullable = true
|
|
||||||
defaultValue = null
|
|
||||||
},
|
|
||||||
navArgument("draftId") {
|
|
||||||
type = NavType.StringType
|
|
||||||
nullable = true
|
|
||||||
defaultValue = null
|
|
||||||
},
|
|
||||||
).toImmutableList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
object RoomByAuthor :
|
@Serializable data class Community(
|
||||||
Route(
|
val id: String,
|
||||||
route = "RoomByAuthor/{id}",
|
) : Route()
|
||||||
icon = R.drawable.ic_moments,
|
|
||||||
arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
object Channel :
|
@Serializable data class Channel(
|
||||||
Route(
|
val id: String,
|
||||||
route = "Channel/{id}",
|
) : Route()
|
||||||
icon = R.drawable.ic_moments,
|
|
||||||
arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
object ChannelMetadataEdit :
|
@Serializable data class ChannelMetadataEdit(
|
||||||
Route(
|
val id: String? = null,
|
||||||
route = "ChannelMetadataEdit?id={id}",
|
) : Route()
|
||||||
icon = R.drawable.ic_moments,
|
|
||||||
arguments =
|
|
||||||
listOf(
|
|
||||||
navArgument("id") {
|
|
||||||
type = NavType.StringType
|
|
||||||
nullable = true
|
|
||||||
defaultValue = null
|
|
||||||
},
|
|
||||||
).toImmutableList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
object Event :
|
@Serializable data class NewGroupDM(
|
||||||
Route(
|
val message: String? = null,
|
||||||
route = "Event/{id}",
|
val attachment: String? = null,
|
||||||
icon = R.drawable.ic_moments,
|
) : Route()
|
||||||
arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
object Settings :
|
@Serializable data class Room(
|
||||||
Route(
|
val id: Int,
|
||||||
route = "Settings",
|
val message: String? = null,
|
||||||
icon = R.drawable.ic_settings,
|
val replyId: HexKey? = null,
|
||||||
)
|
val draftId: HexKey? = null,
|
||||||
|
) : Route()
|
||||||
|
|
||||||
object EditProfile :
|
@Serializable data class RoomByAuthor(
|
||||||
Route(
|
val id: String,
|
||||||
route = "EditProfile",
|
) : Route()
|
||||||
icon = R.drawable.ic_settings,
|
|
||||||
)
|
|
||||||
|
|
||||||
object EditRelays :
|
@Serializable data class EventRedirect(
|
||||||
Route(
|
val id: String,
|
||||||
route = "EditRelays?toAdd={toAdd}",
|
) : Route()
|
||||||
icon = R.drawable.ic_globe,
|
|
||||||
contentDescriptor = R.string.relays,
|
|
||||||
arguments =
|
|
||||||
listOf(
|
|
||||||
navArgument("toAdd") {
|
|
||||||
type = NavType.StringType
|
|
||||||
nullable = true
|
|
||||||
defaultValue = null
|
|
||||||
},
|
|
||||||
).toImmutableList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
object NIP47Setup :
|
@Serializable
|
||||||
Route(
|
data class NewPost(
|
||||||
route = "NIP47Setup?nip47={nip47}",
|
val message: String? = null,
|
||||||
icon = R.drawable.ic_home,
|
val attachment: String? = null,
|
||||||
arguments =
|
val baseReplyTo: String? = null,
|
||||||
listOf(
|
val quote: String? = null,
|
||||||
navArgument("nip47") {
|
val fork: String? = null,
|
||||||
type = NavType.StringType
|
val version: String? = null,
|
||||||
nullable = true
|
val draft: String? = null,
|
||||||
defaultValue = null
|
val enableGeolocation: Boolean = false,
|
||||||
},
|
) : Route()
|
||||||
).toImmutableList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
object NewPost :
|
|
||||||
Route(
|
|
||||||
route = "NewPost?message={message}&attachment={attachment}&baseReplyTo={baseReplyTo}"e={quote}&fork={fork}&version={version}&draft={draft}&enableGeolocation={enableGeolocation}",
|
|
||||||
icon = R.drawable.ic_moments,
|
|
||||||
arguments =
|
|
||||||
listOf(
|
|
||||||
navArgument("message") { type = NavType.StringType },
|
|
||||||
navArgument("attachment") { type = NavType.StringType },
|
|
||||||
navArgument("baseReplyTo") { type = NavType.StringType },
|
|
||||||
navArgument("quote") { type = NavType.StringType },
|
|
||||||
navArgument("fork") { type = NavType.StringType },
|
|
||||||
navArgument("version") { type = NavType.StringType },
|
|
||||||
navArgument("draft") { type = NavType.StringType },
|
|
||||||
navArgument("enableGeolocation") { type = NavType.BoolType },
|
|
||||||
).toImmutableList(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isBaseRoute(
|
inline fun <reified T : Route> isBaseRoute(navController: NavHostController): Boolean = navController.currentBackStackEntry?.destination?.hasRoute<T>() == true
|
||||||
navController: NavHostController,
|
|
||||||
startsWith: String,
|
|
||||||
): Boolean =
|
|
||||||
navController.currentBackStackEntry
|
|
||||||
?.destination
|
|
||||||
?.route
|
|
||||||
?.startsWith(startsWith) ?: false
|
|
||||||
|
|
||||||
fun getRouteWithArguments(navController: NavHostController): String? {
|
fun getRouteWithArguments(navController: NavHostController): Route? {
|
||||||
val currentEntry = navController.currentBackStackEntry ?: return null
|
val entry = navController.currentBackStackEntry ?: return null
|
||||||
return getRouteWithArguments(currentEntry.destination, currentEntry.arguments)
|
val dest = entry.destination
|
||||||
}
|
|
||||||
|
|
||||||
fun getRouteWithArguments(navState: State<NavBackStackEntry?>): String? = navState.value?.let { getRouteWithArguments(it.destination, it.arguments) }
|
return when {
|
||||||
|
dest.hasRoute<Route.Home>() -> entry.toRoute<Route.Home>()
|
||||||
|
dest.hasRoute<Route.Message>() -> entry.toRoute<Route.Message>()
|
||||||
|
dest.hasRoute<Route.Video>() -> entry.toRoute<Route.Video>()
|
||||||
|
dest.hasRoute<Route.Discover>() -> entry.toRoute<Route.Discover>()
|
||||||
|
dest.hasRoute<Route.Notification>() -> entry.toRoute<Route.Notification>()
|
||||||
|
|
||||||
private fun getRouteWithArguments(
|
dest.hasRoute<Route.Search>() -> entry.toRoute<Route.Search>()
|
||||||
destination: NavDestination,
|
dest.hasRoute<Route.SecurityFilters>() -> entry.toRoute<Route.SecurityFilters>()
|
||||||
arguments: Bundle?,
|
dest.hasRoute<Route.Bookmarks>() -> entry.toRoute<Route.Bookmarks>()
|
||||||
): String? {
|
dest.hasRoute<Route.ContentDiscovery>() -> entry.toRoute<Route.ContentDiscovery>()
|
||||||
var route = destination.route ?: return null
|
dest.hasRoute<Route.Drafts>() -> entry.toRoute<Route.Drafts>()
|
||||||
arguments?.let { bundle ->
|
dest.hasRoute<Route.Settings>() -> entry.toRoute<Route.Settings>()
|
||||||
destination.arguments.forEach {
|
dest.hasRoute<Route.EditProfile>() -> entry.toRoute<Route.EditProfile>()
|
||||||
val key = it.key
|
|
||||||
val value = it.value.type[bundle, key]?.toString()
|
dest.hasRoute<Route.Profile>() -> entry.toRoute<Route.Profile>()
|
||||||
if (value == null) {
|
dest.hasRoute<Route.Note>() -> entry.toRoute<Route.Note>()
|
||||||
val keyStart = route.indexOf("{$key}")
|
dest.hasRoute<Route.Hashtag>() -> entry.toRoute<Route.Hashtag>()
|
||||||
// if it is a parameter, removes the complete segment `var={key}` and adjust connectors `#`,
|
dest.hasRoute<Route.Geohash>() -> entry.toRoute<Route.Geohash>()
|
||||||
// `&` or `&`
|
dest.hasRoute<Route.Community>() -> entry.toRoute<Route.Community>()
|
||||||
if (keyStart > 0 && route[keyStart - 1] == '=') {
|
|
||||||
val end = keyStart + "{$key}".length
|
dest.hasRoute<Route.RoomByAuthor>() -> entry.toRoute<Route.RoomByAuthor>()
|
||||||
var start = keyStart
|
dest.hasRoute<Route.Channel>() -> entry.toRoute<Route.Channel>()
|
||||||
for (i in keyStart downTo 0) {
|
dest.hasRoute<Route.ChannelMetadataEdit>() -> entry.toRoute<Route.ChannelMetadataEdit>()
|
||||||
if (route[i] == '#' || route[i] == '?' || route[i] == '&') {
|
dest.hasRoute<Route.EventRedirect>() -> entry.toRoute<Route.EventRedirect>()
|
||||||
start = i + 1
|
dest.hasRoute<Route.EditRelays>() -> entry.toRoute<Route.EditRelays>()
|
||||||
break
|
dest.hasRoute<Route.Nip47NWCSetup>() -> entry.toRoute<Route.Nip47NWCSetup>()
|
||||||
}
|
dest.hasRoute<Route.Room>() -> entry.toRoute<Route.Room>()
|
||||||
}
|
dest.hasRoute<Route.NewPost>() -> entry.toRoute<Route.NewPost>()
|
||||||
if (end < route.length && route[end] == '&') {
|
|
||||||
route = route.removeRange(start, end + 1)
|
else -> {
|
||||||
} else if (end < route.length && route[end] == '#') {
|
null
|
||||||
route = route.removeRange(start - 1, end)
|
|
||||||
} else if (end == route.length) {
|
|
||||||
route = route.removeRange(start - 1, end)
|
|
||||||
} else {
|
|
||||||
route = route.removeRange(start, end)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
route = route.replaceFirst("{$key}", "")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
route = route.replaceFirst("{$key}", value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return route
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildNewPostRoute(
|
|
||||||
draftMessage: String? = null,
|
|
||||||
attachment: Uri? = null,
|
|
||||||
baseReplyTo: String? = null,
|
|
||||||
quote: String? = null,
|
|
||||||
fork: String? = null,
|
|
||||||
version: String? = null,
|
|
||||||
draft: String? = null,
|
|
||||||
enableGeolocation: Boolean = false,
|
|
||||||
): String =
|
|
||||||
"NewPost?" +
|
|
||||||
"message=${draftMessage?.let { URLEncoder.encode(it, "utf-8") } ?: ""}&" +
|
|
||||||
"attachment=${attachment?.let { URLEncoder.encode(it.toString(), "utf-8") } ?: ""}&" +
|
|
||||||
"baseReplyTo=${baseReplyTo ?: ""}&" +
|
|
||||||
"quote=${quote ?: ""}&" +
|
|
||||||
"fork=${fork ?: ""}&" +
|
|
||||||
"version=${version ?: ""}&" +
|
|
||||||
"draft=${draft ?: ""}&" +
|
|
||||||
"enableGeolocation=$enableGeolocation&"
|
|
||||||
|
|||||||
@@ -570,7 +570,7 @@ private fun BoxedAuthor(
|
|||||||
nav: INav,
|
nav: INav,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
) {
|
) {
|
||||||
Box(modifier = Size35Modifier.clickable(onClick = { nav.nav(authorRouteFor(note)) })) {
|
Box(modifier = Size35Modifier.clickable(onClick = { authorRouteFor(note)?.let { nav.nav(it) } })) {
|
||||||
WatchAuthorWithBlank(note, Size35Modifier, accountViewModel) { author ->
|
WatchAuthorWithBlank(note, Size35Modifier, accountViewModel) { author ->
|
||||||
WatchUserMetadataAndFollowsAndRenderUserProfilePictureOrDefaultAuthor(
|
WatchUserMetadataAndFollowsAndRenderUserProfilePictureOrDefaultAuthor(
|
||||||
author,
|
author,
|
||||||
|
|||||||
+5
-7
@@ -24,7 +24,6 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.text.ClickableText
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.OpenInNew
|
import androidx.compose.material.icons.filled.OpenInNew
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -390,8 +389,8 @@ fun DisplayNIP05(
|
|||||||
|
|
||||||
NIP05VerifiedSymbol(nip05Verified, NIP05IconSize, accountViewModel)
|
NIP05VerifiedSymbol(nip05Verified, NIP05IconSize, accountViewModel)
|
||||||
|
|
||||||
ClickableText(
|
ClickableTextPrimary(
|
||||||
text = remember(nip05) { AnnotatedString(domain) },
|
text = domain,
|
||||||
onClick = { runCatching { uri.openUri("https://$domain") } },
|
onClick = { runCatching { uri.openUri("https://$domain") } },
|
||||||
style =
|
style =
|
||||||
LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.nip05, fontSize = Font14SP),
|
LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.nip05, fontSize = Font14SP),
|
||||||
@@ -441,7 +440,7 @@ fun DisplayNip05ProfileStatus(
|
|||||||
|
|
||||||
if (user != "_") {
|
if (user != "_") {
|
||||||
Text(
|
Text(
|
||||||
text = remember { AnnotatedString(user + "@") },
|
text = "$user@",
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp),
|
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
@@ -450,10 +449,9 @@ fun DisplayNip05ProfileStatus(
|
|||||||
domainPadStart = 0.dp
|
domainPadStart = 0.dp
|
||||||
}
|
}
|
||||||
|
|
||||||
ClickableText(
|
ClickableTextPrimary(
|
||||||
text = AnnotatedString(domain),
|
text = domain,
|
||||||
onClick = { nip05.let { runCatching { uri.openUri("https://${it.split("@")[1]}") } } },
|
onClick = { nip05.let { runCatching { uri.openUri("https://${it.split("@")[1]}") } } },
|
||||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary),
|
|
||||||
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = domainPadStart),
|
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = domainPadStart),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ import com.vitorpamplona.amethyst.model.User
|
|||||||
import com.vitorpamplona.amethyst.ui.components.SelectTextDialog
|
import com.vitorpamplona.amethyst.ui.components.SelectTextDialog
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
|
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.buildNewPostRoute
|
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
@@ -173,11 +173,11 @@ fun NoteQuickActionMenu(
|
|||||||
note = note,
|
note = note,
|
||||||
onDismiss = onDismiss,
|
onDismiss = onDismiss,
|
||||||
onWantsToEditDraft = {
|
onWantsToEditDraft = {
|
||||||
val route =
|
nav.nav(
|
||||||
buildNewPostRoute(
|
Route.NewPost(
|
||||||
draft = note.idHex,
|
draft = note.idHex,
|
||||||
)
|
),
|
||||||
nav.nav(route)
|
)
|
||||||
},
|
},
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user