mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-06 06:44:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c034ca8af | ||
|
|
0c8c3003aa | ||
|
|
919dabfefa | ||
|
|
fd06193e93 | ||
|
|
b02fa43628 | ||
|
|
7fdf38d17b | ||
|
|
63e036a9bb | ||
|
|
fc98442f8b | ||
|
|
3bbb780d2b | ||
|
|
42ceb0c404 | ||
|
|
046bd7a0e9 | ||
|
|
0af0f745bf | ||
|
|
b24999d78c | ||
|
|
d1278a4477 | ||
|
|
609ccaa635 | ||
|
|
4bae04ad31 | ||
|
|
8bca729edd | ||
|
|
2b2f04f724 | ||
|
|
231af1d3d8 | ||
|
|
d1b44c0ac0 | ||
|
|
f06c3f7376 | ||
|
|
b8e10b040d | ||
|
|
1a4396dbe3 | ||
|
|
856eb0da05 | ||
|
|
ceaeb8e0b9 | ||
|
|
b29ccd9746 | ||
|
|
f82364939f | ||
|
|
286583bba2 | ||
|
|
bc8e5aa458 | ||
|
|
b393fb042f | ||
|
|
0258c5aac2 | ||
|
|
a03a11c4cc | ||
|
|
3896bbab34 | ||
|
|
5459f31000 | ||
|
|
143c3a1444 | ||
|
|
bfbcf11fa3 | ||
|
|
25823b1f39 | ||
|
|
cb02955ac6 | ||
|
|
b440661e0d | ||
|
|
38ba456a22 | ||
|
|
bca941e045 | ||
|
|
dbaadb4595 | ||
|
|
231ca44e1c | ||
|
|
ca00c87a6e | ||
|
|
2a28111583 | ||
|
|
4def80c914 | ||
|
|
916ece1edb | ||
|
|
3c1f57fac0 | ||
|
|
e166c36731 | ||
|
|
a636f4a786 | ||
|
|
bb127506c7 | ||
|
|
92c704d227 | ||
|
|
e764ebbcb2 | ||
|
|
68fb34a8ad | ||
|
|
02683ba829 | ||
|
|
754a1b3a87 | ||
|
|
4b58da7be7 | ||
|
|
1ac9c850e4 | ||
|
|
3cc4dc1414 | ||
|
|
62ae43e863 | ||
|
|
8149a93f2f | ||
|
|
5a60c3a595 | ||
|
|
2ef2f0d1a5 | ||
|
|
52fef33662 | ||
|
|
a5c6f86b35 | ||
|
|
b80008d695 | ||
|
|
fa06aefbf1 | ||
|
|
ab9ede1e26 | ||
|
|
479b1ab20a | ||
|
|
8ed3f84cb7 | ||
|
|
c128e43fe5 | ||
|
|
f9ddf76686 | ||
|
|
b6cb839fe7 | ||
|
|
2b6a48f391 | ||
|
|
d1da863d3c | ||
|
|
99b7a8005a | ||
|
|
3f3367258f | ||
|
|
15ab955a7d | ||
|
|
5cf8b5aa12 | ||
|
|
eba6d03b53 | ||
|
|
de85239046 | ||
|
|
9176151971 | ||
|
|
cbd8acbd01 | ||
|
|
19c9bfa819 | ||
|
|
80c369a6ae | ||
|
|
d0190c87a2 | ||
|
|
62a2cba18b | ||
|
|
59b3ff1b94 | ||
|
|
35423e1bb6 | ||
|
|
e125690000 | ||
|
|
eba5ecfd91 | ||
|
|
3e43a75acd | ||
|
|
04bf9c07ac | ||
|
|
ac58f99560 | ||
|
|
df328ff139 | ||
|
|
60ba7786a0 | ||
|
|
1773ad5a88 | ||
|
|
26faa2bc69 |
@@ -5,6 +5,7 @@ plugins {
|
||||
alias(libs.plugins.jetbrainsKotlinAndroid)
|
||||
alias(libs.plugins.googleServices)
|
||||
alias(libs.plugins.jetbrainsComposeCompiler)
|
||||
alias(libs.plugins.serialization)
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -15,8 +16,8 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk libs.versions.android.minSdk.get().toInteger()
|
||||
targetSdk libs.versions.android.targetSdk.get().toInteger()
|
||||
versionCode 388
|
||||
versionName "0.89.2"
|
||||
versionCode 394
|
||||
versionName "0.89.8"
|
||||
buildConfigField "String", "RELEASE_NOTES_ID", "\"1130badb252bdf62aaf93cb65bfa2bb09e7600c7d6764894f2954555b3b73018\""
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
@@ -282,6 +283,9 @@ dependencies {
|
||||
// Image compression lib
|
||||
implementation libs.zelory.video.compressor
|
||||
|
||||
// Cbor for cashuB format
|
||||
implementation libs.kotlinx.serialization.cbor
|
||||
|
||||
testImplementation libs.junit
|
||||
testImplementation libs.mockk
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.vitorpamplona.amethyst.service.CashuProcessor
|
||||
import com.vitorpamplona.amethyst.service.CashuToken
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class CashuBTest {
|
||||
val cashuTokenA = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9"
|
||||
val cashuTokenB1 = "cashuBo2F0gqJhaUgA_9SLj17PgGFwgaNhYQFhc3hAYWNjMTI0MzVlN2I4NDg0YzNjZjE4NTAxNDkyMThhZjkwZjcxNmE1MmJmNGE1ZWQzNDdlNDhlY2MxM2Y3NzM4OGFjWCECRFODGd5IXVW-07KaZCvuWHk3WrnnpiDhHki6SCQh88-iYWlIAK0mjE0fWCZhcIKjYWECYXN4QDEzMjNkM2Q0NzA3YTU4YWQyZTIzYWRhNGU5ZjFmNDlmNWE1YjRhYzdiNzA4ZWIwZDYxZjczOGY0ODMwN2U4ZWVhY1ghAjRWqhENhLSsdHrr2Cw7AFrKUL9Ffr1XN6RBT6w659lNo2FhAWFzeEA1NmJjYmNiYjdjYzY0MDZiM2ZhNWQ1N2QyMTc0ZjRlZmY4YjQ0MDJiMTc2OTI2ZDNhNTdkM2MzZGNiYjU5ZDU3YWNYIQJzEpxXGeWZN5qXSmJjY8MzxWyvwObQGr5G1YCCgHicY2FtdWh0dHA6Ly9sb2NhbGhvc3Q6MzMzOGF1Y3NhdA"
|
||||
val cashuTokenB2 = "cashuBpGFkb01pbmliaXRzIHJ1bGVzIWFteEpodHRwOi8vbGJ1dGxoNWxmZ2dxNXI3eHBpd2hyYWpkbDdzeHB1cGdhZ2F6eGw2NXc0YzVjZzcyd3RvZmFzYWQub25pb246MzMzOGF0gaJhaUgAm7I9OpEuTmFwg6NhYRhAYWNYIQPfWR0mG80XbGnj6DO8q1NIyjHSGGIEkoWTA6H16HTpx2FzeEA3YThkY2Y5YjNlOGEyNDdjZTMzOWU3MzY5ZTliNGExOWYzMWVhY2I2OWQ4YjBjNjVkYWFlYjcyZDFhY2I5YWQzo2FhGCBhY1ghA55SwCFBc46dwnjbkb87Mzo30T2EE9Ws_nemuFneDegGYXN4QDlkODFjMWEyNjE2ODUzYWQ4MDQ5Y2JjZDFjN2MyNDdhZGQ4M2IzNzM4Mjg2MjBiYWMyZmQ3ZjNlNWE1OGFjZWKjYWEEYWNYIQM-yAQQTR2t6pIAmfmGM8Wxy7ajKVLOaUg7TrV8o-EdVWFzeEBmNmViMTI4ZmJlMDM3MTEzZTkzZjM3NjllYTYwMTk1NmY1N2NkZWNhNTYwOGY0NWUzMDhhZDU0ZmQ4YTQxNWVhYXVjc2F0"
|
||||
|
||||
@Test()
|
||||
fun parseCashuA() {
|
||||
runBlocking {
|
||||
val parsed = (CashuProcessor().parse(cashuTokenA) as GenericLoadable.Loaded<List<CashuToken>>).loaded[0]
|
||||
|
||||
assertEquals(cashuTokenA, parsed.token)
|
||||
assertEquals("https://8333.space:3338", parsed.mint)
|
||||
assertEquals(10, parsed.totalAmount)
|
||||
|
||||
assertEquals(2, parsed.proofs[0].amount)
|
||||
assertEquals("407915bc212be61a77e3e6d2aeb4c727980bda51cd06a6afc29e2861768a7837", parsed.proofs[0].secret)
|
||||
assertEquals("009a1f293253e41e", parsed.proofs[0].id)
|
||||
assertEquals("02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea", parsed.proofs[0].C)
|
||||
|
||||
assertEquals(8, parsed.proofs[1].amount)
|
||||
assertEquals("fe15109314e61d7756b0f8ee0f23a624acaa3f4e042f61433c728c7057b931be", parsed.proofs[1].secret)
|
||||
assertEquals("009a1f293253e41e", parsed.proofs[1].id)
|
||||
assertEquals("029e8e5050b890a7d6c0968db16bc1d5d5fa040ea1de284f6ec69d61299f671059", parsed.proofs[1].C)
|
||||
}
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun parseCashuB() =
|
||||
runBlocking {
|
||||
val parsed = (CashuProcessor().parse(cashuTokenB1) as GenericLoadable.Loaded<List<CashuToken>>).loaded
|
||||
|
||||
assertEquals(cashuTokenB1, parsed[0].token)
|
||||
assertEquals("http://localhost:3338", parsed[0].mint)
|
||||
assertEquals(1, parsed[0].totalAmount)
|
||||
assertEquals(1, parsed[0].proofs[0].amount)
|
||||
assertEquals("acc12435e7b8484c3cf1850149218af90f716a52bf4a5ed347e48ecc13f77388", parsed[0].proofs[0].secret)
|
||||
assertEquals("00ffd48b8f5ecf80", parsed[0].proofs[0].id)
|
||||
assertEquals("0244538319de485d55bed3b29a642bee5879375ab9e7a620e11e48ba482421f3cf", parsed[0].proofs[0].C)
|
||||
|
||||
assertEquals(3, parsed[1].totalAmount)
|
||||
assertEquals(2, parsed[1].proofs[0].amount)
|
||||
assertEquals("1323d3d4707a58ad2e23ada4e9f1f49f5a5b4ac7b708eb0d61f738f48307e8ee", parsed[1].proofs[0].secret)
|
||||
assertEquals("00ad268c4d1f5826", parsed[1].proofs[0].id)
|
||||
assertEquals("023456aa110d84b4ac747aebd82c3b005aca50bf457ebd5737a4414fac3ae7d94d", parsed[1].proofs[0].C)
|
||||
|
||||
assertEquals(1, parsed[1].proofs[1].amount)
|
||||
assertEquals("56bcbcbb7cc6406b3fa5d57d2174f4eff8b4402b176926d3a57d3c3dcbb59d57", parsed[1].proofs[1].secret)
|
||||
assertEquals("00ad268c4d1f5826", parsed[1].proofs[1].id)
|
||||
assertEquals("0273129c5719e599379a974a626363c333c56cafc0e6d01abe46d5808280789c63", parsed[1].proofs[1].C)
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun parseCashuB2() =
|
||||
runBlocking {
|
||||
val parsed = (CashuProcessor().parse(cashuTokenB2) as GenericLoadable.Loaded<List<CashuToken>>).loaded
|
||||
|
||||
assertEquals(cashuTokenB2, parsed[0].token)
|
||||
assertEquals("http://lbutlh5lfggq5r7xpiwhrajdl7sxpupgagazxl65w4c5cg72wtofasad.onion:3338", parsed[0].mint)
|
||||
assertEquals(100, parsed[0].totalAmount)
|
||||
assertEquals(64, parsed[0].proofs[0].amount)
|
||||
assertEquals("7a8dcf9b3e8a247ce339e7369e9b4a19f31eacb69d8b0c65daaeb72d1acb9ad3", parsed[0].proofs[0].secret)
|
||||
assertEquals("009bb23d3a912e4e", parsed[0].proofs[0].id)
|
||||
assertEquals("03df591d261bcd176c69e3e833bcab5348ca31d218620492859303a1f5e874e9c7", parsed[0].proofs[0].C)
|
||||
|
||||
assertEquals(32, parsed[0].proofs[1].amount)
|
||||
assertEquals("9d81c1a2616853ad8049cbcd1c7c247add83b373828620bac2fd7f3e5a58aceb", parsed[0].proofs[1].secret)
|
||||
assertEquals("009bb23d3a912e4e", parsed[0].proofs[1].id)
|
||||
assertEquals("039e52c02141738e9dc278db91bf3b333a37d13d8413d5acfe77a6b859de0de806", parsed[0].proofs[1].C)
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.FileHeader
|
||||
import com.vitorpamplona.amethyst.service.Nip96MediaServers
|
||||
@@ -72,6 +73,7 @@ class ImageUploadTesting {
|
||||
sensitiveContent = null,
|
||||
serverInfo,
|
||||
onProgress = {},
|
||||
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
||||
)
|
||||
|
||||
val url = result.tags!!.first { it[0] == "url" }.get(1)
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.amethyst
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.os.Looper
|
||||
import android.os.StrictMode
|
||||
@@ -30,6 +31,7 @@ import android.util.Log
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import coil.ImageLoader
|
||||
import coil.disk.DiskCache
|
||||
import coil.memory.MemoryCache
|
||||
import com.vitorpamplona.amethyst.service.ots.OkHttpBlockstreamExplorer
|
||||
import com.vitorpamplona.amethyst.service.ots.OkHttpCalendarBuilder
|
||||
import com.vitorpamplona.amethyst.service.playback.VideoCache
|
||||
@@ -66,10 +68,19 @@ class Amethyst : Application() {
|
||||
.Builder()
|
||||
.directory(applicationContext.safeCacheDir.resolve("image_cache"))
|
||||
.maxSizePercent(0.2)
|
||||
.maximumMaxSizeBytes(500L * 1024 * 1024) // 250MB
|
||||
.maximumMaxSizeBytes(1024 * 1024 * 1024) // 1GB
|
||||
.build()
|
||||
}
|
||||
|
||||
val coilMemCache: MemoryCache by lazy {
|
||||
MemoryCache
|
||||
.Builder(this)
|
||||
.maxSizePercent(0.40) // memory heavy app due to profile pics and videos.
|
||||
.build()
|
||||
}
|
||||
|
||||
fun contentResolverFn(): ContentResolver = contentResolver
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
@@ -106,7 +117,12 @@ class Amethyst : Application() {
|
||||
}
|
||||
}
|
||||
|
||||
fun imageLoaderBuilder(): ImageLoader.Builder = ImageLoader.Builder(applicationContext).diskCache { coilCache }
|
||||
fun imageLoaderBuilder(): ImageLoader.Builder =
|
||||
ImageLoader
|
||||
.Builder(this)
|
||||
.diskCache { coilCache }
|
||||
.memoryCache { coilMemCache }
|
||||
.crossfade(true)
|
||||
|
||||
fun encryptedStorage(npub: String? = null): EncryptedSharedPreferences = EncryptedStorage.preferences(instance, npub)
|
||||
|
||||
|
||||
@@ -2251,21 +2251,25 @@ class Account(
|
||||
idHex: String,
|
||||
url: String,
|
||||
relay: String?,
|
||||
blurhash: String?,
|
||||
dim: String?,
|
||||
hash: String?,
|
||||
mimeType: String?,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
ProfileGalleryEntryEvent.create(
|
||||
url = url,
|
||||
eventid = idHex,
|
||||
relayhint = relay,
|
||||
blurhash = blurhash,
|
||||
hash = hash,
|
||||
dimensions = dim,
|
||||
mimeType = mimeType,
|
||||
/*magnetUri = magnetUri,
|
||||
mimeType = headerInfo.mimeType,
|
||||
hash = headerInfo.hash,
|
||||
size = headerInfo.size.toString(),
|
||||
dimensions = headerInfo.dim,
|
||||
blurhash = headerInfo.blurHash,
|
||||
alt = alt,
|
||||
originalHash = originalHash,
|
||||
sensitiveContent = sensitiveContent, */
|
||||
originalHash = originalHash, */
|
||||
signer = signer,
|
||||
) { event ->
|
||||
Client.send(event)
|
||||
@@ -2354,6 +2358,16 @@ class Account(
|
||||
RelayAuthEvent.create(relayUrl, challenge, signer, onReady = onReady)
|
||||
}
|
||||
|
||||
fun createAuthEvent(
|
||||
relayUrls: List<String>,
|
||||
challenge: String,
|
||||
onReady: (RelayAuthEvent) -> Unit,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
RelayAuthEvent.create(relayUrls, challenge, signer, onReady = onReady)
|
||||
}
|
||||
|
||||
fun isInPrivateBookmarks(
|
||||
note: Note,
|
||||
onReady: (Boolean) -> Unit,
|
||||
@@ -2761,19 +2775,22 @@ class Account(
|
||||
// Takes a User's relay list and adds the types of feeds they are active for.
|
||||
fun activeRelays(): Array<RelaySetupInfo>? {
|
||||
val usersRelayList =
|
||||
userProfile().latestContactList?.relays()?.map {
|
||||
val url = RelayUrlFormatter.normalize(it.key)
|
||||
userProfile()
|
||||
.latestContactList
|
||||
?.relays()
|
||||
?.map {
|
||||
val url = RelayUrlFormatter.normalize(it.key)
|
||||
|
||||
val localFeedTypes =
|
||||
localRelays.firstOrNull { localRelay -> RelayUrlFormatter.normalize(localRelay.url) == url }?.feedTypes?.minus(setOf(FeedType.SEARCH, FeedType.WALLET_CONNECT))
|
||||
?: Constants.defaultRelays
|
||||
.filter { defaultRelay -> defaultRelay.url == url }
|
||||
.firstOrNull()
|
||||
?.feedTypes
|
||||
?: Constants.activeTypesGlobalChats
|
||||
val localFeedTypes =
|
||||
localRelays.firstOrNull { localRelay -> RelayUrlFormatter.normalize(localRelay.url) == url }?.feedTypes?.minus(setOf(FeedType.SEARCH, FeedType.WALLET_CONNECT))
|
||||
?: Constants.defaultRelays
|
||||
.filter { defaultRelay -> defaultRelay.url == url }
|
||||
.firstOrNull()
|
||||
?.feedTypes
|
||||
?: Constants.activeTypesGlobalChats
|
||||
|
||||
RelaySetupInfo(url, it.value.read, it.value.write, localFeedTypes)
|
||||
} ?: return null
|
||||
RelaySetupInfo(url, it.value.read, it.value.write, localFeedTypes)
|
||||
}?.ifEmpty { null } ?: return null
|
||||
|
||||
return usersRelayList.toTypedArray()
|
||||
}
|
||||
|
||||
@@ -139,8 +139,6 @@ open class Note(
|
||||
var relays = listOf<RelayBriefInfoCache.RelayBriefInfo>()
|
||||
private set
|
||||
|
||||
var associatedNote: Note? = null
|
||||
|
||||
var lastReactionsDownloadTime: Map<String, EOSETime> = emptyMap()
|
||||
|
||||
fun id() = Hex.decode(idHex)
|
||||
|
||||
@@ -26,21 +26,35 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextViewerState
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
|
||||
object CachedRichTextParser {
|
||||
val richTextCache = LruCache<String, RichTextViewerState>(50)
|
||||
val richTextCache = LruCache<Int, RichTextViewerState>(50)
|
||||
|
||||
fun getCached(content: String): RichTextViewerState? = richTextCache[content]
|
||||
// fun getCached(content: String): RichTextViewerState? = richTextCache[content]
|
||||
|
||||
fun hashCodeCache(
|
||||
content: String,
|
||||
tags: ImmutableListOfLists<String>,
|
||||
callbackUri: String?,
|
||||
): Int {
|
||||
var result = content.hashCode()
|
||||
result = 31 * result + tags.lists.hashCode()
|
||||
if (callbackUri != null) {
|
||||
result = 31 * result + callbackUri.hashCode()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun parseText(
|
||||
content: String,
|
||||
tags: ImmutableListOfLists<String>,
|
||||
callbackUri: String? = null,
|
||||
): RichTextViewerState {
|
||||
val cached = richTextCache[content]
|
||||
val key = hashCodeCache(content, tags, callbackUri)
|
||||
val cached = richTextCache[key]
|
||||
return if (cached != null) {
|
||||
cached
|
||||
} else {
|
||||
val newUrls = RichTextParser().parseText(content, tags, callbackUri)
|
||||
richTextCache.put(content, newUrls)
|
||||
richTextCache.put(key, newUrls)
|
||||
newUrls
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,14 +23,20 @@ package com.vitorpamplona.amethyst.service
|
||||
import android.content.Context
|
||||
import android.util.LruCache
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.cbor.ByteString
|
||||
import kotlinx.serialization.cbor.Cbor
|
||||
import kotlinx.serialization.decodeFromByteArray
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
@@ -42,16 +48,26 @@ data class CashuToken(
|
||||
val token: String,
|
||||
val mint: String,
|
||||
val totalAmount: Long,
|
||||
val proofs: JsonNode,
|
||||
val proofs: List<Proof>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@Immutable
|
||||
class Proof(
|
||||
val amount: Int,
|
||||
val id: String,
|
||||
val secret: String,
|
||||
val C: String,
|
||||
)
|
||||
|
||||
object CachedCashuProcessor {
|
||||
val cashuCache = LruCache<String, GenericLoadable<CashuToken>>(20)
|
||||
val cashuCache = LruCache<String, GenericLoadable<List<CashuToken>>>(20)
|
||||
|
||||
fun cached(token: String): GenericLoadable<CashuToken> = cashuCache[token] ?: GenericLoadable.Loading()
|
||||
fun cached(token: String): GenericLoadable<List<CashuToken>> = cashuCache[token] ?: GenericLoadable.Loading()
|
||||
|
||||
fun parse(token: String): GenericLoadable<CashuToken> {
|
||||
fun parse(token: String): GenericLoadable<List<CashuToken>> {
|
||||
if (cashuCache[token] !is GenericLoadable.Loaded) {
|
||||
checkNotInMainThread()
|
||||
val newCachuData = CashuProcessor().parse(token)
|
||||
|
||||
cashuCache.put(token, newCachuData)
|
||||
@@ -62,25 +78,138 @@ object CachedCashuProcessor {
|
||||
}
|
||||
|
||||
class CashuProcessor {
|
||||
fun parse(cashuToken: String): GenericLoadable<CashuToken> {
|
||||
@Serializable
|
||||
class V3Token(
|
||||
val unit: String?, // unit
|
||||
val memo: String?, // memo
|
||||
val token: List<V3T>?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class V3T(
|
||||
val mint: String,
|
||||
val proofs: List<Proof>,
|
||||
)
|
||||
|
||||
fun parse(cashuToken: String): GenericLoadable<List<CashuToken>> {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (cashuToken.startsWith("cashuA")) {
|
||||
return parseCashuA(cashuToken)
|
||||
}
|
||||
|
||||
if (cashuToken.startsWith("cashuB")) {
|
||||
return parseCashuB(cashuToken)
|
||||
}
|
||||
|
||||
return GenericLoadable.Error("Could not parse this cashu token")
|
||||
}
|
||||
|
||||
fun parseCashuA(cashuToken: String): GenericLoadable<List<CashuToken>> {
|
||||
checkNotInMainThread()
|
||||
|
||||
try {
|
||||
val base64token = cashuToken.replace("cashuA", "")
|
||||
val cashu = jacksonObjectMapper().readTree(String(Base64.getDecoder().decode(base64token)))
|
||||
val token = cashu.get("token").get(0)
|
||||
val proofs = token.get("proofs")
|
||||
val mint = token.get("mint").asText()
|
||||
val cashu = jacksonObjectMapper().readValue<V3Token>(String(Base64.getDecoder().decode(base64token)))
|
||||
|
||||
var totalAmount = 0L
|
||||
for (proof in proofs) {
|
||||
totalAmount += proof.get("amount").asLong()
|
||||
if (cashu.token == null) {
|
||||
return GenericLoadable.Error("No token found")
|
||||
}
|
||||
|
||||
return GenericLoadable.Loaded(CashuToken(cashuToken, mint, totalAmount, proofs))
|
||||
val converted =
|
||||
cashu.token.map { token ->
|
||||
val proofs = token.proofs
|
||||
val mint = token.mint
|
||||
|
||||
var totalAmount = 0L
|
||||
for (proof in proofs) {
|
||||
totalAmount += proof.amount
|
||||
}
|
||||
|
||||
CashuToken(cashuToken, mint, totalAmount, proofs)
|
||||
}
|
||||
|
||||
return GenericLoadable.Loaded(converted)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
return GenericLoadable.Error<CashuToken>("Could not parse this cashu token")
|
||||
return GenericLoadable.Error<List<CashuToken>>("Could not parse this cashu token")
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class V4Token(
|
||||
val m: String, // mint
|
||||
val u: String, // unit
|
||||
val d: String? = null, // memo
|
||||
val t: Array<V4T>?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class V4T(
|
||||
@ByteString
|
||||
val i: ByteArray, // identifier
|
||||
val p: Array<V4Proof>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class V4Proof(
|
||||
val a: Int, // amount
|
||||
val s: String, // secret
|
||||
@ByteString
|
||||
val c: ByteArray, // signature
|
||||
val d: V4DleqProof? = null, // no idea what this is
|
||||
val w: String? = null, // witness
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class V4DleqProof(
|
||||
@ByteString
|
||||
val e: ByteArray,
|
||||
@ByteString
|
||||
val s: ByteArray,
|
||||
@ByteString
|
||||
val r: ByteArray,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
fun parseCashuB(cashuToken: String): GenericLoadable<List<CashuToken>> {
|
||||
checkNotInMainThread()
|
||||
|
||||
try {
|
||||
val base64token = cashuToken.replace("cashuB", "")
|
||||
|
||||
val parser = Cbor { ignoreUnknownKeys = true }
|
||||
|
||||
val v4Token = parser.decodeFromByteArray<V4Token>(Base64.getUrlDecoder().decode(base64token))
|
||||
|
||||
val v4proofs = v4Token.t ?: return GenericLoadable.Error("No token found")
|
||||
|
||||
val converted =
|
||||
v4proofs.map { id ->
|
||||
val proofs =
|
||||
id.p.map {
|
||||
Proof(
|
||||
it.a,
|
||||
id.i.toHexKey(),
|
||||
it.s,
|
||||
it.c.toHexKey(),
|
||||
)
|
||||
}
|
||||
val mint = v4Token.m
|
||||
|
||||
var totalAmount = 0L
|
||||
for (proof in proofs) {
|
||||
totalAmount += proof.amount
|
||||
}
|
||||
|
||||
CashuToken(cashuToken, mint, totalAmount, proofs)
|
||||
}
|
||||
|
||||
return GenericLoadable.Loaded(converted)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
if (e is CancellationException) throw e
|
||||
return GenericLoadable.Error("Could not parse this cashu token")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +338,21 @@ class CashuProcessor {
|
||||
val factory = Event.mapper.nodeFactory
|
||||
|
||||
val jsonObject = factory.objectNode()
|
||||
jsonObject.put("proofs", token.proofs)
|
||||
|
||||
jsonObject.replace(
|
||||
"proofs",
|
||||
factory.arrayNode(token.proofs.size).apply {
|
||||
token.proofs.forEach {
|
||||
addObject().apply {
|
||||
put("amount", it.amount)
|
||||
put("id", it.id)
|
||||
put("secret", it.secret)
|
||||
put("C", it.C)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
jsonObject.put("pr", invoice)
|
||||
|
||||
val mediaType = "application/json; charset=utf-8".toMediaType()
|
||||
|
||||
@@ -112,7 +112,7 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
|
||||
|
||||
fun createFollowMetadataAndReleaseFilter(): TypedFilter? {
|
||||
val follows = account.liveHomeFollowLists.value?.users
|
||||
val followSet = follows?.plus(account.userProfile().pubkeyHex)?.toList()?.ifEmpty { null }
|
||||
val followSet = follows?.plus(account.userProfile().pubkeyHex)?.shuffled()?.ifEmpty { null }
|
||||
|
||||
return if (followSet != null) {
|
||||
TypedFilter(
|
||||
@@ -124,7 +124,7 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
|
||||
MetadataEvent.KIND,
|
||||
AdvertisedRelayListEvent.KIND,
|
||||
),
|
||||
authors = followSet,
|
||||
authors = followSet.take(500),
|
||||
since =
|
||||
latestEOSEs.users[account.userProfile()]
|
||||
?.followList
|
||||
|
||||
+6
@@ -319,6 +319,9 @@ fun groupByEOSEPresence(notes: Set<Note>): Collection<List<Note>> =
|
||||
.sorted()
|
||||
.joinToString(",")
|
||||
}.values
|
||||
.map {
|
||||
it.sortedBy { it.idHex } // important to keep in order otherwise the Relay thinks the filter has changed and we REQ again
|
||||
}
|
||||
|
||||
fun groupByEOSEPresence(users: Iterable<User>): Collection<List<User>> =
|
||||
users
|
||||
@@ -327,6 +330,9 @@ fun groupByEOSEPresence(users: Iterable<User>): Collection<List<User>> =
|
||||
.sorted()
|
||||
.joinToString(",")
|
||||
}.values
|
||||
.map {
|
||||
it.sortedBy { it.pubkeyHex } // important to keep in order otherwise the Relay thinks the filter has changed and we REQ again
|
||||
}
|
||||
|
||||
fun findMinimumEOSEs(notes: List<Note>): Map<String, EOSETime> {
|
||||
val minLatestEOSEs = mutableMapOf<String, EOSETime>()
|
||||
|
||||
+6
-7
@@ -64,6 +64,7 @@ object NostrSingleUserDataSource : AmethystNostrDataSource("SingleUserFeed") {
|
||||
.map { group ->
|
||||
val groupIds = group.map { it.pubkeyHex }
|
||||
val minEOSEs = findMinimumEOSEsForUsers(group)
|
||||
|
||||
listOf(
|
||||
TypedFilter(
|
||||
types = EVENT_FINDER_TYPES,
|
||||
@@ -92,13 +93,11 @@ object NostrSingleUserDataSource : AmethystNostrDataSource("SingleUserFeed") {
|
||||
checkNotInMainThread()
|
||||
|
||||
usersToWatch.forEach {
|
||||
if (it.latestMetadata != null) {
|
||||
val eose = it.latestEOSEs[relayUrl]
|
||||
if (eose == null) {
|
||||
it.latestEOSEs = it.latestEOSEs + Pair(relayUrl, EOSETime(time))
|
||||
} else {
|
||||
eose.time = time
|
||||
}
|
||||
val eose = it.latestEOSEs[relayUrl]
|
||||
if (eose == null) {
|
||||
it.latestEOSEs = it.latestEOSEs + Pair(relayUrl, EOSETime(time))
|
||||
} else {
|
||||
eose.time = time
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-6
@@ -55,7 +55,7 @@ class RegisterAccounts(
|
||||
|
||||
private suspend fun signAllAuths(
|
||||
notificationToken: String,
|
||||
remainingTos: List<Pair<Account, String>>,
|
||||
remainingTos: List<Pair<Account, List<String>>>,
|
||||
output: MutableList<RelayAuthEvent>,
|
||||
onReady: (List<RelayAuthEvent>) -> Unit,
|
||||
) {
|
||||
@@ -100,7 +100,7 @@ class RegisterAccounts(
|
||||
) {
|
||||
val readyToSend =
|
||||
accounts
|
||||
.map {
|
||||
.mapNotNull {
|
||||
Log.d(tag, "Register Account ${it.npub}")
|
||||
|
||||
val acc = LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub)
|
||||
@@ -131,13 +131,17 @@ class RegisterAccounts(
|
||||
|
||||
Log.d(tag, "Register Account ${acc.userProfile().toBestDisplayName()} Kind3 Reads ${readKind3Relays.joinToString(", ")}")
|
||||
|
||||
(nip65Read + nip17Read + readKind3Relays).map {
|
||||
Pair(acc, it)
|
||||
val relays = (nip65Read + nip17Read + readKind3Relays)
|
||||
|
||||
if (relays.isNotEmpty()) {
|
||||
Pair(acc, relays)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
emptyList<Pair<Account, String>>()
|
||||
null
|
||||
}
|
||||
}.flatten()
|
||||
}
|
||||
|
||||
val listOfAuthEvents = mutableListOf<RelayAuthEvent>()
|
||||
signAllAuths(
|
||||
|
||||
@@ -30,6 +30,7 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
@@ -72,6 +73,7 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
Log.d("Lifetime Event", "MainActivity.onCreate")
|
||||
|
||||
@@ -54,9 +54,9 @@ fun stringRes(
|
||||
String
|
||||
.format(
|
||||
LocalConfiguration.current.locales.get(0),
|
||||
resourceCache.get(id) ?: stringResource(id),
|
||||
resourceCache.get(id) ?: stringResource(id).also { resourceCache.put(id, it) },
|
||||
*args,
|
||||
).also { resourceCache.put(id, it) }
|
||||
)
|
||||
|
||||
fun stringRes(
|
||||
ctx: Context,
|
||||
|
||||
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.components
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -31,11 +30,8 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -53,14 +49,12 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.fasterxml.jackson.databind.node.TextNode
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.hashtags.Cashu
|
||||
import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons
|
||||
@@ -69,7 +63,6 @@ import com.vitorpamplona.amethyst.service.CachedCashuProcessor
|
||||
import com.vitorpamplona.amethyst.service.CashuToken
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
|
||||
import com.vitorpamplona.amethyst.ui.note.CashuIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.CopyIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.OpenInNewIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapIcon
|
||||
@@ -77,14 +70,11 @@ import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size18Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.SmallishBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -108,8 +98,8 @@ fun CashuPreview(
|
||||
|
||||
CrossfadeIfEnabled(targetState = cashuData, label = "CashuPreview", accountViewModel = accountViewModel) {
|
||||
when (it) {
|
||||
is GenericLoadable.Loaded<CashuToken> -> CashuPreview(it.loaded, accountViewModel)
|
||||
is GenericLoadable.Error<CashuToken> ->
|
||||
is GenericLoadable.Loaded<List<CashuToken>> -> CashuPreview(it.loaded, accountViewModel)
|
||||
is GenericLoadable.Error<List<CashuToken>> ->
|
||||
Text(
|
||||
text = "$cashutoken ",
|
||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||
@@ -121,10 +111,12 @@ fun CashuPreview(
|
||||
|
||||
@Composable
|
||||
fun CashuPreview(
|
||||
token: CashuToken,
|
||||
tokens: List<CashuToken>,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
CashuPreviewNew(token, accountViewModel::meltCashu, accountViewModel::toast)
|
||||
tokens.forEach {
|
||||
CashuPreviewNew(it, accountViewModel::meltCashu, accountViewModel::toast)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -137,14 +129,8 @@ fun CashuPreviewPreview() {
|
||||
|
||||
AmethystTheme(sharedPrefsViewModel = sharedPreferencesViewModel) {
|
||||
Column {
|
||||
CashuPreview(
|
||||
token = CashuToken("token", "mint", 32400, TextNode("")),
|
||||
melt = { token, context, onDone -> },
|
||||
toast = { title, message -> },
|
||||
)
|
||||
|
||||
CashuPreviewNew(
|
||||
token = CashuToken("token", "mint", 32400, TextNode("")),
|
||||
token = CashuToken("token", "mint", 32400, listOf()),
|
||||
melt = { token, context, onDone -> },
|
||||
toast = { title, message -> },
|
||||
)
|
||||
@@ -152,148 +138,6 @@ fun CashuPreviewPreview() {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CashuPreview(
|
||||
token: CashuToken,
|
||||
melt: (CashuToken, Context, (String, String) -> Unit) -> Unit,
|
||||
toast: (String, String) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 20.dp, end = 20.dp, top = 10.dp, bottom = 10.dp)
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(1.dp, MaterialTheme.colorScheme.subtleBorder, QuoteBorder),
|
||||
) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(20.dp),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 10.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CustomHashTagIcons.Cashu,
|
||||
null,
|
||||
modifier = Size20Modifier,
|
||||
tint = Color.Unspecified,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringRes(R.string.cashu),
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.W500,
|
||||
modifier = Modifier.padding(start = 10.dp),
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
|
||||
Text(
|
||||
text = "${token.totalAmount} ${stringRes(id = R.string.sats)}",
|
||||
fontSize = 25.sp,
|
||||
fontWeight = FontWeight.W500,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 10.dp),
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(top = 5.dp)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
var isRedeeming by remember { mutableStateOf(false) }
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
isRedeeming = true
|
||||
melt(token, context) { title, message ->
|
||||
toast(title, message)
|
||||
isRedeeming = false
|
||||
}
|
||||
},
|
||||
shape = QuoteBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
) {
|
||||
if (isRedeeming) {
|
||||
LoadingAnimation()
|
||||
} else {
|
||||
ZapIcon(Size20Modifier, tint = Color.White)
|
||||
}
|
||||
Spacer(DoubleHorzSpacer)
|
||||
|
||||
Text(
|
||||
stringRes(id = R.string.cashu_redeem_to_zap),
|
||||
color = Color.White,
|
||||
fontSize = 16.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
Button(
|
||||
onClick = {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("cashu://${token.token}"))
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
|
||||
startActivity(context, intent, null)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
toast(stringRes(context, R.string.cashu), stringRes(context, R.string.cashu_no_wallet_found))
|
||||
}
|
||||
},
|
||||
shape = QuoteBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
) {
|
||||
CashuIcon(Size20Modifier)
|
||||
Spacer(DoubleHorzSpacer)
|
||||
Text(
|
||||
stringRes(id = R.string.cashu_redeem_to_cashu),
|
||||
color = Color.White,
|
||||
fontSize = 16.sp,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
Button(
|
||||
onClick = {
|
||||
// Copying the token to clipboard
|
||||
clipboardManager.setText(AnnotatedString(token.token))
|
||||
},
|
||||
shape = QuoteBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
) {
|
||||
CopyIcon(Size20Modifier, Color.White)
|
||||
Spacer(DoubleHorzSpacer)
|
||||
Text(stringRes(id = R.string.cashu_copy_token), color = Color.White, fontSize = 16.sp)
|
||||
}
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CashuPreviewNew(
|
||||
token: CashuToken,
|
||||
@@ -339,6 +183,12 @@ fun CashuPreviewNew(
|
||||
fontSize = 20.sp,
|
||||
modifier = Modifier.padding(top = 5.dp),
|
||||
)
|
||||
Text(
|
||||
text = "Mint: " + token.mint.replace("https://", ""),
|
||||
fontSize = 7.sp,
|
||||
color = Color.Gray,
|
||||
modifier = Modifier.padding(start = 5.dp, bottom = 1.dp),
|
||||
)
|
||||
|
||||
Row(modifier = Modifier.padding(top = 5.dp)) {
|
||||
var isRedeeming by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -30,6 +30,7 @@ import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.VideoView
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
@@ -40,9 +41,9 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
@@ -217,6 +218,37 @@ fun VideoView(
|
||||
onControllerVisibilityChanged: ((Boolean) -> Unit)? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
alwaysShowVideo: Boolean = false,
|
||||
) {
|
||||
val borderModifier =
|
||||
if (roundedCorner) {
|
||||
MaterialTheme.colorScheme.imageModifier
|
||||
} else if (gallery) {
|
||||
MaterialTheme.colorScheme.videoGalleryModifier
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
VideoView(videoUri, mimeType, title, thumb, borderModifier, isFiniteHeight, waveform, artworkUri, authorName, dimensions, blurhash, nostrUriCallback, onDialog, onControllerVisibilityChanged, accountViewModel, alwaysShowVideo)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VideoView(
|
||||
videoUri: String,
|
||||
mimeType: String?,
|
||||
title: String? = null,
|
||||
thumb: VideoThumb? = null,
|
||||
borderModifier: Modifier,
|
||||
isFiniteHeight: Boolean,
|
||||
waveform: ImmutableList<Int>? = null,
|
||||
artworkUri: String? = null,
|
||||
authorName: String? = null,
|
||||
dimensions: String? = null,
|
||||
blurhash: String? = null,
|
||||
nostrUriCallback: String? = null,
|
||||
onDialog: ((Boolean) -> Unit)? = null,
|
||||
onControllerVisibilityChanged: ((Boolean) -> Unit)? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
alwaysShowVideo: Boolean = false,
|
||||
) {
|
||||
val defaultToStart by remember(videoUri) { mutableStateOf(DEFAULT_MUTED_SETTING.value) }
|
||||
|
||||
@@ -248,8 +280,7 @@ fun VideoView(
|
||||
defaultToStart = defaultToStart,
|
||||
title = title,
|
||||
thumb = thumb,
|
||||
roundedCorner = roundedCorner,
|
||||
gallery = gallery,
|
||||
borderModifier = borderModifier,
|
||||
isFiniteHeight = isFiniteHeight,
|
||||
waveform = waveform,
|
||||
artworkUri = artworkUri,
|
||||
@@ -273,19 +304,12 @@ fun VideoView(
|
||||
}
|
||||
|
||||
Box(modifier, contentAlignment = Alignment.Center) {
|
||||
val image =
|
||||
if (roundedCorner) {
|
||||
MaterialTheme.colorScheme.imageModifier
|
||||
} else {
|
||||
Modifier.fillMaxWidth()
|
||||
}
|
||||
|
||||
// Always displays Blurharh to avoid size flickering
|
||||
DisplayBlurHash(
|
||||
blurhash,
|
||||
null,
|
||||
if (isFiniteHeight) ContentScale.FillWidth else ContentScale.FillWidth,
|
||||
if (ratio != null) image.aspectRatio(ratio) else modifier,
|
||||
if (ratio != null) borderModifier.aspectRatio(ratio) else borderModifier,
|
||||
)
|
||||
|
||||
if (!automaticallyStartPlayback.value) {
|
||||
@@ -302,7 +326,7 @@ fun VideoView(
|
||||
defaultToStart = defaultToStart,
|
||||
title = title,
|
||||
thumb = thumb,
|
||||
roundedCorner = roundedCorner,
|
||||
borderModifier = borderModifier,
|
||||
isFiniteHeight = isFiniteHeight,
|
||||
waveform = waveform,
|
||||
artworkUri = artworkUri,
|
||||
@@ -326,9 +350,9 @@ fun VideoViewInner(
|
||||
defaultToStart: Boolean = false,
|
||||
title: String? = null,
|
||||
thumb: VideoThumb? = null,
|
||||
roundedCorner: Boolean,
|
||||
gallery: Boolean = false,
|
||||
showControls: Boolean = false,
|
||||
isFiniteHeight: Boolean,
|
||||
borderModifier: Modifier,
|
||||
waveform: ImmutableList<Int>? = null,
|
||||
artworkUri: String? = null,
|
||||
authorName: String? = null,
|
||||
@@ -338,7 +362,7 @@ fun VideoViewInner(
|
||||
onDialog: ((Boolean) -> Unit)? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
VideoPlayerActiveMutex(videoUri) { modifier, activeOnScreen ->
|
||||
VideoPlayerActiveMutex(videoUri) { videoModifier, activeOnScreen ->
|
||||
GetMediaItem(videoUri, title, artworkUri, authorName) { mediaItem ->
|
||||
GetVideoController(
|
||||
mediaItem = mediaItem,
|
||||
@@ -351,15 +375,15 @@ fun VideoViewInner(
|
||||
mimeType = mimeType,
|
||||
controller = controller,
|
||||
thumbData = thumb,
|
||||
roundedCorner = roundedCorner,
|
||||
gallery = gallery,
|
||||
hideControls = showControls,
|
||||
isFiniteHeight = isFiniteHeight,
|
||||
nostrUriCallback = nostrUriCallback,
|
||||
waveform = waveform,
|
||||
keepPlaying = keepPlaying,
|
||||
automaticallyStartPlayback = automaticallyStartPlayback,
|
||||
activeOnScreen = activeOnScreen,
|
||||
modifier = modifier,
|
||||
borderModifier = borderModifier,
|
||||
videoModifier = videoModifier,
|
||||
onControllerVisibilityChanged = onControllerVisibilityChanged,
|
||||
onDialog = onDialog,
|
||||
accountViewModel = accountViewModel,
|
||||
@@ -655,9 +679,9 @@ fun VideoPlayerActiveMutex(
|
||||
onDispose { trackingVideos.remove(myCache) }
|
||||
}
|
||||
|
||||
val myModifier =
|
||||
val videoModifier =
|
||||
remember(videoUri) {
|
||||
Modifier.fillMaxWidth().defaultMinSize(minHeight = 70.dp).onVisiblePositionChanges { distanceToCenter ->
|
||||
Modifier.fillMaxWidth().heightIn(min = 100.dp).onVisiblePositionChanges { distanceToCenter ->
|
||||
myCache.distanceToCenter = distanceToCenter
|
||||
|
||||
if (distanceToCenter != null) {
|
||||
@@ -684,7 +708,7 @@ fun VideoPlayerActiveMutex(
|
||||
}
|
||||
}
|
||||
|
||||
inner(myModifier, active)
|
||||
inner(videoModifier, active)
|
||||
}
|
||||
|
||||
@Stable
|
||||
@@ -699,15 +723,15 @@ private fun RenderVideoPlayer(
|
||||
mimeType: String?,
|
||||
controller: MediaController,
|
||||
thumbData: VideoThumb?,
|
||||
roundedCorner: Boolean,
|
||||
gallery: Boolean = false,
|
||||
hideControls: Boolean = false,
|
||||
isFiniteHeight: Boolean,
|
||||
nostrUriCallback: String?,
|
||||
waveform: ImmutableList<Int>? = null,
|
||||
keepPlaying: MutableState<Boolean>,
|
||||
automaticallyStartPlayback: State<Boolean>,
|
||||
activeOnScreen: MutableState<Boolean>,
|
||||
modifier: Modifier,
|
||||
borderModifier: Modifier,
|
||||
videoModifier: Modifier,
|
||||
onControllerVisibilityChanged: ((Boolean) -> Unit)? = null,
|
||||
onDialog: ((Boolean) -> Unit)?,
|
||||
accountViewModel: AccountViewModel,
|
||||
@@ -716,27 +740,9 @@ private fun RenderVideoPlayer(
|
||||
|
||||
val controllerVisible = remember(controller) { mutableStateOf(false) }
|
||||
|
||||
Box {
|
||||
val borders = MaterialTheme.colorScheme.imageModifier
|
||||
val bordersSquare = MaterialTheme.colorScheme.videoGalleryModifier
|
||||
val myModifier =
|
||||
remember(controller) {
|
||||
if (roundedCorner) {
|
||||
modifier.then(
|
||||
borders.defaultMinSize(minHeight = 75.dp).align(Alignment.Center),
|
||||
)
|
||||
} else if (gallery) {
|
||||
Modifier
|
||||
modifier.then(
|
||||
bordersSquare.defaultMinSize(minHeight = 75.dp).align(Alignment.Center),
|
||||
)
|
||||
} else {
|
||||
modifier.fillMaxWidth().defaultMinSize(minHeight = 75.dp).align(Alignment.Center)
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = borderModifier) {
|
||||
AndroidView(
|
||||
modifier = myModifier,
|
||||
modifier = videoModifier,
|
||||
factory = { context: Context ->
|
||||
PlayerView(context).apply {
|
||||
player = controller
|
||||
@@ -748,7 +754,7 @@ private fun RenderVideoPlayer(
|
||||
setBackgroundColor(Color.Transparent.toArgb())
|
||||
setShutterBackgroundColor(Color.Transparent.toArgb())
|
||||
controllerAutoShow = false
|
||||
useController = !gallery
|
||||
useController = !hideControls
|
||||
thumbData?.thumb?.let { defaultArtwork = it }
|
||||
hideController()
|
||||
resizeMode =
|
||||
@@ -757,7 +763,7 @@ private fun RenderVideoPlayer(
|
||||
} else {
|
||||
AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
|
||||
}
|
||||
if (!gallery) {
|
||||
if (!hideControls) {
|
||||
onDialog?.let { innerOnDialog ->
|
||||
setFullscreenButtonClickListener {
|
||||
controller.pause()
|
||||
@@ -776,7 +782,8 @@ private fun RenderVideoPlayer(
|
||||
)
|
||||
|
||||
waveform?.let { Waveform(it, controller, remember { Modifier.align(Alignment.Center) }) }
|
||||
if (!gallery) {
|
||||
|
||||
if (!hideControls) {
|
||||
val startingMuteState = remember(controller) { controller.volume < 0.001 }
|
||||
|
||||
MuteButton(
|
||||
@@ -824,7 +831,7 @@ private fun RenderVideoPlayer(
|
||||
}
|
||||
|
||||
AnimatedShareButton(controllerVisible, Modifier.align(Alignment.TopEnd).padding(end = Size165dp)) { popupExpanded, toggle ->
|
||||
ShareImageAction(accountViewModel = accountViewModel, popupExpanded, videoUri, nostrUriCallback, toggle)
|
||||
ShareImageAction(accountViewModel = accountViewModel, popupExpanded, videoUri, nostrUriCallback, null, null, null, mimeType, toggle)
|
||||
}
|
||||
} else {
|
||||
controller.volume = 0f
|
||||
|
||||
+25
-4
@@ -89,6 +89,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -388,6 +389,12 @@ private fun RenderImageOrVideo(
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val automaticallyStartPlayback = remember { mutableStateOf<Boolean>(true) }
|
||||
val contentScale =
|
||||
if (isFiniteHeight) {
|
||||
ContentScale.Fit
|
||||
} else {
|
||||
ContentScale.FillWidth
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) {
|
||||
if (content is MediaUrlImage) {
|
||||
@@ -405,21 +412,28 @@ private fun RenderImageOrVideo(
|
||||
|
||||
UrlImageView(
|
||||
content = content,
|
||||
contentScale = contentScale,
|
||||
mainImageModifier = mainModifier,
|
||||
loadedImageModifier = Modifier.fillMaxWidth(),
|
||||
isFiniteHeight = isFiniteHeight,
|
||||
controllerVisible = controllerVisible,
|
||||
accountViewModel = accountViewModel,
|
||||
alwayShowImage = true,
|
||||
)
|
||||
} else if (content is MediaUrlVideo) {
|
||||
val borderModifier =
|
||||
if (roundedCorner) {
|
||||
MaterialTheme.colorScheme.imageModifier
|
||||
} else {
|
||||
Modifier.fillMaxWidth()
|
||||
}
|
||||
|
||||
VideoViewInner(
|
||||
videoUri = content.url,
|
||||
mimeType = content.mimeType,
|
||||
title = content.description,
|
||||
artworkUri = content.artworkUri,
|
||||
authorName = content.authorName,
|
||||
roundedCorner = roundedCorner,
|
||||
borderModifier = borderModifier,
|
||||
isFiniteHeight = isFiniteHeight,
|
||||
automaticallyStartPlayback = automaticallyStartPlayback,
|
||||
onControllerVisibilityChanged = onControllerVisibilityChanged,
|
||||
@@ -440,14 +454,21 @@ private fun RenderImageOrVideo(
|
||||
|
||||
LocalImageView(
|
||||
content = content,
|
||||
contentScale = contentScale,
|
||||
mainImageModifier = mainModifier,
|
||||
loadedImageModifier = Modifier.fillMaxWidth(),
|
||||
isFiniteHeight = isFiniteHeight,
|
||||
controllerVisible = controllerVisible,
|
||||
accountViewModel = accountViewModel,
|
||||
alwayShowImage = true,
|
||||
)
|
||||
} else if (content is MediaLocalVideo) {
|
||||
val borderModifier =
|
||||
if (roundedCorner) {
|
||||
MaterialTheme.colorScheme.imageModifier
|
||||
} else {
|
||||
Modifier.fillMaxWidth()
|
||||
}
|
||||
|
||||
content.localFile?.let {
|
||||
VideoViewInner(
|
||||
videoUri = it.toUri().toString(),
|
||||
@@ -455,7 +476,7 @@ private fun RenderImageOrVideo(
|
||||
title = content.description,
|
||||
artworkUri = content.artworkUri,
|
||||
authorName = content.authorName,
|
||||
roundedCorner = roundedCorner,
|
||||
borderModifier = borderModifier,
|
||||
isFiniteHeight = isFiniteHeight,
|
||||
automaticallyStartPlayback = automaticallyStartPlayback,
|
||||
onControllerVisibilityChanged = onControllerVisibilityChanged,
|
||||
|
||||
+107
-31
@@ -79,6 +79,7 @@ import com.vitorpamplona.amethyst.commons.richtext.MediaUrlContent
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
|
||||
import com.vitorpamplona.amethyst.service.BlurHashRequester
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.actions.InformationDialog
|
||||
import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
|
||||
import com.vitorpamplona.amethyst.ui.note.BlankNote
|
||||
@@ -94,6 +95,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size30dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size75dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.hashVerifierMark
|
||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.videoGalleryModifier
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.Nip19Bech32
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
@@ -116,6 +118,13 @@ fun ZoomableContentView(
|
||||
) {
|
||||
var dialogOpen by remember(content) { mutableStateOf(false) }
|
||||
|
||||
val contentScale =
|
||||
if (isFiniteHeight) {
|
||||
ContentScale.Fit
|
||||
} else {
|
||||
ContentScale.FillWidth
|
||||
}
|
||||
|
||||
when (content) {
|
||||
is MediaUrlImage ->
|
||||
SensitivityWarning(content.contentWarning != null, accountViewModel) {
|
||||
@@ -123,7 +132,7 @@ fun ZoomableContentView(
|
||||
val mainImageModifier = Modifier.fillMaxWidth().clickable { dialogOpen = true }
|
||||
val loadedImageModifier = if (roundedCorner) MaterialTheme.colorScheme.imageModifier else Modifier.fillMaxWidth()
|
||||
|
||||
UrlImageView(content, mainImageModifier, loadedImageModifier, isFiniteHeight, controllerVisible, accountViewModel = accountViewModel)
|
||||
UrlImageView(content, contentScale, mainImageModifier, loadedImageModifier, controllerVisible, accountViewModel = accountViewModel)
|
||||
}
|
||||
}
|
||||
is MediaUrlVideo ->
|
||||
@@ -150,7 +159,7 @@ fun ZoomableContentView(
|
||||
val mainImageModifier = Modifier.fillMaxWidth().clickable { dialogOpen = true }
|
||||
val loadedImageModifier = if (roundedCorner) MaterialTheme.colorScheme.imageModifier else Modifier.fillMaxWidth()
|
||||
|
||||
LocalImageView(content, mainImageModifier, loadedImageModifier, isFiniteHeight, controllerVisible, accountViewModel = accountViewModel)
|
||||
LocalImageView(content, contentScale, mainImageModifier, loadedImageModifier, controllerVisible, accountViewModel = accountViewModel)
|
||||
}
|
||||
is MediaLocalVideo ->
|
||||
content.localFile?.let {
|
||||
@@ -176,6 +185,67 @@ fun ZoomableContentView(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GalleryContentView(
|
||||
content: BaseMediaContent,
|
||||
roundedCorner: Boolean,
|
||||
isFiniteHeight: Boolean,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
when (content) {
|
||||
is MediaUrlImage ->
|
||||
SensitivityWarning(content.contentWarning != null, accountViewModel) {
|
||||
TwoSecondController(content) { controllerVisible ->
|
||||
val mainImageModifier = Modifier.fillMaxWidth()
|
||||
val loadedImageModifier = if (roundedCorner) MaterialTheme.colorScheme.imageModifier else Modifier.fillMaxWidth()
|
||||
|
||||
UrlImageView(content, ContentScale.Crop, mainImageModifier, loadedImageModifier, controllerVisible, accountViewModel = accountViewModel)
|
||||
}
|
||||
}
|
||||
is MediaUrlVideo ->
|
||||
SensitivityWarning(content.contentWarning != null, accountViewModel) {
|
||||
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
VideoView(
|
||||
videoUri = content.url,
|
||||
mimeType = content.mimeType,
|
||||
title = content.description,
|
||||
artworkUri = content.artworkUri,
|
||||
borderModifier = MaterialTheme.colorScheme.videoGalleryModifier,
|
||||
authorName = content.authorName,
|
||||
dimensions = content.dim,
|
||||
blurhash = content.blurhash,
|
||||
isFiniteHeight = isFiniteHeight,
|
||||
nostrUriCallback = content.uri,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
is MediaLocalImage ->
|
||||
TwoSecondController(content) { controllerVisible ->
|
||||
val mainImageModifier = Modifier.fillMaxWidth()
|
||||
val loadedImageModifier = if (roundedCorner) MaterialTheme.colorScheme.imageModifier else Modifier.fillMaxWidth()
|
||||
|
||||
LocalImageView(content, ContentScale.Crop, mainImageModifier, loadedImageModifier, controllerVisible, accountViewModel = accountViewModel)
|
||||
}
|
||||
is MediaLocalVideo ->
|
||||
content.localFile?.let {
|
||||
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
VideoView(
|
||||
videoUri = it.toUri().toString(),
|
||||
mimeType = content.mimeType,
|
||||
title = content.description,
|
||||
artworkUri = content.artworkUri,
|
||||
authorName = content.authorName,
|
||||
borderModifier = MaterialTheme.colorScheme.videoGalleryModifier,
|
||||
isFiniteHeight = isFiniteHeight,
|
||||
nostrUriCallback = content.uri,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TwoSecondController(
|
||||
content: BaseMediaContent,
|
||||
@@ -194,30 +264,24 @@ fun TwoSecondController(
|
||||
@Composable
|
||||
fun LocalImageView(
|
||||
content: MediaLocalImage,
|
||||
contentScale: ContentScale,
|
||||
mainImageModifier: Modifier,
|
||||
loadedImageModifier: Modifier,
|
||||
isFiniteHeight: Boolean,
|
||||
controllerVisible: MutableState<Boolean>,
|
||||
accountViewModel: AccountViewModel,
|
||||
alwayShowImage: Boolean = false,
|
||||
) {
|
||||
if (content.localFileExists()) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
val showImage =
|
||||
remember {
|
||||
mutableStateOf(
|
||||
if (alwayShowImage) true else accountViewModel.settings.showImages.value,
|
||||
)
|
||||
}
|
||||
val showImage =
|
||||
remember {
|
||||
mutableStateOf(
|
||||
if (alwayShowImage) true else accountViewModel.settings.showImages.value,
|
||||
)
|
||||
}
|
||||
|
||||
val contentScale =
|
||||
remember {
|
||||
if (isFiniteHeight) ContentScale.Fit else ContentScale.FillWidth
|
||||
}
|
||||
|
||||
val ratio = remember(content) { aspectRatio(content.dim) }
|
||||
|
||||
if (showImage.value) {
|
||||
val ratio = remember(content) { aspectRatio(content.dim) }
|
||||
CrossfadeIfEnabled(targetState = showImage.value, contentAlignment = Alignment.Center, accountViewModel = accountViewModel) {
|
||||
if (it) {
|
||||
SubcomposeAsyncImage(
|
||||
model = content.localFile,
|
||||
contentDescription = content.description,
|
||||
@@ -298,28 +362,28 @@ fun LocalImageView(
|
||||
@Composable
|
||||
fun UrlImageView(
|
||||
content: MediaUrlImage,
|
||||
contentScale: ContentScale,
|
||||
mainImageModifier: Modifier,
|
||||
loadedImageModifier: Modifier,
|
||||
isFiniteHeight: Boolean,
|
||||
controllerVisible: MutableState<Boolean>,
|
||||
accountViewModel: AccountViewModel,
|
||||
alwayShowImage: Boolean = false,
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
val showImage =
|
||||
remember {
|
||||
mutableStateOf(
|
||||
if (alwayShowImage) true else accountViewModel.settings.showImages.value,
|
||||
)
|
||||
}
|
||||
val ratio = remember(content) { aspectRatio(content.dim) }
|
||||
|
||||
val ratio = remember(content) { aspectRatio(content.dim) }
|
||||
val showImage =
|
||||
remember {
|
||||
mutableStateOf(
|
||||
if (alwayShowImage) true else accountViewModel.settings.showImages.value,
|
||||
)
|
||||
}
|
||||
|
||||
if (showImage.value) {
|
||||
CrossfadeIfEnabled(targetState = showImage.value, contentAlignment = Alignment.Center, accountViewModel = accountViewModel) {
|
||||
if (it) {
|
||||
SubcomposeAsyncImage(
|
||||
model = content.url,
|
||||
contentDescription = content.description,
|
||||
contentScale = if (isFiniteHeight) ContentScale.Fit else ContentScale.FillWidth,
|
||||
contentScale = contentScale,
|
||||
modifier = mainImageModifier,
|
||||
) {
|
||||
when (painter.state) {
|
||||
@@ -422,7 +486,7 @@ fun ImageUrlWithDownloadButton(
|
||||
|
||||
val inlineContent = mapOf("inlineContent" to InlineDownloadIcon(showImage))
|
||||
|
||||
val pressIndicator = remember { Modifier.clickable { runCatching { uri.openUri(url) } } }
|
||||
val pressIndicator = remember { Modifier.fillMaxWidth().clickable { runCatching { uri.openUri(url) } } }
|
||||
|
||||
Text(
|
||||
text = annotatedTermsString,
|
||||
@@ -605,6 +669,10 @@ fun ShareImageAction(
|
||||
popupExpanded = popupExpanded,
|
||||
videoUri = content.url,
|
||||
postNostrUri = content.uri,
|
||||
blurhash = content.blurhash,
|
||||
dim = content.dim,
|
||||
hash = content.hash,
|
||||
mimeType = content.mimeType,
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
} else if (content is MediaPreloadedContent) {
|
||||
@@ -613,6 +681,10 @@ fun ShareImageAction(
|
||||
popupExpanded = popupExpanded,
|
||||
videoUri = content.localFile?.toUri().toString(),
|
||||
postNostrUri = content.uri,
|
||||
blurhash = content.blurhash,
|
||||
dim = content.dim,
|
||||
hash = null,
|
||||
mimeType = content.mimeType,
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
}
|
||||
@@ -625,6 +697,10 @@ fun ShareImageAction(
|
||||
popupExpanded: MutableState<Boolean>,
|
||||
videoUri: String?,
|
||||
postNostrUri: String?,
|
||||
blurhash: String?,
|
||||
dim: String?,
|
||||
hash: String?,
|
||||
mimeType: String?,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
DropdownMenu(
|
||||
@@ -660,7 +736,7 @@ fun ShareImageAction(
|
||||
if (videoUri != null) {
|
||||
var n19 = Nip19Bech32.uriToRoute(postNostrUri)?.entity as? Nip19Bech32.NEvent
|
||||
if (n19 != null) {
|
||||
accountViewModel.addMediaToGallery(n19.hex, videoUri, n19.relay[0]) // TODO Whole list or first?
|
||||
accountViewModel.addMediaToGallery(n19.hex, videoUri, n19.relay[0], blurhash, dim, hash, mimeType) // TODO Whole list or first?
|
||||
accountViewModel.toast(R.string.image_saved_to_the_gallery, R.string.image_saved_to_the_gallery)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-11
@@ -47,16 +47,7 @@ class UserProfileGalleryFeedFilter(
|
||||
}
|
||||
|
||||
var sorted = sort(notes)
|
||||
var finalnotes = setOf<Note>()
|
||||
for (item in sorted) {
|
||||
val note = (item.event as ProfileGalleryEntryEvent).event()?.let { LocalCache.checkGetOrCreateNote(it) }
|
||||
if (note != null) {
|
||||
note.associatedNote = item
|
||||
finalnotes = finalnotes + note
|
||||
}
|
||||
}
|
||||
|
||||
return finalnotes.toList()
|
||||
return sorted.toList()
|
||||
}
|
||||
|
||||
override fun applyFilter(collection: Set<Note>): Set<Note> = innerApplyFilter(collection)
|
||||
@@ -74,7 +65,7 @@ class UserProfileGalleryFeedFilter(
|
||||
): Boolean {
|
||||
val noteEvent = it.event
|
||||
return (
|
||||
(it.event?.pubKey() == user.pubkeyHex && noteEvent is ProfileGalleryEntryEvent) && noteEvent.hasUrl() && noteEvent.hasEvent() // && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET))
|
||||
(it.event?.pubKey() == user.pubkeyHex && noteEvent is ProfileGalleryEntryEvent) && noteEvent.hasUrl() && noteEvent.hasFromEvent() // && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET))
|
||||
) &&
|
||||
params.match(noteEvent) &&
|
||||
account.isAcceptable(it)
|
||||
|
||||
@@ -27,6 +27,11 @@ import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.material3.BottomAppBarDefaults.windowInsets
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -43,11 +48,11 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size0dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10Modifier
|
||||
@@ -126,11 +131,18 @@ private fun RenderBottomMenu(
|
||||
navEntryState: State<NavBackStackEntry?>,
|
||||
nav: (Route, Boolean) -> Unit,
|
||||
) {
|
||||
Column(modifier = BottomTopHeight) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.windowInsetsPadding(windowInsets)
|
||||
.consumeWindowInsets(windowInsets)
|
||||
.height(50.dp),
|
||||
) {
|
||||
HorizontalDivider(
|
||||
thickness = DividerThickness,
|
||||
)
|
||||
NavigationBar(tonalElevation = Size0dp, containerColor = Color.Transparent) {
|
||||
NavigationBar(tonalElevation = Size0dp) {
|
||||
bottomNavigationItems.forEach { item ->
|
||||
HasNewItemsIcon(item, accountViewModel, navEntryState, nav)
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ fun AppNavigation(
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val nav =
|
||||
remember {
|
||||
remember(navController) {
|
||||
{ route: String ->
|
||||
scope.launch {
|
||||
if (getRouteWithArguments(navController) != route) {
|
||||
@@ -219,7 +219,6 @@ fun AppNavigation(
|
||||
|
||||
composable(Route.BlockedUsers.route, content = { HiddenUsersScreen(accountViewModel, nav) })
|
||||
composable(Route.Bookmarks.route, content = { BookmarkListScreen(accountViewModel, nav) })
|
||||
|
||||
composable(Route.Drafts.route, content = { DraftListScreen(accountViewModel, nav) })
|
||||
|
||||
Route.ContentDiscovery.let { route ->
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.navigation
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.os.Debug
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -66,6 +68,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -130,7 +133,6 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size22Modifier
|
||||
@@ -987,7 +989,6 @@ fun FlexibleTopBarWithBackButton(
|
||||
navigationIcon = { IconButton(onClick = popBack) { ArrowBackIcon() } },
|
||||
actions = {},
|
||||
)
|
||||
Spacer(modifier = HalfVertSpacer)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
}
|
||||
@@ -1036,6 +1037,14 @@ fun debugState(context: Context) {
|
||||
|
||||
Log.d("STATE DUMP", "Total Native Heap Allocated: " + nativeHeap + " MB")
|
||||
|
||||
val activityManager: ActivityManager? = context.getSystemService()
|
||||
if (activityManager != null) {
|
||||
val isLargeHeap = (context.applicationInfo.flags and ApplicationInfo.FLAG_LARGE_HEAP) != 0
|
||||
val memClass = if (isLargeHeap) activityManager.largeMemoryClass else activityManager.memoryClass
|
||||
|
||||
Log.d("STATE DUMP", "Memory Class " + memClass + " MB (largeHeap $isLargeHeap)")
|
||||
}
|
||||
|
||||
Log.d("STATE DUMP", "Connected Relays: " + RelayPool.connectedRelays())
|
||||
|
||||
val imageLoader = Coil.imageLoader(context)
|
||||
@@ -1094,6 +1103,13 @@ fun debugState(context: Context) {
|
||||
LocalCache.observablesByKindAndAuthor.size,
|
||||
)
|
||||
|
||||
Log.d(
|
||||
"STATE DUMP",
|
||||
"Spam: " +
|
||||
LocalCache.antiSpam.recentMessages.size() +
|
||||
" / " + LocalCache.antiSpam.spamMessages.size(),
|
||||
)
|
||||
|
||||
Log.d(
|
||||
"STATE DUMP",
|
||||
"Memory used by Events: " +
|
||||
@@ -1111,11 +1127,11 @@ fun debugState(context: Context) {
|
||||
LocalCache.addressables
|
||||
.sumByGroup(groupMap = { _, it -> it.event?.kind() }, sumOf = { _, it -> it.event?.countMemory() ?: 0L })
|
||||
|
||||
qttNotes.forEach { kind, qtt ->
|
||||
Log.d("STATE DUMP", "Kind $kind:\t$qtt elements\t${bytesNotes.get(kind)?.div((1024 * 1024))}MB ")
|
||||
qttNotes.toList().sortedByDescending { bytesNotes.get(it.first) }.forEach { (kind, qtt) ->
|
||||
Log.d("STATE DUMP", "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesNotes.get(kind)?.div((1024 * 1024))}MB ")
|
||||
}
|
||||
qttAddressables.forEach { kind, qtt ->
|
||||
Log.d("STATE DUMP", "Kind $kind:\t$qtt elements\t${bytesAddressables.get(kind)?.div((1024 * 1024))}MB ")
|
||||
qttAddressables.toList().sortedByDescending { bytesNotes.get(it.first) }.forEach { (kind, qtt) ->
|
||||
Log.d("STATE DUMP", "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesAddressables.get(kind)?.div((1024 * 1024))}MB ")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -136,7 +136,9 @@ fun DrawerContent(
|
||||
drawerTonalElevation = 0.dp,
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxHeight().verticalScroll(rememberScrollState()),
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
ProfileContent(
|
||||
baseAccountUser = accountViewModel.account.userProfile(),
|
||||
@@ -146,7 +148,7 @@ fun DrawerContent(
|
||||
)
|
||||
|
||||
Column(drawerSpacing) {
|
||||
EditStatusBoxes(accountViewModel.account.userProfile(), accountViewModel)
|
||||
EditStatusBoxes(accountViewModel.account.userProfile(), accountViewModel, drawerState)
|
||||
}
|
||||
|
||||
FollowingAndFollowerCounts(accountViewModel.account.userProfile(), onClickUser)
|
||||
@@ -263,15 +265,16 @@ fun ProfileContentTemplate(
|
||||
private fun EditStatusBoxes(
|
||||
baseAccountUser: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
drawerState: DrawerState,
|
||||
) {
|
||||
LoadStatuses(user = baseAccountUser, accountViewModel) { statuses ->
|
||||
if (statuses.isEmpty()) {
|
||||
StatusEditBar(accountViewModel = accountViewModel)
|
||||
StatusEditBar(accountViewModel = accountViewModel, drawerState = drawerState)
|
||||
} else {
|
||||
statuses.forEach {
|
||||
val originalStatus by it.live().content.observeAsState()
|
||||
|
||||
StatusEditBar(originalStatus, it.address, accountViewModel)
|
||||
StatusEditBar(originalStatus, it.address, accountViewModel, drawerState = drawerState)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -282,11 +285,17 @@ fun StatusEditBar(
|
||||
savedStatus: String? = null,
|
||||
tag: ATag? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
drawerState: DrawerState,
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
val currentStatus = remember { mutableStateOf(savedStatus ?: "") }
|
||||
val hasChanged = remember { derivedStateOf { currentStatus.value != (savedStatus ?: "") } }
|
||||
LaunchedEffect(drawerState.isClosed) {
|
||||
if (drawerState.isClosed) {
|
||||
focusManager.clearFocus(true)
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = currentStatus.value,
|
||||
|
||||
+46
-7
@@ -71,6 +71,7 @@ import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.nip05
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.events.AddressableEvent
|
||||
import com.vitorpamplona.quartz.events.UserMetadata
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
@@ -168,7 +169,7 @@ private fun VerifyAndDisplayNIP05OrStatusLine(
|
||||
if (nip05Verified.value != true) {
|
||||
DisplayNIP05(nip05, nip05Verified, accountViewModel)
|
||||
} else if (!statuses.isEmpty()) {
|
||||
RotateStatuses(statuses, accountViewModel, nav)
|
||||
ObserveRotateStatuses(statuses, accountViewModel, nav)
|
||||
} else {
|
||||
DisplayNIP05(nip05, nip05Verified, accountViewModel)
|
||||
}
|
||||
@@ -183,6 +184,29 @@ private fun VerifyAndDisplayNIP05OrStatusLine(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ObserveRotateStatuses(
|
||||
statuses: ImmutableList<AddressableNote>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
ObserveAllStatusesToAvoidSwitchigAllTheTime(statuses)
|
||||
|
||||
RotateStatuses(
|
||||
statuses,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ObserveAllStatusesToAvoidSwitchigAllTheTime(statuses: ImmutableList<AddressableNote>) {
|
||||
statuses
|
||||
.map {
|
||||
it.live().metadata.observeAsState()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RotateStatuses(
|
||||
statuses: ImmutableList<AddressableNote>,
|
||||
@@ -221,14 +245,29 @@ fun DisplayStatus(
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val noteState by addressableNote.live().metadata.observeAsState()
|
||||
val noteEvent = noteState?.note?.event ?: return
|
||||
|
||||
val content = remember(noteState) { addressableNote.event?.content() ?: "" }
|
||||
val type = remember(noteState) { (addressableNote.event as? AddressableEvent)?.dTag() ?: "" }
|
||||
val url = remember(noteState) { addressableNote.event?.firstTaggedUrl()?.ifBlank { null } }
|
||||
val nostrATag = remember(noteState) { addressableNote.event?.firstTaggedAddress() }
|
||||
val nostrHexID =
|
||||
remember(noteState) { addressableNote.event?.firstTaggedEvent()?.ifBlank { null } }
|
||||
DisplayStatusInner(
|
||||
noteEvent.content(),
|
||||
(noteEvent as? AddressableEvent)?.dTag() ?: "",
|
||||
noteEvent.firstTaggedUrl()?.ifBlank { null },
|
||||
noteEvent.firstTaggedAddress(),
|
||||
noteEvent.firstTaggedEvent()?.ifBlank { null },
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayStatusInner(
|
||||
content: String,
|
||||
type: String,
|
||||
url: String?,
|
||||
nostrATag: ATag?,
|
||||
nostrHexID: String?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
when (type) {
|
||||
"music" ->
|
||||
Icon(
|
||||
|
||||
@@ -991,11 +991,7 @@ fun FirstUserInfoRow(
|
||||
BoostedMark()
|
||||
}
|
||||
|
||||
if (editState.value is GenericLoadable.Loaded) {
|
||||
(editState.value as? GenericLoadable.Loaded<EditState>)?.loaded?.let {
|
||||
DisplayEditStatus(it)
|
||||
}
|
||||
}
|
||||
CheckAndDisplayEditStatus(editState)
|
||||
|
||||
if (baseNote.isDraft()) {
|
||||
DisplayDraft()
|
||||
@@ -1007,6 +1003,15 @@ fun FirstUserInfoRow(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CheckAndDisplayEditStatus(editState: State<GenericLoadable<EditState>>) {
|
||||
if (editState.value is GenericLoadable.Loaded) {
|
||||
(editState.value as? GenericLoadable.Loaded<EditState>)?.loaded?.let {
|
||||
DisplayEditStatus(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun observeEdits(
|
||||
baseNote: Note,
|
||||
|
||||
+4
-2
@@ -42,9 +42,10 @@ import kotlinx.coroutines.launch
|
||||
@Composable
|
||||
fun DisplayUncitedHashtags(
|
||||
event: Event,
|
||||
callbackUri: String? = null,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
DisplayUncitedHashtags(event, event.content, nav)
|
||||
DisplayUncitedHashtags(event, event.content, callbackUri, nav)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@@ -52,6 +53,7 @@ fun DisplayUncitedHashtags(
|
||||
fun DisplayUncitedHashtags(
|
||||
event: Event,
|
||||
content: String,
|
||||
callbackUri: String? = null,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val unusedHashtags by
|
||||
@@ -59,7 +61,7 @@ fun DisplayUncitedHashtags(
|
||||
val tagsInEvent = event.hashtags()
|
||||
if (tagsInEvent.isNotEmpty()) {
|
||||
launch(Dispatchers.Default) {
|
||||
val state = CachedRichTextParser.parseText(content, event.tags.toImmutableListOfLists())
|
||||
val state = CachedRichTextParser.parseText(content, event.tags.toImmutableListOfLists(), callbackUri)
|
||||
|
||||
val tagsInContent =
|
||||
state
|
||||
|
||||
@@ -188,10 +188,9 @@ fun NoteDropDownMenu(
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.copy_text)) },
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.decrypt(note) { clipboardManager.setText(AnnotatedString(it)) }
|
||||
onDismiss()
|
||||
}
|
||||
val lastNoteVersion = (editState?.value as? GenericLoadable.Loaded)?.loaded?.modificationToShow?.value ?: note
|
||||
accountViewModel.decrypt(lastNoteVersion) { clipboardManager.setText(AnnotatedString(it)) }
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
|
||||
@@ -216,6 +216,8 @@ fun AudioHeader(
|
||||
}
|
||||
}
|
||||
|
||||
val callbackUri = remember(note) { note.toNostrUri() }
|
||||
|
||||
content?.let {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -231,7 +233,7 @@ fun AudioHeader(
|
||||
tags = tags,
|
||||
backgroundColor = background,
|
||||
id = note.idHex,
|
||||
callbackUri = note.toNostrUri(),
|
||||
callbackUri = callbackUri,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
@@ -240,7 +242,7 @@ fun AudioHeader(
|
||||
|
||||
if (noteEvent.hasHashtags()) {
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
DisplayUncitedHashtags(noteEvent, nav)
|
||||
DisplayUncitedHashtags(noteEvent, callbackUri, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,23 +170,23 @@ private fun RenderGitPatchEvent(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
} else {
|
||||
val callbackUri = remember(note) { note.toNostrUri() }
|
||||
|
||||
SensitivityWarning(
|
||||
note = note,
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
val modifier = remember(note) { Modifier.fillMaxWidth() }
|
||||
val tags =
|
||||
remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList }
|
||||
val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList }
|
||||
|
||||
TranslatableRichTextViewer(
|
||||
content = eventContent,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
quotesLeft = quotesLeft,
|
||||
modifier = modifier,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
id = note.idHex,
|
||||
callbackUri = note.toNostrUri(),
|
||||
callbackUri = callbackUri,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
@@ -196,6 +196,7 @@ private fun RenderGitPatchEvent(
|
||||
DisplayUncitedHashtags(
|
||||
event = noteEvent,
|
||||
content = eventContent,
|
||||
callbackUri = callbackUri,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
@@ -274,11 +275,12 @@ private fun RenderGitIssueEvent(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
} else {
|
||||
val callbackUri = remember(note) { note.toNostrUri() }
|
||||
|
||||
SensitivityWarning(
|
||||
note = note,
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
val modifier = remember(note) { Modifier.fillMaxWidth() }
|
||||
val tags =
|
||||
remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList }
|
||||
|
||||
@@ -286,18 +288,18 @@ private fun RenderGitIssueEvent(
|
||||
content = eventContent,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
quotesLeft = quotesLeft,
|
||||
modifier = modifier,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
id = note.idHex,
|
||||
callbackUri = note.toNostrUri(),
|
||||
callbackUri = callbackUri,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
|
||||
if (note.event?.hasHashtags() == true) {
|
||||
DisplayUncitedHashtags(noteEvent, eventContent, nav)
|
||||
DisplayUncitedHashtags(noteEvent, eventContent, callbackUri, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.types
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
@@ -127,8 +128,9 @@ fun DisplayHighlight(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
DisplayQuoteAuthor(
|
||||
highlightQuote = highlight,
|
||||
authorHex = authorHex,
|
||||
url = url,
|
||||
baseUrl = url,
|
||||
postAddress = postAddress,
|
||||
postVersion = postVersion,
|
||||
accountViewModel = accountViewModel,
|
||||
@@ -139,8 +141,9 @@ fun DisplayHighlight(
|
||||
|
||||
@Composable
|
||||
private fun DisplayQuoteAuthor(
|
||||
highlightQuote: String,
|
||||
authorHex: String?,
|
||||
url: String?,
|
||||
baseUrl: String?,
|
||||
postAddress: ATag?,
|
||||
postVersion: HexKey?,
|
||||
accountViewModel: AccountViewModel,
|
||||
@@ -196,7 +199,9 @@ private fun DisplayQuoteAuthor(
|
||||
version?.let {
|
||||
DisplayEntryForNote(it, userBase, accountViewModel, nav)
|
||||
}
|
||||
} else if (url != null) {
|
||||
} else if (baseUrl != null) {
|
||||
val url = "$baseUrl${if (baseUrl.contains("#")) "&" else "#"}:~:text=${Uri.encode(highlightQuote)}"
|
||||
|
||||
DisplayEntryForAUrl(url, userBase, accountViewModel, nav)
|
||||
} else if (userBase != null) {
|
||||
userBase?.let {
|
||||
|
||||
@@ -21,11 +21,13 @@
|
||||
package com.vitorpamplona.amethyst.ui.note.types
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
@@ -40,13 +42,19 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.min
|
||||
import coil.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.components.VideoView
|
||||
import com.vitorpamplona.amethyst.ui.components.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -56,13 +64,44 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.events.Participant
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun RenderLiveActivityEventPreview() {
|
||||
val event = Event.fromJson("{\"id\":\"19406ad34ce3c653d62eb73c1816ac27dcf473c2ccdccf5af7d90d2633c62561\",\"pubkey\":\"6b66886b3add72c779d205be574ec2d7cec619061ac3e75717389b26445989e4\",\"created_at\":1719084750,\"kind\":30311,\"tags\":[[\"r\",\"podcast:guid:72d5e069-f907-5ee7-b0d7-45404f4f0aa5\"],[\"r\",\"podcast:item:guid:bfc33d6e-e00f-4f11-a2ff-94865b7867aa\"],[\"d\",\"bfc33d6e-e00f-4f11-a2ff-94865b7867aa\"],[\"title\",\"The Online Identity Time Bomb\"],[\"summary\",\"Online identity is a ticking time bomb. But are trustworthy, open-source solutions ready to disarm it, or will we be stuck with lackluster, proprietary systems?\\n Live chat: https:/jblive.tv\"],[\"streaming\",\"https://jblive.fm\"],[\"starts\",\"1719167400\"],[\"status\",\"planned\"],[\"image\",\"https://station.us-iad-1.linodeobjects.com/art/lup-mp3.jpg\"]],\"content\":\"\",\"sig\":\"2ce3fae9ad4512541aaae4dbd9484f50df62ab95ba935d7512b736f087f151c1d15ec2bf62d3135474f16c3e070f335b24349c5493461873ddca3051804ca944\"}") as LiveActivitiesEvent
|
||||
val accountViewModel = mockAccountViewModel()
|
||||
val nav: (String) -> Unit = {}
|
||||
val baseNote: Note?
|
||||
|
||||
runBlocking {
|
||||
withContext(Dispatchers.IO) {
|
||||
LocalCache.justConsume(event, null)
|
||||
baseNote = LocalCache.getOrCreateNote("19406ad34ce3c653d62eb73c1816ac27dcf473c2ccdccf5af7d90d2633c62561")
|
||||
}
|
||||
}
|
||||
|
||||
if (baseNote != null) {
|
||||
ThemeComparisonColumn(
|
||||
toPreview = {
|
||||
RenderLiveActivityEvent(baseNote = baseNote, accountViewModel, nav)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderLiveActivityEvent(
|
||||
baseNote: Note,
|
||||
@@ -129,20 +168,6 @@ fun RenderLiveActivityEventInner(
|
||||
}
|
||||
}
|
||||
|
||||
var participantUsers by remember {
|
||||
mutableStateOf<ImmutableList<Pair<Participant, User>>>(
|
||||
persistentListOf(),
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = eventUpdates) {
|
||||
accountViewModel.loadParticipants(participants) { newParticipantUsers ->
|
||||
if (!equalImmutableLists(newParticipantUsers, participantUsers)) {
|
||||
participantUsers = newParticipantUsers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
media?.let { media ->
|
||||
if (status == LiveActivitiesEvent.STATUS_LIVE) {
|
||||
CheckIfUrlIsOnline(media, accountViewModel) { isOnline ->
|
||||
@@ -178,19 +203,43 @@ fun RenderLiveActivityEventInner(
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (status == LiveActivitiesEvent.STATUS_ENDED) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(10.dp)
|
||||
.height(100.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(id = R.string.live_stream_has_ended),
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
} else {
|
||||
if (status == LiveActivitiesEvent.STATUS_ENDED || (status == LiveActivitiesEvent.STATUS_PLANNED && (starts ?: 0) < TimeUtils.eightHoursAgo())) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(vertical = 10.dp)
|
||||
.heightIn(min = 100.dp),
|
||||
) {
|
||||
cover?.let {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
AsyncImage(model = it, contentDescription = null, modifier = MaterialTheme.colorScheme.imageModifier)
|
||||
}
|
||||
} ?: run { DisplayAuthorBanner(baseNote) }
|
||||
|
||||
Text(
|
||||
text = stringRes(id = R.string.live_stream_has_ended),
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var participantUsers by remember {
|
||||
mutableStateOf<ImmutableList<Pair<Participant, User>>>(
|
||||
persistentListOf(),
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = eventUpdates) {
|
||||
accountViewModel.loadParticipants(participants) { newParticipantUsers ->
|
||||
if (!equalImmutableLists(newParticipantUsers, participantUsers)) {
|
||||
participantUsers = newParticipantUsers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -47,6 +47,7 @@ fun RenderNIP90ContentDiscoveryResponse(
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val noteEvent = note.event as? NIP90ContentDiscoveryResponseEvent ?: return
|
||||
val callbackUri = remember(note) { note.toNostrUri() }
|
||||
|
||||
SensitivityWarning(
|
||||
note = note,
|
||||
@@ -64,13 +65,13 @@ fun RenderNIP90ContentDiscoveryResponse(
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
id = note.idHex,
|
||||
callbackUri = note.toNostrUri(),
|
||||
callbackUri = callbackUri,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
|
||||
if (noteEvent.hasHashtags()) {
|
||||
DisplayUncitedHashtags(noteEvent, noteEvent.content, nav)
|
||||
DisplayUncitedHashtags(noteEvent, noteEvent.content, callbackUri, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ fun RenderPoll(
|
||||
)
|
||||
} else {
|
||||
val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList }
|
||||
val callbackUri = remember(note) { note.toNostrUri() }
|
||||
|
||||
SensitivityWarning(
|
||||
note = note,
|
||||
@@ -110,7 +111,7 @@ fun RenderPoll(
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
id = note.idHex,
|
||||
callbackUri = note.toNostrUri(),
|
||||
callbackUri = callbackUri,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
@@ -125,7 +126,7 @@ fun RenderPoll(
|
||||
}
|
||||
|
||||
if (noteEvent.hasHashtags()) {
|
||||
DisplayUncitedHashtags(noteEvent, eventContent, nav)
|
||||
DisplayUncitedHashtags(noteEvent, eventContent, callbackUri, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +101,8 @@ fun RenderPrivateMessage(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
} else {
|
||||
val callbackUri = remember(note) { note.toNostrUri() }
|
||||
|
||||
SensitivityWarning(
|
||||
note = note,
|
||||
accountViewModel = accountViewModel,
|
||||
@@ -113,14 +115,14 @@ fun RenderPrivateMessage(
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
id = note.idHex,
|
||||
callbackUri = note.toNostrUri(),
|
||||
callbackUri = callbackUri,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
|
||||
if (noteEvent.hasHashtags()) {
|
||||
DisplayUncitedHashtags(noteEvent, eventContent, nav)
|
||||
DisplayUncitedHashtags(noteEvent, eventContent, callbackUri, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +128,8 @@ fun RenderTextEvent(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
} else {
|
||||
val callbackUri = remember(note) { note.toNostrUri() }
|
||||
|
||||
SensitivityWarning(
|
||||
note = note,
|
||||
accountViewModel = accountViewModel,
|
||||
@@ -150,14 +152,14 @@ fun RenderTextEvent(
|
||||
} else {
|
||||
note.idHex
|
||||
},
|
||||
callbackUri = note.toNostrUri(),
|
||||
callbackUri = callbackUri,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
|
||||
if (noteEvent.hasHashtags()) {
|
||||
DisplayUncitedHashtags(noteEvent, eventContent, nav)
|
||||
DisplayUncitedHashtags(noteEvent, eventContent, callbackUri, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +165,8 @@ fun VideoDisplay(
|
||||
}
|
||||
|
||||
summary?.let {
|
||||
val callbackUri = remember(note) { note.toNostrUri() }
|
||||
|
||||
TranslatableRichTextViewer(
|
||||
content = it,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
@@ -173,7 +175,7 @@ fun VideoDisplay(
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
id = note.idHex,
|
||||
callbackUri = note.toNostrUri(),
|
||||
callbackUri = callbackUri,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
@@ -182,7 +184,7 @@ fun VideoDisplay(
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
DisplayUncitedHashtags(event, summary, nav)
|
||||
DisplayUncitedHashtags(event, summary, callbackUri, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ fun LoggedInPage(
|
||||
)
|
||||
}
|
||||
},
|
||||
contentResolver = { Amethyst.instance.contentResolver },
|
||||
contentResolver = Amethyst.instance::contentResolverFn,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -196,7 +196,7 @@ fun LoggedInPage(
|
||||
)
|
||||
}
|
||||
},
|
||||
contentResolver = { Amethyst.instance.contentResolver },
|
||||
contentResolver = Amethyst.instance::contentResolverFn,
|
||||
)
|
||||
onDispose {
|
||||
accountViewModel.account.signer.launcher
|
||||
|
||||
@@ -81,6 +81,7 @@ import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.components.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
|
||||
import com.vitorpamplona.amethyst.ui.note.CheckAndDisplayEditStatus
|
||||
import com.vitorpamplona.amethyst.ui.note.CheckHiddenFeedWatchBlockAndReport
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayDraft
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayOtsIfInOriginal
|
||||
@@ -95,7 +96,6 @@ import com.vitorpamplona.amethyst.ui.note.RenderRepost
|
||||
import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.calculateBackgroundColor
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DisplayEditStatus
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DisplayFollowingCommunityInPost
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DisplayFollowingHashtagsInPost
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DisplayLocation
|
||||
@@ -421,11 +421,7 @@ private fun FullBleedNoteCompose(
|
||||
DisplayFollowingHashtagsInPost(baseNote, accountViewModel, nav)
|
||||
}
|
||||
|
||||
if (editState.value is GenericLoadable.Loaded) {
|
||||
(editState.value as? GenericLoadable.Loaded<EditState>)?.loaded?.let {
|
||||
DisplayEditStatus(it)
|
||||
}
|
||||
}
|
||||
CheckAndDisplayEditStatus(editState)
|
||||
|
||||
TimeAgo(note = baseNote)
|
||||
|
||||
|
||||
+11
-3
@@ -317,7 +317,7 @@ class AccountViewModel(
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.Eagerly,
|
||||
SharingStarted.WhileSubscribed(10000, 10000),
|
||||
NoteComposeReportState(),
|
||||
).also {
|
||||
noteIsHiddenFlows.put(note, it)
|
||||
@@ -334,7 +334,7 @@ class AccountViewModel(
|
||||
.map { it.note.relays.size > 3 }
|
||||
.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.Eagerly,
|
||||
SharingStarted.WhileSubscribed(10000, 10000),
|
||||
note.relays.size > 3,
|
||||
).also {
|
||||
noteMustShowExpandButtonFlows.put(note, it)
|
||||
@@ -673,8 +673,12 @@ class AccountViewModel(
|
||||
hex: String,
|
||||
url: String,
|
||||
relay: String?,
|
||||
blurhash: String?,
|
||||
dim: String?,
|
||||
hash: String?,
|
||||
mimeType: String?,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) { account.addToGallery(hex, url, relay) }
|
||||
viewModelScope.launch(Dispatchers.IO) { account.addToGallery(hex, url, relay, blurhash, dim, hash, mimeType) }
|
||||
}
|
||||
|
||||
fun removefromMediaGallery(note: Note) {
|
||||
@@ -725,6 +729,10 @@ class AccountViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun delete(notes: List<Note>) {
|
||||
viewModelScope.launch(Dispatchers.IO) { account.delete(notes) }
|
||||
}
|
||||
|
||||
fun delete(note: Note) {
|
||||
viewModelScope.launch(Dispatchers.IO) { account.delete(note) }
|
||||
}
|
||||
|
||||
+69
-2
@@ -23,21 +23,32 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ElevatedButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.components.SwipeToDeleteContainer
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedState
|
||||
@@ -114,17 +125,73 @@ private fun DraftFeedLoaded(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||
|
||||
if (showDeleteDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
showDeleteDialog = false
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(R.string.drafts))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(R.string.delete_all_drafts_confirmation))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
accountViewModel.delete(state.feed.value)
|
||||
showDeleteDialog = false
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(R.string.yes))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
showDeleteDialog = false
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(R.string.no))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = FeedPadding,
|
||||
state = listState,
|
||||
) {
|
||||
stickyHeader {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
Arrangement.Center,
|
||||
) {
|
||||
ElevatedButton(
|
||||
onClick = { showDeleteDialog = true },
|
||||
) {
|
||||
Text(stringResource(R.string.delete_all))
|
||||
}
|
||||
}
|
||||
}
|
||||
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
|
||||
SwipeToDeleteContainer(
|
||||
modifier = Modifier.fillMaxWidth().animateContentSize(),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.animateContentSize(),
|
||||
onStartToEnd = { accountViewModel.delete(item) },
|
||||
onEndToStart = { accountViewModel.delete(item) },
|
||||
) {
|
||||
Row(Modifier.fillMaxWidth().animateItemPlacement().background(MaterialTheme.colorScheme.background)) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.animateItemPlacement()
|
||||
.background(MaterialTheme.colorScheme.background),
|
||||
) {
|
||||
NoteCompose(
|
||||
item,
|
||||
routeForLastRead = routeForLastRead,
|
||||
|
||||
@@ -34,9 +34,12 @@ import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
@@ -487,10 +490,11 @@ private fun MainScaffold(
|
||||
) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier.padding(
|
||||
top = it.calculateTopPadding(),
|
||||
bottom = it.calculateBottomPadding(),
|
||||
),
|
||||
Modifier
|
||||
.padding(it)
|
||||
.consumeWindowInsets(it)
|
||||
.systemBarsPadding()
|
||||
.imePadding(),
|
||||
) {
|
||||
AppNavigation(
|
||||
homeFeedViewModel = homeFeedViewModel,
|
||||
|
||||
+109
-74
@@ -45,18 +45,21 @@ import androidx.compose.ui.Alignment.Companion.BottomStart
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import coil.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser.Companion.isVideoUrl
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.components.GalleryContentView
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
import com.vitorpamplona.amethyst.ui.components.VideoView
|
||||
import com.vitorpamplona.amethyst.ui.components.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.note.CheckHiddenFeedWatchBlockAndReport
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableNote
|
||||
import com.vitorpamplona.amethyst.ui.note.LongPressToQuickActionGallery
|
||||
@@ -79,7 +82,6 @@ import com.vitorpamplona.quartz.events.ProfileGalleryEntryEvent
|
||||
fun RenderGalleryFeed(
|
||||
viewModel: FeedViewModel,
|
||||
routeForLastRead: String?,
|
||||
forceEventKind: Int?,
|
||||
listState: LazyGridState,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
@@ -103,7 +105,6 @@ fun RenderGalleryFeed(
|
||||
state,
|
||||
routeForLastRead,
|
||||
listState,
|
||||
forceEventKind,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
@@ -121,7 +122,6 @@ private fun GalleryFeedLoaded(
|
||||
state: FeedState.Loaded,
|
||||
routeForLastRead: String?,
|
||||
listState: LazyGridState,
|
||||
forceEventKind: Int?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
@@ -131,14 +131,18 @@ private fun GalleryFeedLoaded(
|
||||
state = listState,
|
||||
) {
|
||||
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
|
||||
val defaultModifier = remember { Modifier.fillMaxWidth().animateItemPlacement() }
|
||||
val defaultModifier =
|
||||
remember {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.animateItemPlacement()
|
||||
}
|
||||
|
||||
Row(defaultModifier) {
|
||||
GalleryCardCompose(
|
||||
baseNote = item,
|
||||
routeForLastRead = routeForLastRead,
|
||||
modifier = Modifier,
|
||||
forceEventKind = forceEventKind,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
@@ -157,7 +161,6 @@ fun GalleryCardCompose(
|
||||
routeForLastRead: String? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
forceEventKind: Int?,
|
||||
isHiddenFeed: Boolean = false,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
@@ -172,20 +175,41 @@ fun GalleryCardCompose(
|
||||
nav = nav,
|
||||
) { canPreview ->
|
||||
|
||||
if (baseNote.associatedNote != null) {
|
||||
if (baseNote.associatedNote!!.event != null) {
|
||||
val image = (baseNote.associatedNote!!.event as ProfileGalleryEntryEvent).url()
|
||||
if (image != null) {
|
||||
GalleryCard(
|
||||
galleryNote = baseNote.associatedNote!!,
|
||||
baseNote = baseNote,
|
||||
image = image,
|
||||
modifier = modifier,
|
||||
parentBackgroundColor = parentBackgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
val galleryEvent = (baseNote.event as? ProfileGalleryEntryEvent) ?: return@CheckHiddenFeedWatchBlockAndReport
|
||||
|
||||
galleryEvent.url()?.let { image ->
|
||||
val sourceEvent = galleryEvent.fromEvent()
|
||||
|
||||
if (sourceEvent != null) {
|
||||
LoadNote(baseNoteHex = sourceEvent, accountViewModel = accountViewModel) { sourceNote ->
|
||||
if (sourceNote != null) {
|
||||
ClickableGalleryCard(
|
||||
galleryNote = baseNote,
|
||||
baseNote = sourceNote,
|
||||
image = image,
|
||||
modifier = modifier,
|
||||
parentBackgroundColor = parentBackgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
} else {
|
||||
GalleryCard(
|
||||
galleryNote = baseNote,
|
||||
image = image,
|
||||
modifier = modifier,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
GalleryCard(
|
||||
galleryNote = baseNote,
|
||||
image = image,
|
||||
modifier = modifier,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,7 +217,7 @@ fun GalleryCardCompose(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GalleryCard(
|
||||
fun ClickableGalleryCard(
|
||||
galleryNote: Note,
|
||||
baseNote: Note,
|
||||
image: String,
|
||||
@@ -204,44 +228,38 @@ fun GalleryCard(
|
||||
) {
|
||||
// baseNote.event?.let { Text(text = it.pubKey()) }
|
||||
LongPressToQuickActionGallery(baseNote = galleryNote, accountViewModel = accountViewModel) { showPopup ->
|
||||
CheckNewAndRenderChannelCard(
|
||||
baseNote,
|
||||
image,
|
||||
modifier,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
showPopup,
|
||||
nav,
|
||||
)
|
||||
val backgroundColor =
|
||||
calculateBackgroundColor(
|
||||
createdAt = baseNote.createdAt(),
|
||||
parentBackgroundColor = parentBackgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
|
||||
ClickableNote(
|
||||
baseNote = baseNote,
|
||||
backgroundColor = backgroundColor,
|
||||
modifier = modifier,
|
||||
accountViewModel = accountViewModel,
|
||||
showPopup = showPopup,
|
||||
nav = nav,
|
||||
) {
|
||||
InnerGalleryCardBox(galleryNote, image, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CheckNewAndRenderChannelCard(
|
||||
baseNote: Note,
|
||||
fun GalleryCard(
|
||||
galleryNote: Note,
|
||||
image: String,
|
||||
modifier: Modifier = Modifier,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
showPopup: () -> Unit,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val backgroundColor =
|
||||
calculateBackgroundColor(
|
||||
createdAt = baseNote.createdAt(),
|
||||
parentBackgroundColor = parentBackgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
|
||||
ClickableNote(
|
||||
baseNote = baseNote,
|
||||
backgroundColor = backgroundColor,
|
||||
modifier = modifier,
|
||||
accountViewModel = accountViewModel,
|
||||
showPopup = showPopup,
|
||||
nav = nav,
|
||||
) {
|
||||
InnerGalleryCardBox(baseNote, image, accountViewModel, nav)
|
||||
LongPressToQuickActionGallery(baseNote = galleryNote, accountViewModel = accountViewModel) { showPopup ->
|
||||
Column(modifier = modifier) {
|
||||
InnerGalleryCardBox(galleryNote, image, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,7 +285,6 @@ data class GalleryThumb(
|
||||
val id: String?,
|
||||
val image: String?,
|
||||
val title: String?,
|
||||
// val price: Price?,
|
||||
)
|
||||
|
||||
@Composable
|
||||
@@ -287,7 +304,6 @@ fun RenderGalleryThumb(
|
||||
image = image,
|
||||
title = "",
|
||||
// noteEvent?.title(),
|
||||
// price = noteEvent?.price(),
|
||||
)
|
||||
}.distinctUntilChanged()
|
||||
.observeAsState(
|
||||
@@ -298,12 +314,14 @@ fun RenderGalleryThumb(
|
||||
),
|
||||
)
|
||||
|
||||
InnerRenderGalleryThumb(card as GalleryThumb, baseNote, accountViewModel)
|
||||
InnerRenderGalleryThumb(card, baseNote, accountViewModel)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun RenderGalleryThumbPreview(accountViewModel: AccountViewModel) {
|
||||
fun RenderGalleryThumbPreview() {
|
||||
val accountViewModel = mockAccountViewModel()
|
||||
|
||||
Surface(Modifier.size(200.dp)) {
|
||||
InnerRenderGalleryThumb(
|
||||
card =
|
||||
@@ -311,7 +329,6 @@ fun RenderGalleryThumbPreview(accountViewModel: AccountViewModel) {
|
||||
id = "",
|
||||
image = null,
|
||||
title = "Like New",
|
||||
// price = Price("800000", "SATS", null),
|
||||
),
|
||||
note = Note("hex"),
|
||||
accountViewModel = accountViewModel,
|
||||
@@ -332,27 +349,45 @@ fun InnerRenderGalleryThumb(
|
||||
contentAlignment = BottomStart,
|
||||
) {
|
||||
card.image?.let {
|
||||
var blurHash = (note.event as ProfileGalleryEntryEvent).blurhash()
|
||||
var description = (note.event as ProfileGalleryEntryEvent).content
|
||||
// var hash = (note.event as ProfileGalleryEntryEvent).hash()
|
||||
var dimensions = (note.event as ProfileGalleryEntryEvent).dimensions()
|
||||
var mimeType = (note.event as ProfileGalleryEntryEvent).mimeType()
|
||||
var content: BaseMediaContent? = null
|
||||
|
||||
if (isVideoUrl(it)) {
|
||||
VideoView(
|
||||
videoUri = it,
|
||||
mimeType = null,
|
||||
title = "",
|
||||
authorName = note.author?.toBestDisplayName(),
|
||||
roundedCorner = false,
|
||||
gallery = true,
|
||||
isFiniteHeight = false,
|
||||
alwaysShowVideo = true,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
content =
|
||||
MediaUrlVideo(
|
||||
url = it,
|
||||
description = description,
|
||||
hash = null,
|
||||
blurhash = blurHash,
|
||||
dim = dimensions,
|
||||
uri = null,
|
||||
mimeType = mimeType,
|
||||
)
|
||||
} else {
|
||||
AsyncImage(
|
||||
model = it,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
content =
|
||||
MediaUrlImage(
|
||||
url = it,
|
||||
description = description,
|
||||
hash = null, // We don't want to show the hash banner here
|
||||
blurhash = blurHash,
|
||||
dim = dimensions,
|
||||
uri = null,
|
||||
mimeType = mimeType,
|
||||
)
|
||||
}
|
||||
|
||||
GalleryContentView(
|
||||
content = content,
|
||||
roundedCorner = false,
|
||||
isFiniteHeight = false,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
// }
|
||||
?: run { DisplayGalleryAuthorBanner(note) }
|
||||
}
|
||||
}
|
||||
|
||||
+10
-11
@@ -240,6 +240,16 @@ fun PrepareViewModels(
|
||||
),
|
||||
)
|
||||
|
||||
val galleryFeedViewModel: NostrUserProfileGalleryFeedViewModel =
|
||||
viewModel(
|
||||
key = baseUser.pubkeyHex + "UserGalleryFeedViewModel",
|
||||
factory =
|
||||
NostrUserProfileGalleryFeedViewModel.Factory(
|
||||
baseUser,
|
||||
accountViewModel.account,
|
||||
),
|
||||
)
|
||||
|
||||
val followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel =
|
||||
viewModel(
|
||||
key = baseUser.pubkeyHex + "UserProfileFollowersUserFeedViewModel",
|
||||
@@ -298,16 +308,6 @@ fun PrepareViewModels(
|
||||
),
|
||||
)
|
||||
|
||||
val galleryFeedViewModel: NostrUserProfileGalleryFeedViewModel =
|
||||
viewModel(
|
||||
key = baseUser.pubkeyHex + "UserGalleryFeedViewModel",
|
||||
factory =
|
||||
NostrUserProfileGalleryFeedViewModel.Factory(
|
||||
baseUser,
|
||||
accountViewModel.account,
|
||||
),
|
||||
)
|
||||
|
||||
val reportsFeedViewModel: NostrUserProfileReportFeedViewModel =
|
||||
viewModel(
|
||||
key = baseUser.pubkeyHex + "UserProfileReportFeedViewModel",
|
||||
@@ -1574,7 +1574,6 @@ fun TabGallery(
|
||||
RenderGalleryFeed(
|
||||
feedViewModel,
|
||||
null,
|
||||
0,
|
||||
listState,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
|
||||
+1
-1
@@ -198,7 +198,7 @@ fun LoginPage(
|
||||
}
|
||||
}
|
||||
},
|
||||
contentResolver = { Amethyst.instance.contentResolver },
|
||||
contentResolver = Amethyst.instance::contentResolverFn,
|
||||
)
|
||||
onDispose { externalSignerLauncher.clearLauncher() }
|
||||
}
|
||||
|
||||
@@ -65,8 +65,8 @@ private val DarkColorPalette =
|
||||
primary = Purple200,
|
||||
secondary = Teal200,
|
||||
tertiary = Teal200,
|
||||
background = Color(0xFF000000),
|
||||
surface = Color(0xFF000000),
|
||||
background = Color(red = 0, green = 0, blue = 0),
|
||||
surface = Color(red = 0, green = 0, blue = 0),
|
||||
surfaceVariant = Color(red = 29, green = 26, blue = 34),
|
||||
)
|
||||
|
||||
@@ -451,14 +451,13 @@ fun AmethystTheme(
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
val window = (view.context as Activity).window
|
||||
val insetsController = WindowCompat.getInsetsController(window, view)
|
||||
if (darkTheme) {
|
||||
window.statusBarColor = colors.background.toArgb()
|
||||
} else {
|
||||
window.statusBarColor = colors.primary.toArgb()
|
||||
}
|
||||
val insets = WindowCompat.getInsetsController(window, view)
|
||||
|
||||
insets.isAppearanceLightNavigationBars = !darkTheme
|
||||
insets.isAppearanceLightStatusBars = !darkTheme
|
||||
|
||||
window.statusBarColor = colors.background.toArgb()
|
||||
window.navigationBarColor = colors.background.toArgb()
|
||||
insetsController.isAppearanceLightNavigationBars = !darkTheme
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<string name="post_was_hidden">Esta publicación se ocultó porque menciona tus usuarios o palabras ocultas</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">Post marcado como inapropiado por</string>
|
||||
<string name="post_not_found">El evento se está cargando o no se puede encontrar en la lista de relés</string>
|
||||
<string name="post_not_found_short">👀</string>
|
||||
<string name="channel_image">Imagen del canal</string>
|
||||
<string name="referenced_event_not_found">Evento referenciado no encontrado</string>
|
||||
<string name="could_not_decrypt_the_message">No se pudo desencriptar el mensaje</string>
|
||||
@@ -132,6 +133,7 @@
|
||||
<string name="conversations">Conversaciones</string>
|
||||
<string name="notes">Notas</string>
|
||||
<string name="replies">Respuestas</string>
|
||||
<string name="gallery">Galería</string>
|
||||
<string name="follows">"Siguiendo"</string>
|
||||
<string name="reports">"Reportes"</string>
|
||||
<string name="more_options">Más opciones</string>
|
||||
@@ -255,6 +257,8 @@
|
||||
<string name="quick_action_delete">Eliminar</string>
|
||||
<string name="quick_action_unfollow">Dejar de seguir</string>
|
||||
<string name="quick_action_follow">Seguir</string>
|
||||
<string name="quick_action_request_deletion_gallery_title">Eliminar de la galería</string>
|
||||
<string name="quick_action_request_deletion_gallery_alert_body">Elimina este contenido multimedia de la galería, aunque puedes volver a agregarlo luego.</string>
|
||||
<string name="quick_action_request_deletion_alert_title">Solicitar eliminación</string>
|
||||
<string name="quick_action_request_deletion_alert_body">Amethyst solicitará que se elimine su nota de los relays a los que está conectado actualmente. No hay garantía de que su nota se elimine permanentemente de esos relays, o de otros relays donde pueda almacenarse.</string>
|
||||
<string name="quick_action_block_dialog_btn">Bloquear</string>
|
||||
@@ -517,6 +521,7 @@
|
||||
<string name="share_or_save">Compartir o guardar</string>
|
||||
<string name="copy_url_to_clipboard">Copiar URL al portapapeles</string>
|
||||
<string name="copy_the_note_id_to_the_clipboard">Copiar ID de la nota al portapapeles</string>
|
||||
<string name="add_media_to_gallery">Agregar contenido multimedia a la galería</string>
|
||||
<string name="created_at">Creado</string>
|
||||
<string name="rules">Reglas</string>
|
||||
<string name="login_with_external_signer">Iniciar sesión con Amber</string>
|
||||
@@ -720,6 +725,8 @@
|
||||
<string name="private_outbox_section_explainer">Inserta entre 1 y 3 relés para almacenar eventos que nadie más pueda ver, como los borradores o la configuración de la app. Lo ideal es que estos relés sean locales o requieran autenticación antes de descargar el contenido de cada usuario.</string>
|
||||
<string name="kind_3_section">Relés generales</string>
|
||||
<string name="kind_3_section_description">Amethyst usa estos relés para descargar publicaciones por ti.</string>
|
||||
<string name="kind_3_recommended_section">Relés recomendados</string>
|
||||
<string name="kind_3_recommended_section_description">Agrega los siguientes relés a la lista de relés generales para recibir publicaciones de los usuarios de la lista.</string>
|
||||
<string name="search_section">Relés de búsqueda</string>
|
||||
<string name="search_section_explainer">Lista de relés que se usarán al buscar contenido o usuarios. El etiquetado y la búsqueda no funcionarán si no hay opciones disponibles. Asegúrate de que implementen NIP-50.</string>
|
||||
<string name="local_section">Relés locales</string>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<string name="post_was_hidden">Esta publicación se ocultó porque menciona tus usuarios o palabras ocultas</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">La publicación fue reportada por</string>
|
||||
<string name="post_not_found">El evento se está cargando o no se puede encontrar en la lista de relés</string>
|
||||
<string name="post_not_found_short">👀</string>
|
||||
<string name="channel_image">Imagen del canal</string>
|
||||
<string name="referenced_event_not_found">No se encontró el evento referenciado</string>
|
||||
<string name="could_not_decrypt_the_message">No se pudo desencriptar el mensaje</string>
|
||||
@@ -132,6 +133,7 @@
|
||||
<string name="conversations">Conversaciones</string>
|
||||
<string name="notes">Notas</string>
|
||||
<string name="replies">Respuestas</string>
|
||||
<string name="gallery">Galería</string>
|
||||
<string name="follows">"Seguidos"</string>
|
||||
<string name="reports">"Reportes"</string>
|
||||
<string name="more_options">Más opciones</string>
|
||||
@@ -255,6 +257,8 @@
|
||||
<string name="quick_action_delete">Eliminar</string>
|
||||
<string name="quick_action_unfollow">Dejar de seguir</string>
|
||||
<string name="quick_action_follow">Seguir</string>
|
||||
<string name="quick_action_request_deletion_gallery_title">Eliminar de la galería</string>
|
||||
<string name="quick_action_request_deletion_gallery_alert_body">Elimina este contenido multimedia de la galería, aunque puedes volver a agregarlo luego.</string>
|
||||
<string name="quick_action_request_deletion_alert_title">Solicitar eliminación</string>
|
||||
<string name="quick_action_request_deletion_alert_body">Amethyst solicitará que se elimine la nota de los relés con los que tienes conexión actualmente. No hay garantía de que la nota se elimine permanentemente de esos relés o de otros donde pueda estar guardada.</string>
|
||||
<string name="quick_action_block_dialog_btn">Bloquear</string>
|
||||
@@ -517,6 +521,7 @@
|
||||
<string name="share_or_save">Compartir o guardar</string>
|
||||
<string name="copy_url_to_clipboard">Copiar URL al portapapeles</string>
|
||||
<string name="copy_the_note_id_to_the_clipboard">Copiar ID de la nota al portapapeles</string>
|
||||
<string name="add_media_to_gallery">Agregar contenido multimedia a la galería</string>
|
||||
<string name="created_at">Creado</string>
|
||||
<string name="rules">Reglas</string>
|
||||
<string name="login_with_external_signer">Iniciar sesión con Amber</string>
|
||||
@@ -720,6 +725,8 @@
|
||||
<string name="private_outbox_section_explainer">Inserta entre 1 y 3 relés para almacenar eventos que nadie más pueda ver, como los borradores o la configuración de la app. Lo ideal es que estos relés sean locales o requieran autenticación antes de descargar el contenido de cada usuario.</string>
|
||||
<string name="kind_3_section">Relés generales</string>
|
||||
<string name="kind_3_section_description">Amethyst usa estos relés para descargar publicaciones por ti.</string>
|
||||
<string name="kind_3_recommended_section">Relés recomendados</string>
|
||||
<string name="kind_3_recommended_section_description">Agrega los siguientes relés a la lista de relés generales para recibir publicaciones de los usuarios de la lista.</string>
|
||||
<string name="search_section">Relés de búsqueda</string>
|
||||
<string name="search_section_explainer">Lista de relés que se usarán al buscar contenido o usuarios. El etiquetado y la búsqueda no funcionarán si no hay opciones disponibles. Asegúrate de que implementen NIP-50.</string>
|
||||
<string name="local_section">Relés locales</string>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<string name="post_was_hidden">Esta publicación se ocultó porque menciona tus usuarios o palabras ocultas</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">La publicación fue reportada por</string>
|
||||
<string name="post_not_found">El evento se está cargando o no se puede encontrar en la lista de relés</string>
|
||||
<string name="post_not_found_short">👀</string>
|
||||
<string name="channel_image">Imagen del canal</string>
|
||||
<string name="referenced_event_not_found">No se encontró el evento referenciado</string>
|
||||
<string name="could_not_decrypt_the_message">No se pudo desencriptar el mensaje</string>
|
||||
@@ -132,6 +133,7 @@
|
||||
<string name="conversations">Conversaciones</string>
|
||||
<string name="notes">Notas</string>
|
||||
<string name="replies">Respuestas</string>
|
||||
<string name="gallery">Galería</string>
|
||||
<string name="follows">"Seguidos"</string>
|
||||
<string name="reports">"Reportes"</string>
|
||||
<string name="more_options">Más opciones</string>
|
||||
@@ -255,6 +257,8 @@
|
||||
<string name="quick_action_delete">Eliminar</string>
|
||||
<string name="quick_action_unfollow">Dejar de seguir</string>
|
||||
<string name="quick_action_follow">Seguir</string>
|
||||
<string name="quick_action_request_deletion_gallery_title">Eliminar de la galería</string>
|
||||
<string name="quick_action_request_deletion_gallery_alert_body">Elimina este contenido multimedia de la galería, aunque puedes volver a agregarlo luego.</string>
|
||||
<string name="quick_action_request_deletion_alert_title">Solicitar eliminación</string>
|
||||
<string name="quick_action_request_deletion_alert_body">Amethyst solicitará que se elimine la nota de los relés con los que tienes conexión actualmente. No hay garantía de que la nota se elimine permanentemente de esos relés o de otros donde pueda estar guardada.</string>
|
||||
<string name="quick_action_block_dialog_btn">Bloquear</string>
|
||||
@@ -517,6 +521,7 @@
|
||||
<string name="share_or_save">Compartir o guardar</string>
|
||||
<string name="copy_url_to_clipboard">Copiar URL al portapapeles</string>
|
||||
<string name="copy_the_note_id_to_the_clipboard">Copiar ID de la nota al portapapeles</string>
|
||||
<string name="add_media_to_gallery">Agregar contenido multimedia a la galería</string>
|
||||
<string name="created_at">Creado</string>
|
||||
<string name="rules">Reglas</string>
|
||||
<string name="login_with_external_signer">Iniciar sesión con Amber</string>
|
||||
@@ -720,6 +725,8 @@
|
||||
<string name="private_outbox_section_explainer">Inserta entre 1 y 3 relés para almacenar eventos que nadie más pueda ver, como los borradores o la configuración de la app. Lo ideal es que estos relés sean locales o requieran autenticación antes de descargar el contenido de cada usuario.</string>
|
||||
<string name="kind_3_section">Relés generales</string>
|
||||
<string name="kind_3_section_description">Amethyst usa estos relés para descargar publicaciones por ti.</string>
|
||||
<string name="kind_3_recommended_section">Relés recomendados</string>
|
||||
<string name="kind_3_recommended_section_description">Agrega los siguientes relés a la lista de relés generales para recibir publicaciones de los usuarios de la lista.</string>
|
||||
<string name="search_section">Relés de búsqueda</string>
|
||||
<string name="search_section_explainer">Lista de relés que se usarán al buscar contenido o usuarios. El etiquetado y la búsqueda no funcionarán si no hay opciones disponibles. Asegúrate de que implementen NIP-50.</string>
|
||||
<string name="local_section">Relés locales</string>
|
||||
|
||||
@@ -803,4 +803,6 @@
|
||||
<string name="http_status_508">Boucle détectée - Le serveur détecte une boucle infinie lors du traitement de la requête</string>
|
||||
<string name="http_status_511">Authentification réseau requise - Le client doit être authentifié pour accéder au réseau</string>
|
||||
<string name="add_a_nip96_server">Ajouter un serveur NIP-96</string>
|
||||
<string name="delete_all">Tout supprimer</string>
|
||||
<string name="delete_all_drafts_confirmation">Êtes-vous sûr de vouloir supprimer tous les brouillons ?</string>
|
||||
</resources>
|
||||
|
||||
@@ -725,6 +725,8 @@
|
||||
<string name="private_outbox_section_explainer">१ - ३ पुनःप्रसारकों को जोडें उन घटनाओं को रखने के लिए जो कोई अन्य नहीं देख सकता जैसे कि पाण्डुलिपियाँ अथवा क्रमक के स्थापना विकल्प। आदर्शतः ये पुनःप्रसारक स्थानीय होने चाहिए अथवा प्रत्येक उपयोगकर्ता के विषयवस्तु अवरोहण के पूर्व परिचय प्रमाणीकरण अनिवार्य होना चाहिए।</string>
|
||||
<string name="kind_3_section">सामान्य पुनःप्रसारक</string>
|
||||
<string name="kind_3_section_description">अमेथिस्ट इन पुनःप्रसारकों का प्रयोग करता है आपके पत्रों का अवरोहण करने के लिए।</string>
|
||||
<string name="kind_3_recommended_section">पुनःशंसित पुनःप्रसारक</string>
|
||||
<string name="kind_3_recommended_section_description">सूचित उपयोगकर्ताओं के पत्र प्राप्त करने के लिए निम्नलिखित पुनःप्रसारकों को अपने सामान्य पुनःप्रसारक सूची में जोडें।</string>
|
||||
<string name="search_section">खोज पुनःप्रसारक</string>
|
||||
<string name="search_section_explainer">पुनःप्रसारकों की सूची जिनको किसी विषयवस्तु अथवा उपयोगकर्ताओं को खोजने में प्रयोग करना चाहिए। कोई विकल्प नहीं तो उपयोगकर्ता सूचक जोडना तथा खोज निष्क्रिय होंगे। सुनिश्चित करें कि वे निप॰-५० को कार्यान्वित किये हैं।</string>
|
||||
<string name="local_section">स्थानीय पुनःप्रसारक</string>
|
||||
|
||||
@@ -9,11 +9,13 @@
|
||||
<string name="post_was_hidden">Ez a bejegyzés el lett rejtve, mert említ rejtett felhasználóidat vagy szavaidat</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">A bejegyzést nem megfelelőként jelölte meg</string>
|
||||
<string name="post_not_found">Bejegyzés nem található</string>
|
||||
<string name="post_not_found_short">👀</string>
|
||||
<string name="channel_image">Csatorna Kép</string>
|
||||
<string name="referenced_event_not_found">A hivatkozott esemény nem található</string>
|
||||
<string name="could_not_decrypt_the_message">Az üzenet dekódolása sikertelen</string>
|
||||
<string name="group_picture">Csoport Kép</string>
|
||||
<string name="explicit_content">Szókimondó tartalom</string>
|
||||
<string name="spam">Spam</string>
|
||||
<string name="spam_description">Ebből a csomópontból érkező spam események száma</string>
|
||||
<string name="impersonation">Megszemélyesítés</string>
|
||||
<string name="illegal_behavior">Illegális viselkedés</string>
|
||||
@@ -35,6 +37,7 @@
|
||||
<string name="report_impersonation">Megszemélyesítés bejelentés</string>
|
||||
<string name="report_explicit_content">Szókimondó tartalom bejelentés</string>
|
||||
<string name="report_illegal_behaviour">Illegális viselkedés bejelentés</string>
|
||||
<string name="report_malware">Vírus bejelentése</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_reply">Jelentkezzen be privát kulccsal, hogy válaszolni tudjon</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_boost_posts">Jelentkezzen be privát kulccsal a bejegyzések megosztásához</string>
|
||||
<string name="login_with_a_private_key_to_like_posts">Jelentkezzen be privát kulccsal a bejegyzések kedveléséhez</string>
|
||||
@@ -52,6 +55,7 @@
|
||||
<string name="edited_number">szerkesztés #%1$s</string>
|
||||
<string name="original">eredeti</string>
|
||||
<string name="quote">Idézet</string>
|
||||
<string name="fork">Tükrözés</string>
|
||||
<string name="propose_an_edit">Szerkesztési javaslat</string>
|
||||
<string name="new_amount_in_sats">Új összeg sats-ban</string>
|
||||
<string name="add">Hozzáad</string>
|
||||
@@ -129,6 +133,7 @@
|
||||
<string name="conversations">Beszélgetések</string>
|
||||
<string name="notes">Bejegyzések</string>
|
||||
<string name="replies">Válaszok</string>
|
||||
<string name="gallery">Galéria</string>
|
||||
<string name="follows">"Követek"</string>
|
||||
<string name="reports">"Jelentések"</string>
|
||||
<string name="more_options">Több beállítás</string>
|
||||
@@ -251,6 +256,8 @@
|
||||
<string name="quick_action_delete">Törlés</string>
|
||||
<string name="quick_action_unfollow">Kikövetés</string>
|
||||
<string name="quick_action_follow">Követem</string>
|
||||
<string name="quick_action_request_deletion_gallery_title">Törlés a Galériából</string>
|
||||
<string name="quick_action_request_deletion_gallery_alert_body">Távolítsa el ezt a médiát a galériából, később elolvashatod</string>
|
||||
<string name="quick_action_request_deletion_alert_title">Törlés Kérése</string>
|
||||
<string name="quick_action_request_deletion_alert_body">Az Amethyst kérni fogja, hogy töröljék a bejegyzésed azokról a csomópontokról, amelyekhez jelenleg csatlakozol. Nincs garancia arra, hogy a bejegyzésed ezekről a csomópontokról vagy más közvetítőkből (ahol tárolni lehet) véglegesen törlődik.</string>
|
||||
<string name="quick_action_block_dialog_btn">Tiltás</string>
|
||||
@@ -264,6 +271,7 @@
|
||||
<string name="report_dialog_impersonation">Rosszindulatú megszemélyesítés</string>
|
||||
<string name="report_dialog_nudity">Meztelenség vagy pornográf tartalom</string>
|
||||
<string name="report_dialog_illegal">Illegális viselkedés</string>
|
||||
<string name="report_dialog_malware">Vírus</string>
|
||||
<string name="report_dialog_blocking_a_user">Az alkalmazásban egy felhasználó letiltása minden hozzá köthető tartalmat elrejt. Jegyzeteid továbbra is nyilvánosan megtekinthetők, beleértve a letiltott személyek számára is. A letiltott felhasználók a Biztonsági szűrők felületen jelennek meg.</string>
|
||||
<string name="report_dialog_block_hide_user_btn"><![CDATA[Felhasználó Tiltása és Elrejtése]]></string>
|
||||
<string name="report_dialog_report_btn">Visszaélés jelentése</string>
|
||||
@@ -301,6 +309,7 @@
|
||||
<string name="poll_zap_value_min">Minimum Zap</string>
|
||||
<string name="poll_zap_value_max">Maximum Zap</string>
|
||||
<string name="poll_consensus_threshold">Konszenzus</string>
|
||||
<string name="poll_consensus_threshold_percent">(0–100)%</string>
|
||||
<string name="poll_closing_time">Szavazás lezárása</string>
|
||||
<string name="poll_closing_time_days">napok</string>
|
||||
<string name="poll_unable_to_vote">Nem lehet szavazni</string>
|
||||
@@ -335,6 +344,14 @@
|
||||
<string name="zap_type_nonzap_explainer">Nostr-ban nyoma sincs, csak a Lightning-ben</string>
|
||||
<string name="file_server">Fájl Szerver</string>
|
||||
<string name="zap_forward_lnAddress">LnCím vagy @Felhasználó</string>
|
||||
<string name="media_servers">Média Szerverek</string>
|
||||
<string name="set_preferred_media_servers">Állítsd be a kívánt médiafeltöltő szervereket.</string>
|
||||
<string name="no_media_server_message">Nincs beállítva egyéni médiaszerver. Használhatod az Amethyst listáját, vagy alább hozzáadhatsz egyet ↓</string>
|
||||
<string name="built_in_media_servers_title">Beépített médiaszerverek</string>
|
||||
<string name="built_in_servers_description">Az Amethist alapértelmezett listája. Hozzáadhatod őket egyenként, vagy hozzáadhatod az egész listát.</string>
|
||||
<string name="use_default_servers">Az alapértelmezett lista használata</string>
|
||||
<string name="add_media_server">Médiaszerver hozzáadása</string>
|
||||
<string name="delete_media_server">Médiaszerver törlése</string>
|
||||
<string name="upload_server_relays_nip95">Saját csomópontjaid (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">A fájlokat a csomópontokra töltik fel és ott tárolják. Rögzített URL-től mentesek (harmadik féltől való függőség). Győződj meg róla, hogy legalább egy NIP-95 csomópont a csomópontlistában szerepel</string>
|
||||
<string name="connect_via_tor_short">Tor/Orbot beállítása</string>
|
||||
@@ -363,6 +380,7 @@
|
||||
<string name="app_notification_dms_channel_description">Értesítést küld, amikor privát üzeneted érkezik</string>
|
||||
<string name="app_notification_zaps_channel_name">Zap-et kaptál</string>
|
||||
<string name="app_notification_zaps_channel_description">Értesítést küld, amikor valaki neked Zap-et küldött</string>
|
||||
<string name="app_notification_zaps_channel_message">%1$s sats</string>
|
||||
<string name="app_notification_zaps_channel_message_from">%1$s -tól</string>
|
||||
<string name="app_notification_zaps_channel_message_for">%1$s -ért</string>
|
||||
<string name="reply_notify">Értesítés: </string>
|
||||
@@ -414,6 +432,7 @@
|
||||
<string name="minimum_pow">Minimális PoW</string>
|
||||
<string name="auth">Azonosítás</string>
|
||||
<string name="payment">Fizetés</string>
|
||||
<string name="cashu">Cashu Token</string>
|
||||
<string name="cashu_redeem">Beváltás</string>
|
||||
<string name="cashu_redeem_to_zap">Küldje a Zap tárcába</string>
|
||||
<string name="cashu_redeem_to_cashu">Nyissa meg a Cashu tárcában</string>
|
||||
@@ -442,6 +461,7 @@
|
||||
<string name="settings">Beállítások</string>
|
||||
<string name="connectivity_type_always">Mindig</string>
|
||||
<string name="connectivity_type_wifi_only">Csak WIFI</string>
|
||||
<string name="connectivity_type_unmetered_wifi_only">Korlátlan WiFi</string>
|
||||
<string name="connectivity_type_never">Soha</string>
|
||||
<string name="ui_feature_set_type_complete">Teljes</string>
|
||||
<string name="ui_feature_set_type_simplified">Egyszerűsített</string>
|
||||
@@ -500,6 +520,7 @@
|
||||
<string name="share_or_save">Megosztás vagy Mentés</string>
|
||||
<string name="copy_url_to_clipboard">Az URL vágólapra másolása</string>
|
||||
<string name="copy_the_note_id_to_the_clipboard">A bejegyzésazonosító vágólapra másolása</string>
|
||||
<string name="add_media_to_gallery">Média hozzáadása a galériához</string>
|
||||
<string name="created_at">Létrehozva</string>
|
||||
<string name="rules">Szabályok</string>
|
||||
<string name="login_with_external_signer">Bejelentkezés Amber-el</string>
|
||||
@@ -530,6 +551,7 @@
|
||||
<string name="missing_lud16">Hiányzó lightning beállítás</string>
|
||||
<string name="user_x_does_not_have_a_lightning_address_setup_to_receive_sats">A %1$s felhasználó a sat fogadáshoz nem rendelkezik LN cím beállítással</string>
|
||||
<string name="zap_split_weight">Százalék</string>
|
||||
<string name="zap_split_weight_placeholder">25</string>
|
||||
<string name="splitting_zaps_with">Zap-ek megosztása</string>
|
||||
<string name="forwarding_zaps_to">Zap-ek továbbítása</string>
|
||||
<string name="lightning_wallets_not_found2">Lightning tárca nem található</string>
|
||||
@@ -596,9 +618,11 @@
|
||||
<string name="hi_there_is_this_still_available">Szia, ez még aktuális?</string>
|
||||
<string name="classifieds">Elem eladása</string>
|
||||
<string name="classifieds_title">Cím</string>
|
||||
<string name="classifieds_title_placeholder">iPhone 13</string>
|
||||
<string name="classifieds_condition">Állapot</string>
|
||||
<string name="classifieds_category">Kategória</string>
|
||||
<string name="classifieds_price">Ár (satoshiban)</string>
|
||||
<string name="classifieds_price_placeholder">1000</string>
|
||||
<string name="classifieds_location">Helyszín</string>
|
||||
<string name="classifieds_location_placeholder">Város, állam, ország</string>
|
||||
<string name="classifieds_condition_new">Új</string>
|
||||
@@ -617,6 +641,7 @@
|
||||
<string name="classifieds_category_books">Könyvek</string>
|
||||
<string name="classifieds_category_pets">Háziállatok</string>
|
||||
<string name="classifieds_category_sports">Sport</string>
|
||||
<string name="classifieds_category_fitness">Fitnesz</string>
|
||||
<string name="classifieds_category_art">Művészet</string>
|
||||
<string name="classifieds_category_crafts">Iparművészet</string>
|
||||
<string name="classifieds_category_home">Otthon</string>
|
||||
@@ -631,6 +656,8 @@
|
||||
<string name="server_did_not_provide_a_url_after_uploading">A szerver a feltöltés után nem adott URL-t</string>
|
||||
<string name="could_not_download_from_the_server">Nem sikerült a szerverről a feltöltött médiát letölteni</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">Nem sikerült előkészíteni a helyi fájlt a feltöltésre: %1$s</string>
|
||||
<string name="failed_to_upload_with_message">Nem sikerült feltölteni: %1$s</string>
|
||||
<string name="failed_to_delete_with_message">Nem sikerült törölni: %1$s</string>
|
||||
<string name="edit_draft">Piszkozat szerkesztése</string>
|
||||
<string name="login_with_qr_code">Bejelentkezés QR kóddal</string>
|
||||
<string name="route">Útvonal</string>
|
||||
@@ -649,6 +676,9 @@
|
||||
<string name="close_all_reactions_to_this_post">Zárja be a bejegyzésre adott összes reakciót</string>
|
||||
<string name="reply_description">Válasz</string>
|
||||
<string name="boost_or_quote_description">Megosztás vagy Idézés</string>
|
||||
<string name="like_description">Like</string>
|
||||
<string name="zap_description">Zap</string>
|
||||
<string name="change_reaction">Változtasd meg a gyors reakciókat</string>
|
||||
<string name="profile_image_of_user">%1$s profilképe</string>
|
||||
<string name="relay_info">%1$s csomópont</string>
|
||||
<string name="expand_relay_list">Csomópont lista kibontása</string>
|
||||
@@ -694,6 +724,8 @@
|
||||
<string name="private_outbox_section_explainer">Adj hozzá 1–3 csomópontot, hogy olyan eseményeket tároljon, amelyeket senki más nem láthat, például a piszkozataidat és/vagy az alkalmazásbeállításaidat. Ideális esetben ezek a csomópontok vagy helyiek, vagy az egyes felhasználók tartalmának letöltése előtt hitelesítést igényelnek.</string>
|
||||
<string name="kind_3_section">Általános Csomópontok</string>
|
||||
<string name="kind_3_section_description">Az Amethyst ezeket a csomópontokat a bejegyzések letöltésére használja.</string>
|
||||
<string name="kind_3_recommended_section">Ajánlott csomópontok</string>
|
||||
<string name="kind_3_recommended_section_description">Add hozzá a következő csomópontokat az Általános Csomópontok listájához, hogy a felsorolt felhasználóktól üzeneteket kapj.</string>
|
||||
<string name="search_section">Kereső Csomópontok</string>
|
||||
<string name="search_section_explainer">A tartalom vagy a felhasználók kereséséhez használt csomópontok listája. A címkézés és a keresés nem működik, ha itt nincsenek megadott lehetőségek. Győződj meg arról, hogy alkalmazzák a NIP-50-et.</string>
|
||||
<string name="local_section">Helyi Csomópontok</string>
|
||||
@@ -705,10 +737,14 @@
|
||||
<string name="this_version_brought_to_you_by">Ezt a verziót hozta neked:</string>
|
||||
<string name="version_name">%1$s verzió</string>
|
||||
<string name="thank_you">Köszönöm!</string>
|
||||
<string name="max_limit">Max Limit</string>
|
||||
<string name="restricted_writes">Korlátozott írások</string>
|
||||
<string name="forked_from">Fork-olva innen</string>
|
||||
<string name="forked_tag">FORK</string>
|
||||
<string name="git_repository">Git-tár: %1$s</string>
|
||||
<string name="git_web_address">Web:</string>
|
||||
<string name="git_clone_address">Klónoz:</string>
|
||||
<string name="existed_since">OTS: %1$s</string>
|
||||
<string name="ots_info_title">Időbélyeg igazolás</string>
|
||||
<string name="ots_info_description">Bizonyíték van arra, hogy ezt a bejegyzést valamikor %1$s előtt írták alá. A bizonyítékot ezen a napon és időpontban a Bitcoin blokkláncába bélyegezték.</string>
|
||||
<string name="edit_post">Bejegyzés szerkesztése</string>
|
||||
@@ -736,4 +772,36 @@
|
||||
<string name="dvm_requesting_job">Feladatot kér a DVM-től</string>
|
||||
<string name="nwc_payment_request">Fizetési kérelem elküldve, megerősítésére vár a tárcádtól</string>
|
||||
<string name="dvm_waiting_to_confirm_payment">Várakozás a DVM-re a fizetés megerősítésére vagy az eredmények elküldésére</string>
|
||||
<string name="http_status_400">Hibás kérés – A szerver nem tudja vagy nem akarja a kérést feldolgozni.</string>
|
||||
<string name="http_status_401">Jogosulatlan – A felhasználó nem rendelkezik érvényes hitelesítési adatokkal</string>
|
||||
<string name="http_status_402">Fizetés szükséges – A szerver fizetséget kér a kérés teljesítéséhez</string>
|
||||
<string name="http_status_403">Tiltott – A felhasználónak nincs hozzáférési joga a kérelem benyújtásához</string>
|
||||
<string name="http_status_404">Nem található – A szerver a kért címet nem találja</string>
|
||||
<string name="http_status_405">Módszer Nem Engedélyezett – A kiszolgáló támogatja a kérési módszert, de a célerőforrás nem</string>
|
||||
<string name="http_status_406">Nem elfogadható – A szerver nem talál olyan tartalmat, amely a kérést kielégítené.</string>
|
||||
<string name="http_status_407">Proxy hitelesítés szükséges – A felhasználó nem rendelkezik érvényes hitelesítési adatokkal</string>
|
||||
<string name="http_status_408">Kérelem időtúllépése – A szerver időtúllépés miatt valaki másra vár</string>
|
||||
<string name="http_status_409">Ütközés – A szerver nem tudja teljesíteni a kérést, mert az erőforrással konfliktus van</string>
|
||||
<string name="http_status_410">Eltűnt – A kért tartalmat a szerverről véglegesen törölték, és nem lesz visszaállítva</string>
|
||||
<string name="http_status_411">Kötelező hossz – A szerver elutasítja a kérést, mert nincs kellően definiálva</string>
|
||||
<string name="http_status_412">Előfeltétel nem sikerült – A fejlécben lévő kérés előfeltételeit, a szerver nem teljesíti</string>
|
||||
<string name="http_status_413">A kérés túl nagy – A kérés nagyobb, mint a szerver által meghatározott korlátok, és a szerver nem hajlandó feldolgozni</string>
|
||||
<string name="http_status_414">Túl hosszú URI – Az ügyfél által kért URL túl hosszú ahhoz, hogy a szerver feldolgozza. </string>
|
||||
<string name="http_status_415">Nem támogatott médiatípus – A kérés olyan médiaformátumot használ, amelyet a szerver nem támogat</string>
|
||||
<string name="http_status_416">Tartomány nem teljesíthető – A szerver nem tudja teljesíteni a kérés Tartomány fejlécében jelzett értéket. </string>
|
||||
<string name="http_status_417">Az elvárás nem sikerült – A szerver nem tudja teljesíteni a Várható kérés fejlécében jelzett követelményeket</string>
|
||||
<string name="http_status_426">Frissítés szükséges – A szerver megtagadja a kérés feldolgozását az aktuális protokoll használatával, hacsak az ügyfél nem frissít egy másik protokollra.</string>
|
||||
<string name="http_status_500">Belső szerverhiba – A szerver váratlan hibát észlelt, és nem tudja a kérést teljesíteni</string>
|
||||
<string name="http_status_501">Nincs végrehajtva – A szerver nem tudja a kérést teljesíteni, vagy nem ismeri fel a kérés módszerét</string>
|
||||
<string name="http_status_502">Rossz átjáró – A szerver átjáróként működik, és érvénytelen választ kapott a gazdagéptől</string>
|
||||
<string name="http_status_503">A szolgáltatás nem elérhető – Ez gyakran akkor fordul elő, ha a kiszolgáló túlterhelt vagy karbantartás miatt leáll</string>
|
||||
<string name="http_status_504">Átjáró Időtúllépés – A szerver átjáróként vagy proxyként működött, és a válaszra várva időtúllépés történt</string>
|
||||
<string name="http_status_505">A HTTP-verzió nem támogatott – A szerver a kérésben szereplő HTTP-verziót nem támogatja</string>
|
||||
<string name="http_status_506">Váltózó porbléma – A kiszolgálón belső konfigurációs hiba lépett fel</string>
|
||||
<string name="http_status_507">Elégtelen tárhely – A szervernek nincs elég tárhelye a kérés sikeres feldolgozásához</string>
|
||||
<string name="http_status_508">Hurok észlelve – A szerver a kérés feldolgozása közben végtelen hurkot észlel</string>
|
||||
<string name="http_status_511">Hálózati hitelesítés szükséges – A klienst a hálózat eléréséhez hitelesíteni kell</string>
|
||||
<string name="add_a_nip96_server">Adj hozzá egy NIP-96 szervert</string>
|
||||
<string name="delete_all">Törlés mind</string>
|
||||
<string name="delete_all_drafts_confirmation">Biztosan törölni szeretnéd az összes jegyzetet?</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<style name="Theme.Amethyst" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<style name="Theme.Amethyst" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<item name="android:statusBarColor">@color/purple_700</item>
|
||||
<item name="android:windowBackground">@color/black</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode" tools:ignore="NewApi">shortEdges</item>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<string name="impersonation">Podszywanie się</string>
|
||||
<string name="illegal_behavior">Antyspołeczne zachowanie</string>
|
||||
<string name="other">Inne</string>
|
||||
<string name="unknown">Nieznany</string>
|
||||
<string name="unknown">Nieznani</string>
|
||||
<string name="relay_icon">Ikona transmitera</string>
|
||||
<string name="unknown_author">Autor nieznany</string>
|
||||
<string name="copy_text">Skopiuj tekst</string>
|
||||
@@ -126,7 +126,7 @@
|
||||
<string name="copy_channel_id_note_to_the_clipboard">Kopiuj ID kanału (wpisu) do schowka</string>
|
||||
<string name="edits_the_channel_metadata">Edytuje metadane kanału</string>
|
||||
<string name="join">Dołącz</string>
|
||||
<string name="known">Popularne</string>
|
||||
<string name="known">Znajomi</string>
|
||||
<string name="new_requests">Nowe Prośby</string>
|
||||
<string name="blocked_users">Zablokowani użytkownicy</string>
|
||||
<string name="new_threads">Nowe Wpisy</string>
|
||||
@@ -725,6 +725,8 @@
|
||||
<string name="private_outbox_section_explainer">Wstaw od 1 do 3 transmiterów przechowujących dane zdarzeń, których nikt inny nie widzi, takich jak Wersje robocze i/lub ustawienia aplikacji. Idealnie byłoby, gdyby te transmitery były lokalne lub wymagały uwierzytelnienia przed pobraniem treści każdego użytkownika.</string>
|
||||
<string name="kind_3_section">Transmitery ogólne</string>
|
||||
<string name="kind_3_section_description">Amethyst używa tych przekaźników, aby pobrać dla Ciebie posty.</string>
|
||||
<string name="kind_3_recommended_section">Polecane Transmitery</string>
|
||||
<string name="kind_3_recommended_section_description">Dodaj te transmitery do głównej listy, aby otrzymywać wiadomości od wymienionych użytkowników.</string>
|
||||
<string name="search_section">Transmitery wyszukujące</string>
|
||||
<string name="search_section_explainer">Lista transmiterów używanych podczas wyszukiwania treści lub użytkowników. Tagowanie i wyszukiwanie nie będą działać, jeśli nie będą dostępne żadne opcje. Upewnij się, że zaimplementowano NIP-50.</string>
|
||||
<string name="local_section">Lokalne Transmitery</string>
|
||||
@@ -786,6 +788,7 @@
|
||||
<string name="http_status_413">Payload Zbyt duży - Żądanie jest większe niż określone limity serwera i serwer odmawia jego przetwarzania</string>
|
||||
<string name="http_status_414">URI Zbyt długi - URL klienta jest zbyt długi, aby serwer mógł go przetworzyć. </string>
|
||||
<string name="http_status_415">Nieobsługiwany format plików - żądanie używa formatu multimediów, którego serwer nie obsługuje</string>
|
||||
<string name="http_status_417">Błąd oczekiwania-serwer nie może spełnić wymagań wskazanych w polu nagłówka żądania</string>
|
||||
<string name="http_status_426">Wymagana aktualizacja - serwer odmawia przetwarzania żądania przy użyciu bieżącego protokołu, chyba że klient uaktualni go do innego protokołu.</string>
|
||||
<string name="http_status_500">Wewnętrzny błąd serwera - serwer napotkał nieoczekiwany błąd i nie może zakończyć żądania</string>
|
||||
<string name="http_status_501">Nie zaimplementowano - serwer nie może spełnić żądania lub nie rozpoznaje metody żądania</string>
|
||||
|
||||
@@ -722,6 +722,8 @@
|
||||
<string name="private_outbox_section_explainer">ใส่รีเลย์ 1-3 ตัวเพื่อเก็บเหตุการณ์ที่ไม่มีใครสามารถเห็นได้ เช่น ร่างโพสต์ และการตั้งค่าแอปของคุณ รีเลย์เหล่านี้ควรเป็นรีเลย์ภายในเครื่องหรือรีเลย์ที่ต้องมีการยืนยันตัวตนก่อนที่จะดาวน์โหลดเนื้อหาของผู้ใช้แต่ละคน</string>
|
||||
<string name="kind_3_section">รีเลย์ทั่วไป</string>
|
||||
<string name="kind_3_section_description">Amethyst ใช้รีเลย์เหล่านี้เพื่อดาวน์โหลดโพสต์สำหรับคุณ</string>
|
||||
<string name="kind_3_recommended_section">รีเลย์ที่แนะนำ</string>
|
||||
<string name="kind_3_recommended_section_description">เพิ่มรีเลย์เข้ารายการรีเลย์ทั่วไปเพื่อให้เห็นโพสต์จากผู้ใช้อื่น ๆ มากขึ้น</string>
|
||||
<string name="search_section">ค้นหารีเลย์</string>
|
||||
<string name="search_section_explainer">รายการรีเลย์ที่จะใช้เมื่อค้นหาข้อมูลหรือผู้ใช้ การแท็กและการค้นหาจะไม่ทำงานหากไม่มีตัวเลือก ตรวจสอบให้แน่ใจว่ารีเลย์เหล่านี้รองรับ NIP-50</string>
|
||||
<string name="local_section">รีเลย์ในพื้นที่ของคุณ</string>
|
||||
|
||||
@@ -726,6 +726,7 @@
|
||||
<string name="kind_3_section">通用中继</string>
|
||||
<string name="kind_3_section_description">Amethyst 会通过这些中继为您获取帖子。</string>
|
||||
<string name="kind_3_recommended_section">推荐的中继器</string>
|
||||
<string name="kind_3_recommended_section_description">在通用中继列表中添加下列中继以接收列表中用户的帖子。</string>
|
||||
<string name="search_section">搜索中继</string>
|
||||
<string name="search_section_explainer">用于关键词和标签检索的中继列表。如果没有可用选项,也就无法使用关键词和标签检索。需要确保它们支持 NIP-50。</string>
|
||||
<string name="local_section">本地中继</string>
|
||||
@@ -802,4 +803,6 @@
|
||||
<string name="http_status_508">Loop Detected - 服务器处理该请求时检测到死循环。</string>
|
||||
<string name="http_status_511">Network Authentication Required - 客户端必须通过认证才能访问该网络。</string>
|
||||
<string name="add_a_nip96_server">添加 NIP-96 服务器</string>
|
||||
<string name="delete_all">删除所有</string>
|
||||
<string name="delete_all_drafts_confirmation">确定要删除所有草稿吗?</string>
|
||||
</resources>
|
||||
|
||||
@@ -949,4 +949,6 @@
|
||||
<string name="http_status_511">Network Authentication Required - The client must be authenticated to access the network</string>
|
||||
|
||||
<string name="add_a_nip96_server">Add a NIP-96 Server</string>
|
||||
<string name="delete_all">Delete all</string>
|
||||
<string name="delete_all_drafts_confirmation">Are you sure you want to delete all drafts?</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
# preserve the line number information for debugging stack traces.
|
||||
-dontobfuscate
|
||||
-keepattributes LocalVariableTable
|
||||
-keepattributes LocalVariableTypeTable
|
||||
-keepattributes *Annotation*
|
||||
-keepattributes SourceFile
|
||||
-keepattributes LineNumberTable
|
||||
-keepattributes Signature
|
||||
-keepattributes Exceptions
|
||||
-keepattributes InnerClasses
|
||||
-keepattributes EnclosingMethod
|
||||
-keepattributes MethodParameters
|
||||
-keepparameternames
|
||||
|
||||
-keepdirectories libs
|
||||
|
||||
# Keep all names
|
||||
-keepnames class ** { *; }
|
||||
|
||||
# Keep All enums
|
||||
-keep enum ** { *; }
|
||||
|
||||
-keep class com.vitorpamplona.ammolite.service.** { *; }
|
||||
-keep class com.vitorpamplona.ammolite.relays.** { *; }
|
||||
Vendored
+21
@@ -19,6 +19,27 @@
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
# preserve the line number information for debugging stack traces.
|
||||
-dontobfuscate
|
||||
-keepattributes LocalVariableTable
|
||||
-keepattributes LocalVariableTypeTable
|
||||
-keepattributes *Annotation*
|
||||
-keepattributes SourceFile
|
||||
-keepattributes LineNumberTable
|
||||
-keepattributes Signature
|
||||
-keepattributes Exceptions
|
||||
-keepattributes InnerClasses
|
||||
-keepattributes EnclosingMethod
|
||||
-keepattributes MethodParameters
|
||||
-keepparameternames
|
||||
|
||||
-keepdirectories libs
|
||||
|
||||
# Keep all names
|
||||
-keepnames class ** { *; }
|
||||
|
||||
# Keep All enums
|
||||
-keep enum ** { *; }
|
||||
|
||||
-keep class com.vitorpamplona.ammolite.service.** { *; }
|
||||
-keep class com.vitorpamplona.ammolite.relays.** { *; }
|
||||
@@ -28,7 +28,10 @@ import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* The Nostr Client manages multiple personae the user may switch between. Events are received and
|
||||
@@ -110,7 +113,7 @@ object Client : RelayPool.Listener {
|
||||
checkNotInMainThread()
|
||||
|
||||
subscribe(
|
||||
object : Listener() {
|
||||
object : Listener {
|
||||
override fun onEvent(
|
||||
event: Event,
|
||||
subId: String,
|
||||
@@ -130,6 +133,87 @@ object Client : RelayPool.Listener {
|
||||
RelayPool.sendFilter(subscriptionId, filters)
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
suspend fun sendAndWaitForResponse(
|
||||
signedEvent: EventInterface,
|
||||
relay: String? = null,
|
||||
feedTypes: Set<FeedType>? = null,
|
||||
relayList: List<RelaySetupInfo>? = null,
|
||||
onDone: (() -> Unit)? = null,
|
||||
additionalListener: Listener? = null,
|
||||
timeoutInSeconds: Long = 15,
|
||||
): Boolean {
|
||||
checkNotInMainThread()
|
||||
|
||||
val size = if (relay != null) 1 else relayList?.size ?: RelayPool.availableRelays()
|
||||
val latch = CountDownLatch(size)
|
||||
val relayErrors = mutableMapOf<String, String>()
|
||||
var result = false
|
||||
|
||||
Log.d("sendAndWaitForResponse", "Waiting for $size responses")
|
||||
|
||||
val subscription =
|
||||
object : Listener {
|
||||
override fun onError(
|
||||
error: Error,
|
||||
subscriptionId: String,
|
||||
relay: Relay,
|
||||
) {
|
||||
relayErrors[relay.url]?.let {
|
||||
latch.countDown()
|
||||
}
|
||||
Log.d("sendAndWaitForResponse", "onError Error from relay ${relay.url} count: ${latch.count} error: $error")
|
||||
}
|
||||
|
||||
override fun onRelayStateChange(
|
||||
type: Relay.StateType,
|
||||
relay: Relay,
|
||||
subscriptionId: String?,
|
||||
) {
|
||||
if (type == Relay.StateType.DISCONNECT || type == Relay.StateType.EOSE) {
|
||||
latch.countDown()
|
||||
}
|
||||
if (type == Relay.StateType.CONNECT) {
|
||||
Log.d("sendAndWaitForResponse", "${type.name} Sending event to relay ${relay.url} count: ${latch.count}")
|
||||
relay.sendOverride(signedEvent)
|
||||
}
|
||||
Log.d("sendAndWaitForResponse", "onRelayStateChange ${type.name} from relay ${relay.url} count: ${latch.count}")
|
||||
}
|
||||
|
||||
override fun onSendResponse(
|
||||
eventId: String,
|
||||
success: Boolean,
|
||||
message: String,
|
||||
relay: Relay,
|
||||
) {
|
||||
if (eventId == signedEvent.id()) {
|
||||
if (success) {
|
||||
result = true
|
||||
}
|
||||
latch.countDown()
|
||||
Log.d("sendAndWaitForResponse", "onSendResponse Received response for $eventId from relay ${relay.url} count: ${latch.count} message $message success $success")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subscribe(subscription)
|
||||
additionalListener?.let { subscribe(it) }
|
||||
|
||||
val job =
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
send(signedEvent, relay, feedTypes, relayList, onDone)
|
||||
}
|
||||
job.join()
|
||||
|
||||
runBlocking {
|
||||
latch.await(timeoutInSeconds, TimeUnit.SECONDS)
|
||||
}
|
||||
Log.d("sendAndWaitForResponse", "countdown finished")
|
||||
unsubscribe(subscription)
|
||||
additionalListener?.let { unsubscribe(it) }
|
||||
return result
|
||||
}
|
||||
|
||||
fun sendFilterOnlyIfDisconnected(
|
||||
subscriptionId: String = UUID.randomUUID().toString().substring(0..10),
|
||||
filters: List<TypedFilter> = listOf(),
|
||||
@@ -206,6 +290,7 @@ object Client : RelayPool.Listener {
|
||||
// }
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onSendResponse(
|
||||
eventId: String,
|
||||
success: Boolean,
|
||||
@@ -219,6 +304,7 @@ object Client : RelayPool.Listener {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onAuth(
|
||||
relay: Relay,
|
||||
challenge: String,
|
||||
@@ -228,6 +314,7 @@ object Client : RelayPool.Listener {
|
||||
GlobalScope.launch(Dispatchers.Default) { listeners.forEach { it.onAuth(relay, challenge) } }
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onNotify(
|
||||
relay: Relay,
|
||||
description: String,
|
||||
@@ -239,6 +326,7 @@ object Client : RelayPool.Listener {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onSend(
|
||||
relay: Relay,
|
||||
msg: String,
|
||||
@@ -249,6 +337,7 @@ object Client : RelayPool.Listener {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onBeforeSend(
|
||||
relay: Relay,
|
||||
event: EventInterface,
|
||||
@@ -258,6 +347,7 @@ object Client : RelayPool.Listener {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onError(
|
||||
error: Error,
|
||||
subscriptionId: String,
|
||||
@@ -282,7 +372,7 @@ object Client : RelayPool.Listener {
|
||||
|
||||
fun getSubscriptionFilters(subId: String): List<TypedFilter> = subscriptions[subId] ?: emptyList()
|
||||
|
||||
abstract class Listener {
|
||||
interface Listener {
|
||||
/** A new message was received */
|
||||
open fun onEvent(
|
||||
event: Event,
|
||||
|
||||
@@ -67,7 +67,7 @@ abstract class NostrDataSource(
|
||||
): Int = 31 * str1.hashCode() + str2.hashCode()
|
||||
|
||||
private val clientListener =
|
||||
object : Client.Listener() {
|
||||
object : Client.Listener {
|
||||
override fun onEvent(
|
||||
event: Event,
|
||||
subscriptionId: String,
|
||||
@@ -138,13 +138,13 @@ abstract class NostrDataSource(
|
||||
}
|
||||
|
||||
init {
|
||||
Log.d(this.javaClass.simpleName, "${this.javaClass.simpleName} Subscribe")
|
||||
Log.d("DataSource", "${this.javaClass.simpleName} Subscribe")
|
||||
Client.subscribe(clientListener)
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
// makes sure to run
|
||||
Log.d(this.javaClass.simpleName, "${this.javaClass.simpleName} Unsubscribe")
|
||||
Log.d("DataSource", "${this.javaClass.simpleName} Unsubscribe")
|
||||
stop()
|
||||
Client.unsubscribe(clientListener)
|
||||
scope.cancel()
|
||||
@@ -152,7 +152,7 @@ abstract class NostrDataSource(
|
||||
}
|
||||
|
||||
open fun start() {
|
||||
println("DataSource: ${this.javaClass.simpleName} Start")
|
||||
Log.d("DataSource", "${this.javaClass.simpleName} Start")
|
||||
active = true
|
||||
resetFilters()
|
||||
}
|
||||
@@ -160,7 +160,7 @@ abstract class NostrDataSource(
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
open fun stop() {
|
||||
active = false
|
||||
println("DataSource: ${this.javaClass.simpleName} Stop")
|
||||
Log.d("DataSource", "${this.javaClass.simpleName} Stop")
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
subscriptions.values.forEach { subscription ->
|
||||
@@ -172,7 +172,7 @@ abstract class NostrDataSource(
|
||||
|
||||
open fun stopSync() {
|
||||
active = false
|
||||
println("DataSource: ${this.javaClass.simpleName} Stop")
|
||||
Log.d("DataSource", "${this.javaClass.simpleName} Stop")
|
||||
|
||||
subscriptions.values.forEach { subscription ->
|
||||
Client.close(subscription.id)
|
||||
@@ -209,7 +209,7 @@ abstract class NostrDataSource(
|
||||
}
|
||||
|
||||
private fun resetFiltersSuspend() {
|
||||
println("DataSource: ${this.javaClass.simpleName} resetFiltersSuspend $active")
|
||||
Log.d("DataSource", "${this.javaClass.simpleName} resetFiltersSuspend $active")
|
||||
checkNotInMainThread()
|
||||
|
||||
// saves the channels that are currently active
|
||||
|
||||
@@ -75,6 +75,7 @@ class Relay(
|
||||
private var afterEOSEPerSubscription = mutableMapOf<String, Boolean>()
|
||||
|
||||
private val authResponse = mutableMapOf<HexKey, Boolean>()
|
||||
private val authChallengesSent = mutableSetOf<String>()
|
||||
private val outboxCache = mutableMapOf<HexKey, EventInterface>()
|
||||
|
||||
fun register(listener: Listener) {
|
||||
@@ -232,20 +233,20 @@ class Relay(
|
||||
// checks if this is an actual failure. Closing the socket generates an onFailure as well.
|
||||
if (!(socket == null && (t.message == "Socket is closed" || t.message == "Socket closed"))) {
|
||||
RelayStats.newError(url, response?.message ?: t.message)
|
||||
|
||||
Log.w("Relay", "Relay onFailure $url, ${response?.message} $response ${t.message} $socket")
|
||||
t.printStackTrace()
|
||||
listeners.forEach {
|
||||
it.onError(
|
||||
this@Relay,
|
||||
"",
|
||||
Error("WebSocket Failure. Response: $response. Exception: ${t.message}", t),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Failures disconnect the relay.
|
||||
markConnectionAsClosed()
|
||||
|
||||
Log.w("Relay", "Relay onFailure $url, ${response?.message} $response ${t.message} $socket")
|
||||
t.printStackTrace()
|
||||
listeners.forEach {
|
||||
it.onError(
|
||||
this@Relay,
|
||||
"",
|
||||
Error("WebSocket Failure. Response: $response. Exception: ${t.message}", t),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,6 +373,9 @@ class Relay(
|
||||
|
||||
fun resetEOSEStatuses() {
|
||||
afterEOSEPerSubscription = LinkedHashMap(afterEOSEPerSubscription.size)
|
||||
|
||||
authResponse.clear()
|
||||
authChallengesSent.clear()
|
||||
}
|
||||
|
||||
fun sendFilter(
|
||||
@@ -492,8 +496,19 @@ class Relay(
|
||||
}
|
||||
|
||||
private fun sendAuth(signedEvent: RelayAuthEvent) {
|
||||
authResponse.put(signedEvent.id, false)
|
||||
writeToSocket("""["AUTH",${signedEvent.toJson()}]""")
|
||||
val challenge = signedEvent.challenge() ?: ""
|
||||
|
||||
// only send replies to new challenges to avoid infinite loop:
|
||||
// 1. Auth is sent
|
||||
// 2. auth is rejected
|
||||
// 3. auth is requested
|
||||
// 4. auth is sent
|
||||
// ...
|
||||
if (!authChallengesSent.contains(challenge)) {
|
||||
authResponse.put(signedEvent.id, false)
|
||||
authChallengesSent.add(challenge)
|
||||
writeToSocket("""["AUTH",${signedEvent.toJson()}]""")
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendEvent(signedEvent: EventInterface) {
|
||||
@@ -533,7 +548,7 @@ class Relay(
|
||||
}
|
||||
RelayStats.addBytesSent(url, str.bytesUsedInMemory())
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
if (BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "benchmark") {
|
||||
Log.d("Relay", "Relay send $url $str")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,7 +235,7 @@ class RichTextParser {
|
||||
|
||||
if (word.startsWith("lnurl", true)) return WithdrawSegment(word)
|
||||
|
||||
if (word.startsWith("cashuA", true)) return CashuSegment(word)
|
||||
if (word.startsWith("cashuA", true) || word.startsWith("cashuB", true)) return CashuSegment(word)
|
||||
|
||||
if (word.startsWith("#")) return parseHash(word, tags)
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="r-4qtqp9 r-yyyyoo r-1xvli5t r-dnmrzs r-bnwqim r-1plcrui r-lrvibr r-1hdv0qi"><g><path d="M12 21.638h-.014C9.403 21.59 1.95 14.856 1.95 8.478c0-3.064 2.525-5.754 5.403-5.754 2.29 0 3.83 1.58 4.646 2.73.814-1.148 2.354-2.73 4.645-2.73 2.88 0 5.404 2.69 5.404 5.755 0 6.376-7.454 13.11-10.037 13.157H12zM7.354 4.225c-2.08 0-3.903 1.988-3.903 4.255 0 5.74 7.034 11.596 8.55 11.658 1.518-.062 8.55-5.917 8.55-11.658 0-2.267-1.823-4.255-3.903-4.255-2.528 0-3.94 2.936-3.952 2.965-.23.562-1.156.562-1.387 0-.014-.03-1.425-2.965-3.954-2.965z"></path></g></svg>
|
||||
|
After Width: | Height: | Size: 618 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="r-4qtqp9 r-yyyyoo r-1xvli5t r-dnmrzs r-bnwqim r-1plcrui r-lrvibr r-1hdv0qi"><g><path d="M14.046 2.242l-4.148-.01h-.002c-4.374 0-7.8 3.427-7.8 7.802 0 4.098 3.186 7.206 7.465 7.37v3.828c0 .108.044.286.12.403.142.225.384.347.632.347.138 0 .277-.038.402-.118.264-.168 6.473-4.14 8.088-5.506 1.902-1.61 3.04-3.97 3.043-6.312v-.017c-.006-4.367-3.43-7.787-7.8-7.788zm3.787 12.972c-1.134.96-4.862 3.405-6.772 4.643V16.67c0-.414-.335-.75-.75-.75h-.396c-3.66 0-6.318-2.476-6.318-5.886 0-3.534 2.768-6.302 6.3-6.302l4.147.01h.002c3.532 0 6.3 2.766 6.302 6.296-.003 1.91-.942 3.844-2.514 5.176z"></path></g></svg>
|
||||
|
After Width: | Height: | Size: 669 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="r-4qtqp9 r-yyyyoo r-1xvli5t r-dnmrzs r-bnwqim r-1plcrui r-lrvibr r-1hdv0qi"><g><path d="M23.77 15.67c-.292-.293-.767-.293-1.06 0l-2.22 2.22V7.65c0-2.068-1.683-3.75-3.75-3.75h-5.85c-.414 0-.75.336-.75.75s.336.75.75.75h5.85c1.24 0 2.25 1.01 2.25 2.25v10.24l-2.22-2.22c-.293-.293-.768-.293-1.06 0s-.294.768 0 1.06l3.5 3.5c.145.147.337.22.53.22s.383-.072.53-.22l3.5-3.5c.294-.292.294-.767 0-1.06zm-10.66 3.28H7.26c-1.24 0-2.25-1.01-2.25-2.25V6.46l2.22 2.22c.148.147.34.22.532.22s.384-.073.53-.22c.293-.293.293-.768 0-1.06l-3.5-3.5c-.293-.294-.768-.294-1.06 0l-3.5 3.5c-.294.292-.294.767 0 1.06s.767.293 1.06 0l2.22-2.22V16.7c0 2.068 1.683 3.75 3.75 3.75h5.85c.414 0 .75-.336.75-.75s-.337-.75-.75-.75z"></path></g></svg>
|
||||
|
After Width: | Height: | Size: 782 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="r-m0bqgq r-4qtqp9 r-yyyyoo r-1xvli5t r-dnmrzs r-18qmn74 r-1hfyk0a r-bnwqim r-1plcrui r-lrvibr"><g><path d="M21.53 20.47l-3.66-3.66C19.195 15.24 20 13.214 20 11c0-4.97-4.03-9-9-9s-9 4.03-9 9 4.03 9 9 9c2.215 0 4.24-.804 5.808-2.13l3.66 3.66c.147.146.34.22.53.22s.385-.073.53-.22c.295-.293.295-.767.002-1.06zM3.5 11c0-4.135 3.365-7.5 7.5-7.5s7.5 3.365 7.5 7.5-3.365 7.5-7.5 7.5-7.5-3.365-7.5-7.5z"></path></g></svg>
|
||||
|
After Width: | Height: | Size: 480 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="r-4qtqp9 r-yyyyoo r-1xvli5t r-dnmrzs r-bnwqim r-1plcrui r-lrvibr r-1hdv0qi"><g><path d="M17.53 7.47l-5-5c-.293-.293-.768-.293-1.06 0l-5 5c-.294.293-.294.768 0 1.06s.767.294 1.06 0l3.72-3.72V15c0 .414.336.75.75.75s.75-.336.75-.75V4.81l3.72 3.72c.146.147.338.22.53.22s.384-.072.53-.22c.293-.293.293-.767 0-1.06z"></path><path d="M19.708 21.944H4.292C3.028 21.944 2 20.916 2 19.652V14c0-.414.336-.75.75-.75s.75.336.75.75v5.652c0 .437.355.792.792.792h15.416c.437 0 .792-.355.792-.792V14c0-.414.336-.75.75-.75s.75.336.75.75v5.652c0 1.264-1.028 2.292-2.292 2.292z"></path></g></svg>
|
||||
|
After Width: | Height: | Size: 643 B |
@@ -1,6 +1,6 @@
|
||||
[versions]
|
||||
accompanistAdaptive = "0.34.0"
|
||||
activityCompose = "1.9.0"
|
||||
activityCompose = "1.9.1"
|
||||
agp = "8.5.1"
|
||||
android-compileSdk = "34"
|
||||
android-minSdk = "26"
|
||||
@@ -13,24 +13,26 @@ benchmark = "1.2.4"
|
||||
benchmarkJunit4 = "1.2.4"
|
||||
biometricKtx = "1.2.0-alpha05"
|
||||
blurhash = "1.0.0"
|
||||
coil = "2.6.0"
|
||||
coil = "2.7.0"
|
||||
composeBom = "2024.06.00"
|
||||
coreKtx = "1.13.1"
|
||||
espressoCore = "3.6.1"
|
||||
firebaseBom = "33.1.2"
|
||||
fragmentKtx = "1.8.1"
|
||||
fragmentKtx = "1.8.2"
|
||||
gms = "4.4.2"
|
||||
jacksonModuleKotlin = "2.17.1"
|
||||
jacksonModuleKotlin = "2.17.2"
|
||||
jna = "5.14.0"
|
||||
junit = "4.13.2"
|
||||
kotlin = "2.0.0"
|
||||
kotlinxCollectionsImmutable = "0.3.7"
|
||||
kotlinxSerialization = "1.7.1"
|
||||
kotlinxSerializationPlugin = "2.0.0"
|
||||
languageId = "17.0.5"
|
||||
lazysodiumAndroid = "5.1.0"
|
||||
lifecycleRuntimeKtx = "2.8.3"
|
||||
lifecycleRuntimeKtx = "2.8.4"
|
||||
lightcompressor = "1.3.2"
|
||||
markdown = "077a2cde64"
|
||||
media3 = "1.3.1"
|
||||
media3 = "1.4.0"
|
||||
mockk = "1.13.12"
|
||||
navigationCompose = "2.7.7"
|
||||
okhttp = "5.0.0-alpha.14"
|
||||
@@ -98,6 +100,7 @@ jackson-module-kotlin = { group = "com.fasterxml.jackson.module", name = "jackso
|
||||
jna = { group = "net.java.dev.jna", name = "jna", version.ref = "jna" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" }
|
||||
kotlinx-serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kotlinxSerialization" }
|
||||
lazysodium-android = { group = "com.goterl", name = "lazysodium-android", version.ref = "lazysodiumAndroid" }
|
||||
markdown-commonmark = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-commonmark", version.ref = "markdown" }
|
||||
markdown-ui = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-ui", version.ref = "markdown" }
|
||||
@@ -126,4 +129,5 @@ diffplugSpotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
|
||||
googleServices = { id = "com.google.gms.google-services", version.ref = "gms" }
|
||||
jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
jetbrainsKotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||
jetbrainsComposeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
jetbrainsComposeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
serialization = { id = 'org.jetbrains.kotlin.plugin.serialization', version.ref = 'kotlinxSerializationPlugin' }
|
||||
@@ -44,6 +44,24 @@ class Nip01Test {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetPublicCompressedWith2() {
|
||||
val key = "e6159851715b4aa6190c22b899b0c792847de0a4435ac5b678f35738351c43b0".hexToByteArray()
|
||||
assertEquals(
|
||||
"029fa4ce8c87ca546b196e6518db80a6780e1bd5552b61f9f17bafee5d4e34e09b",
|
||||
nip01.compressedPubkeyCreate(key).toHexKey(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetPublicCompressedWith3() {
|
||||
val key = "65f039136f8da8d3e87b4818746b53318d5481e24b2673f162815144223a0b5a".hexToByteArray()
|
||||
assertEquals(
|
||||
"033dcef7585efbdb68747d919152bd481e21f5e952aaaef5a19604fbd096a93dd5",
|
||||
nip01.compressedPubkeyCreate(key).toHexKey(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDeterministicSign() {
|
||||
assertEquals(
|
||||
|
||||
@@ -25,6 +25,7 @@ import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.crypto.nip01.Nip01
|
||||
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import fr.acinq.secp256k1.Secp256k1
|
||||
@@ -48,6 +49,7 @@ class NIP44v2Test {
|
||||
|
||||
private val random = SecureRandom()
|
||||
private val nip44v2 = Nip44v2(Secp256k1.get(), random)
|
||||
private val nip01 = Nip01(Secp256k1.get(), random)
|
||||
|
||||
@Test
|
||||
fun conversationKeyTest() {
|
||||
@@ -67,6 +69,34 @@ class NIP44v2Test {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCompressedWith02Keys() {
|
||||
val privateKeyA = "f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561".hexToByteArray()
|
||||
val privateKeyB = "65f039136f8da8d3e87b4818746b53318d5481e24b2673f162815144223a0b5a".hexToByteArray()
|
||||
|
||||
val publicKeyA = nip01.pubkeyCreate(privateKeyA)
|
||||
val publicKeyB = nip01.pubkeyCreate(privateKeyB)
|
||||
|
||||
assertEquals(
|
||||
nip44v2.getConversationKey(privateKeyA, publicKeyB).toHexKey(),
|
||||
nip44v2.getConversationKey(privateKeyB, publicKeyA).toHexKey(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCompressedWith03Keys() {
|
||||
val privateKeyA = "f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561".hexToByteArray()
|
||||
val privateKeyB = "e6159851715b4aa6190c22b899b0c792847de0a4435ac5b678f35738351c43b0".hexToByteArray()
|
||||
|
||||
val publicKeyA = nip01.pubkeyCreate(privateKeyA)
|
||||
val publicKeyB = nip01.pubkeyCreate(privateKeyB)
|
||||
|
||||
assertEquals(
|
||||
nip44v2.getConversationKey(privateKeyA, publicKeyB).toHexKey(),
|
||||
nip44v2.getConversationKey(privateKeyB, publicKeyA).toHexKey(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun encryptDecryptTest() {
|
||||
for (v in vectors.v2?.valid?.encryptDecrypt!!) {
|
||||
|
||||
@@ -33,7 +33,9 @@ class Nip01(
|
||||
/** Provides a 32B "private key" aka random number */
|
||||
fun privkeyCreate() = random(32)
|
||||
|
||||
fun pubkeyCreate(privKey: ByteArray) = secp256k1.pubKeyCompress(secp256k1.pubkeyCreate(privKey)).copyOfRange(1, 33)
|
||||
fun compressedPubkeyCreate(privKey: ByteArray) = secp256k1.pubKeyCompress(secp256k1.pubkeyCreate(privKey))
|
||||
|
||||
fun pubkeyCreate(privKey: ByteArray) = compressedPubkeyCreate(privKey).copyOfRange(1, 33)
|
||||
|
||||
fun sign(
|
||||
data: ByteArray,
|
||||
|
||||
@@ -59,7 +59,7 @@ class FileServersEvent(
|
||||
servers
|
||||
.map {
|
||||
arrayOf("server", it)
|
||||
}.plusElement(arrayOf("alt", "Relay list to use for Search"))
|
||||
}.plusElement(arrayOf("alt", ALT))
|
||||
.toTypedArray()
|
||||
|
||||
fun updateRelayList(
|
||||
|
||||
@@ -58,9 +58,9 @@ class ProfileGalleryEntryEvent(
|
||||
|
||||
fun hasUrl() = tags.any { it.size > 1 && it[0] == URL }
|
||||
|
||||
fun event() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1)
|
||||
fun fromEvent() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1)
|
||||
|
||||
fun hasEvent() = tags.any { it.size > 1 && it[0] == "e" }
|
||||
fun hasFromEvent() = tags.any { it.size > 1 && it[0] == "e" }
|
||||
|
||||
fun isOneOf(mimeTypes: Set<String>) = tags.any { it.size > 1 && it[0] == MIME_TYPE && mimeTypes.contains(it[1]) }
|
||||
|
||||
|
||||
@@ -56,5 +56,23 @@ class RelayAuthEvent(
|
||||
)
|
||||
signer.sign(createdAt, KIND, tags, content, onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
relays: List<String>,
|
||||
challenge: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (RelayAuthEvent) -> Unit,
|
||||
) {
|
||||
val content = ""
|
||||
val tags =
|
||||
relays
|
||||
.map {
|
||||
arrayOf("relay", it)
|
||||
}.plusElement(
|
||||
arrayOf("challenge", challenge),
|
||||
).toTypedArray()
|
||||
signer.sign(createdAt, KIND, tags, content, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user