mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-04-08 20:08:06 +02:00
Merge branch 'main' into main
This commit is contained in:
commit
499939ed68
@ -162,13 +162,13 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation platform(libs.androidx.compose.bom)
|
||||
|
||||
implementation project(path: ':quartz')
|
||||
implementation project(path: ':commons')
|
||||
implementation libs.androidx.core.ktx
|
||||
implementation libs.androidx.activity.compose
|
||||
|
||||
implementation platform(libs.androidx.compose.bom)
|
||||
|
||||
implementation libs.androidx.ui
|
||||
implementation libs.androidx.ui.graphics
|
||||
implementation libs.androidx.ui.tooling.preview
|
||||
@ -282,9 +282,13 @@ dependencies {
|
||||
|
||||
testImplementation libs.junit
|
||||
testImplementation libs.mockk
|
||||
|
||||
androidTestImplementation platform(libs.androidx.compose.bom)
|
||||
androidTestImplementation libs.androidx.junit
|
||||
androidTestImplementation libs.androidx.junit.ktx
|
||||
androidTestImplementation libs.androidx.espresso.core
|
||||
|
||||
debugImplementation platform(libs.androidx.compose.bom)
|
||||
debugImplementation libs.androidx.ui.tooling
|
||||
debugImplementation libs.androidx.ui.test.manifest
|
||||
}
|
||||
|
@ -20,6 +20,8 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.FileHeader
|
||||
@ -29,70 +31,17 @@ import com.vitorpamplona.amethyst.service.Nip96Uploader
|
||||
import com.vitorpamplona.amethyst.ui.actions.ImageDownloader
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.assertTrue
|
||||
import junit.framework.TestCase.fail
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.util.Base64
|
||||
import java.io.ByteArrayOutputStream
|
||||
import kotlin.random.Random
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ImageUploadTesting {
|
||||
val contentType = "image/gif"
|
||||
val image =
|
||||
"R0lGODlhPQBEAPeoAJosM//AwO/AwHVYZ/z595kzAP/s7P+goOXMv8+fhw/v739/f+8PD98fH/8mJl+fn/9ZWb8/PzW" +
|
||||
"lwv///6wWGbImAPgTEMImIN9gUFCEm/gDALULDN8PAD6atYdCTX9gUNKlj8wZAKUsAOzZz+UMAOsJAP/Z2c" +
|
||||
"cMDA8PD/95eX5NWvsJCOVNQPtfX/8zM8+QePLl38MGBr8JCP+zs9myn/8GBqwpAP/GxgwJCPny78lzYLgjA" +
|
||||
"J8vAP9fX/+MjMUcAN8zM/9wcM8ZGcATEL+QePdZWf/29uc/P9cmJu9MTDImIN+/r7+/vz8/P8VNQGNugV8A" +
|
||||
"AF9fX8swMNgTAFlDOICAgPNSUnNWSMQ5MBAQEJE3QPIGAM9AQMqGcG9vb6MhJsEdGM8vLx8fH98AANIWAMu" +
|
||||
"QeL8fABkTEPPQ0OM5OSYdGFl5jo+Pj/+pqcsTE78wMFNGQLYmID4dGPvd3UBAQJmTkP+8vH9QUK+vr8ZWSH" +
|
||||
"pzcJMmILdwcLOGcHRQUHxwcK9PT9DQ0O/v70w5MLypoG8wKOuwsP/g4P/Q0IcwKEswKMl8aJ9fX2xjdOtGR" +
|
||||
"s/Pz+Dg4GImIP8gIH0sKEAwKKmTiKZ8aB/f39Wsl+LFt8dgUE9PT5x5aHBwcP+AgP+WltdgYMyZfyywz78A" +
|
||||
"AAAAAAD///8AAP9mZv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAKgALAAAAAA9AEQAAAj/AFEJHEiwoMGDCBMqXMi" +
|
||||
"wocAbBww4nEhxoYkUpzJGrMixogkfGUNqlNixJEIDB0SqHGmyJSojM1bKZOmyop0gM3Oe2liTISKMOoPy7G" +
|
||||
"nwY9CjIYcSRYm0aVKSLmE6nfq05QycVLPuhDrxBlCtYJUqNAq2bNWEBj6ZXRuyxZyDRtqwnXvkhACDV+euT" +
|
||||
"eJm1Ki7A73qNWtFiF+/gA95Gly2CJLDhwEHMOUAAuOpLYDEgBxZ4GRTlC1fDnpkM+fOqD6DDj1aZpITp0dt" +
|
||||
"GCDhr+fVuCu3zlg49ijaokTZTo27uG7Gjn2P+hI8+PDPERoUB318bWbfAJ5sUNFcuGRTYUqV/3ogfXp1rWl" +
|
||||
"Mc6awJjiAAd2fm4ogXjz56aypOoIde4OE5u/F9x199dlXnnGiHZWEYbGpsAEA3QXYnHwEFliKAgswgJ8LPe" +
|
||||
"iUXGwedCAKABACCN+EA1pYIIYaFlcDhytd51sGAJbo3onOpajiihlO92KHGaUXGwWjUBChjSPiWJuOO/LYI" +
|
||||
"m4v1tXfE6J4gCSJEZ7YgRYUNrkji9P55sF/ogxw5ZkSqIDaZBV6aSGYq/lGZplndkckZ98xoICbTcIJGQAZ" +
|
||||
"cNmdmUc210hs35nCyJ58fgmIKX5RQGOZowxaZwYA+JaoKQwswGijBV4C6SiTUmpphMspJx9unX4KaimjDv9" +
|
||||
"aaXOEBteBqmuuxgEHoLX6Kqx+yXqqBANsgCtit4FWQAEkrNbpq7HSOmtwag5w57GrmlJBASEU18ADjUYb3A" +
|
||||
"DTinIttsgSB1oJFfA63bduimuqKB1keqwUhoCSK374wbujvOSu4QG6UvxBRydcpKsav++Ca6G8A6Pr1x2kV" +
|
||||
"MyHwsVxUALDq/krnrhPSOzXG1lUTIoffqGR7Goi2MAxbv6O2kEG56I7CSlRsEFKFVyovDJoIRTg7sugNRDG" +
|
||||
"qCJzJgcKE0ywc0ELm6KBCCJo8DIPFeCWNGcyqNFE06ToAfV0HBRgxsvLThHn1oddQMrXj5DyAQgjEHSAJMW" +
|
||||
"ZwS3HPxT/QMbabI/iBCliMLEJKX2EEkomBAUCxRi42VDADxyTYDVogV+wSChqmKxEKCDAYFDFj4OmwbY7bD" +
|
||||
"GdBhtrnTQYOigeChUmc1K3QTnAUfEgGFgAWt88hKA6aCRIXhxnQ1yg3BCayK44EWdkUQcBByEQChFXfCB77" +
|
||||
"6aQsG0BIlQgQgE8qO26X1h8cEUep8ngRBnOy74E9QgRgEAC8SvOfQkh7FDBDmS43PmGoIiKUUEGkMEC/PJH" +
|
||||
"gxw0xH74yx/3XnaYRJgMB8obxQW6kL9QYEJ0FIFgByfIL7/IQAlvQwEpnAC7DtLNJCKUoO/w45c44GwCXiA" +
|
||||
"FB/OXAATQryUxdN4LfFiwgjCNYg+kYMIEFkCKDs6PKAIJouyGWMS1FSKJOMRB/BoIxYJIUXFUxNwoIkEKPA" +
|
||||
"gCBZSQHQ1A2EWDfDEUVLyADj5AChSIQW6gu10bE/JG2VnCZGfo4R4d0sdQoBAHhPjhIB94v/wRoRKQWGRHg" +
|
||||
"rhGSQJxCS+0pCZbEhAAOw=="
|
||||
|
||||
val contentTypePng = "image/png"
|
||||
val imagePng =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAApgAAAKYB3X3" +
|
||||
"/OAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANCSURBVEiJtZZPbBtFFMZ/M7ubXd" +
|
||||
"tdb1xSFyeilBapySVU8h8OoFaooFSqiihIVIpQBKci6KEg9Q6H9kovIHoCIVQJJCKE1ENFjnAgcaSGC6rEn" +
|
||||
"xBwA04Tx43t2FnvDAfjkNibxgHxnWb2e/u992bee7tCa00YFsffekFY+nUzFtjW0LrvjRXrCDIAaPLlW0nH" +
|
||||
"L0SsZtVoaF98mLrx3pdhOqLtYPHChahZcYYO7KvPFxvRl5XPp1sN3adWiD1ZAqD6XYK1b/dvE5IWryTt2ud" +
|
||||
"LFedwc1+9kLp+vbbpoDh+6TklxBeAi9TL0taeWpdmZzQDry0AcO+jQ12RyohqqoYoo8RDwJrU+qXkjWtfi8" +
|
||||
"Xxt58BdQuwQs9qC/afLwCw8tnQbqYAPsgxE1S6F3EAIXux2oQFKm0ihMsOF71dHYx+f3NND68ghCu1YIoeP" +
|
||||
"PQN1pGRABkJ6Bus96CutRZMydTl+TvuiRW1m3n0eDl0vRPcEysqdXn+jsQPsrHMquGeXEaY4Yk4wxWcY5V/" +
|
||||
"9scqOMOVUFthatyTy8QyqwZ+kDURKoMWxNKr2EeqVKcTNOajqKoBgOE28U4tdQl5p5bwCw7BWquaZSzAPlw" +
|
||||
"jlithJtp3pTImSqQRrb2Z8PHGigD4RZuNX6JYj6wj7O4TFLbCO/Mn/m8R+h6rYSUb3ekokRY6f/YukArN97" +
|
||||
"9jcW+V/S8g0eT/N3VN3kTqWbQ428m9/8k0P/1aIhF36PccEl6EhOcAUCrXKZXXWS3XKd2vc/TRBG9O5ELC1" +
|
||||
"7MmWubD2nKhUKZa26Ba2+D3P+4/MNCFwg59oWVeYhkzgN/JDR8deKBoD7Y+ljEjGZ0sosXVTvbc6RHirr2r" +
|
||||
"eNy1OXd6pJsQ+gqjk8VWFYmHrwBzW/n+uMPFiRwHB2I7ih8ciHFxIkd/3Omk5tCDV1t+2nNu5sxxpDFNx+h" +
|
||||
"uNhVT3/zMDz8usXC3ddaHBj1GHj/As08fwTS7Kt1HBTmyN29vdwAw+/wbwLVOJ3uAD1wi/dUH7Qei66Pfyu" +
|
||||
"Rj4Ik9is+hglfbkbfR3cnZm7chlUWLdwmprtCohX4HUtlOcQjLYCu+fzGJH2QRKvP3UNz8bWk1qMxjGTOMT" +
|
||||
"hZ3kvgLI5AzFfo379UAAAAASUVORK5CYII="
|
||||
|
||||
private suspend fun testBase(server: Nip96MediaServers.ServerName) {
|
||||
val serverInfo =
|
||||
Nip96Retriever()
|
||||
@ -100,7 +49,15 @@ class ImageUploadTesting {
|
||||
server.baseUrl,
|
||||
)
|
||||
|
||||
val bytes = Base64.getDecoder().decode(imagePng)
|
||||
val bitmap = Bitmap.createBitmap(200, 300, Bitmap.Config.ARGB_8888)
|
||||
for (x in 0 until bitmap.width) {
|
||||
for (y in 0 until bitmap.height) {
|
||||
bitmap.setPixel(x, y, Color.rgb(Random.nextInt(), Random.nextInt(), Random.nextInt()))
|
||||
}
|
||||
}
|
||||
val baos = ByteArrayOutputStream()
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos)
|
||||
val bytes = baos.toByteArray()
|
||||
val inputStream = bytes.inputStream()
|
||||
|
||||
val account = Account(KeyPair())
|
||||
@ -110,7 +67,7 @@ class ImageUploadTesting {
|
||||
.uploadImage(
|
||||
inputStream,
|
||||
bytes.size.toLong(),
|
||||
contentTypePng,
|
||||
"image/png",
|
||||
alt = null,
|
||||
sensitiveContent = null,
|
||||
serverInfo,
|
||||
@ -124,31 +81,31 @@ class ImageUploadTesting {
|
||||
val contentType = result.tags!!.first { it[0] == "m" }.get(1)
|
||||
val ox = result.tags!!.first { it[0] == "ox" }.get(1)
|
||||
|
||||
Assert.assertTrue(url.startsWith("http"))
|
||||
Assert.assertTrue("${server.name}: Invalid result url", url.startsWith("http"))
|
||||
|
||||
val imageData: ByteArray =
|
||||
ImageDownloader().waitAndGetImage(url)
|
||||
?: run {
|
||||
fail("Should not be null")
|
||||
fail("${server.name}: Should not be null")
|
||||
return
|
||||
}
|
||||
|
||||
FileHeader.prepare(
|
||||
imageData,
|
||||
contentTypePng,
|
||||
"image/png",
|
||||
null,
|
||||
onReady = {
|
||||
if (dim != null) {
|
||||
assertEquals(dim, it.dim)
|
||||
// assertEquals("${server.name}: Invalid dimensions", it.dim, dim)
|
||||
}
|
||||
if (size != null) {
|
||||
assertEquals(size, it.size.toString())
|
||||
// assertEquals("${server.name}: Invalid size", it.size.toString(), size)
|
||||
}
|
||||
if (hash != null) {
|
||||
assertEquals(hash, it.hash)
|
||||
assertEquals("${server.name}: Invalid hash", it.hash, hash)
|
||||
}
|
||||
},
|
||||
onError = { fail("It should not fail") },
|
||||
onError = { fail("${server.name}: It should not fail") },
|
||||
)
|
||||
|
||||
// delay(1000)
|
||||
@ -156,6 +113,14 @@ class ImageUploadTesting {
|
||||
// assertTrue(Nip96Uploader(account).delete(ox, contentType, serverInfo))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runTestOnDefaultServers() =
|
||||
runBlocking {
|
||||
Nip96MediaServers.DEFAULT.forEach {
|
||||
testBase(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun testNostrCheck() =
|
||||
runBlocking {
|
||||
@ -163,12 +128,14 @@ class ImageUploadTesting {
|
||||
}
|
||||
|
||||
@Test()
|
||||
@Ignore("Not Working anymore")
|
||||
fun testNostrage() =
|
||||
runBlocking {
|
||||
testBase(Nip96MediaServers.ServerName("nostrage", "https://nostrage.com"))
|
||||
}
|
||||
|
||||
@Test()
|
||||
@Ignore("Not Working anymore")
|
||||
fun testSove() =
|
||||
runBlocking {
|
||||
testBase(Nip96MediaServers.ServerName("sove", "https://sove.rent"))
|
||||
@ -181,6 +148,7 @@ class ImageUploadTesting {
|
||||
}
|
||||
|
||||
@Test()
|
||||
@Ignore("Not Working anymore")
|
||||
fun testSovbit() =
|
||||
runBlocking {
|
||||
testBase(Nip96MediaServers.ServerName("sovbit", "https://files.sovbit.host"))
|
||||
@ -191,4 +159,17 @@ class ImageUploadTesting {
|
||||
runBlocking {
|
||||
testBase(Nip96MediaServers.ServerName("void.cat", "https://void.cat"))
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun testNostrPic() =
|
||||
runBlocking {
|
||||
testBase(Nip96MediaServers.ServerName("nostpic.com", "https://nostpic.com"))
|
||||
}
|
||||
|
||||
@Test()
|
||||
@Ignore("Not Working anymore")
|
||||
fun testNostrOnch() =
|
||||
runBlocking {
|
||||
testBase(Nip96MediaServers.ServerName("nostr.onch.services", "https://nostr.onch.services"))
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
fun TranslatableRichTextViewer(
|
||||
content: String,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
tags: ImmutableListOfLists<String>,
|
||||
backgroundColor: MutableState<Color>,
|
||||
@ -40,6 +41,7 @@ fun TranslatableRichTextViewer(
|
||||
) = ExpandableRichTextViewer(
|
||||
content,
|
||||
canPreview,
|
||||
quotesLeft,
|
||||
modifier,
|
||||
tags,
|
||||
backgroundColor,
|
||||
|
@ -238,8 +238,10 @@ class Account(
|
||||
private val liveHomeList: StateFlow<NoteState?> by lazy {
|
||||
defaultHomeFollowList
|
||||
.transformLatest {
|
||||
LocalCache.checkGetOrCreateAddressableNote(it)?.flow()?.metadata?.stateFlow?.let {
|
||||
emit(it)
|
||||
if (it != GLOBAL_FOLLOWS && it != KIND3_FOLLOWS) {
|
||||
LocalCache.checkGetOrCreateAddressableNote(it)?.flow()?.metadata?.stateFlow?.let {
|
||||
emit(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
.flattenMerge()
|
||||
@ -273,8 +275,10 @@ class Account(
|
||||
private val liveNotificationList: StateFlow<NoteState?> by lazy {
|
||||
defaultNotificationFollowList
|
||||
.transformLatest {
|
||||
LocalCache.checkGetOrCreateAddressableNote(it)?.flow()?.metadata?.stateFlow?.let {
|
||||
emit(it)
|
||||
if (it != GLOBAL_FOLLOWS && it != KIND3_FOLLOWS) {
|
||||
LocalCache.checkGetOrCreateAddressableNote(it)?.flow()?.metadata?.stateFlow?.let {
|
||||
emit(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
.flattenMerge()
|
||||
@ -308,8 +312,10 @@ class Account(
|
||||
private val liveStoriesList: StateFlow<NoteState?> by lazy {
|
||||
defaultStoriesFollowList
|
||||
.transformLatest {
|
||||
LocalCache.checkGetOrCreateAddressableNote(it)?.flow()?.metadata?.stateFlow?.let {
|
||||
emit(it)
|
||||
if (it != GLOBAL_FOLLOWS && it != KIND3_FOLLOWS) {
|
||||
LocalCache.checkGetOrCreateAddressableNote(it)?.flow()?.metadata?.stateFlow?.let {
|
||||
emit(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
.flattenMerge()
|
||||
@ -343,8 +349,10 @@ class Account(
|
||||
private val liveDiscoveryList: StateFlow<NoteState?> by lazy {
|
||||
defaultDiscoveryFollowList
|
||||
.transformLatest {
|
||||
LocalCache.checkGetOrCreateAddressableNote(it)?.flow()?.metadata?.stateFlow?.let {
|
||||
emit(it)
|
||||
if (it != GLOBAL_FOLLOWS && it != KIND3_FOLLOWS) {
|
||||
LocalCache.checkGetOrCreateAddressableNote(it)?.flow()?.metadata?.stateFlow?.let {
|
||||
emit(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
.flattenMerge()
|
||||
|
@ -1645,39 +1645,51 @@ object LocalCache {
|
||||
}
|
||||
|
||||
return notes.filter { _, note ->
|
||||
(
|
||||
note.event !is GenericRepostEvent &&
|
||||
note.event !is RepostEvent &&
|
||||
note.event !is CommunityPostApprovalEvent &&
|
||||
note.event !is ReactionEvent &&
|
||||
note.event !is GiftWrapEvent &&
|
||||
note.event !is SealedGossipEvent &&
|
||||
note.event !is OtsEvent &&
|
||||
note.event !is LnZapEvent &&
|
||||
note.event !is LnZapRequestEvent
|
||||
) &&
|
||||
(
|
||||
note.event?.content()?.contains(text, true)
|
||||
?: false ||
|
||||
note.event?.matchTag1With(text) ?: false ||
|
||||
note.idHex.startsWith(text, true) ||
|
||||
note.idNote().startsWith(text, true)
|
||||
)
|
||||
if (note.event is GenericRepostEvent ||
|
||||
note.event is RepostEvent ||
|
||||
note.event is CommunityPostApprovalEvent ||
|
||||
note.event is ReactionEvent ||
|
||||
note.event is LnZapEvent ||
|
||||
note.event is LnZapRequestEvent
|
||||
) {
|
||||
return@filter false
|
||||
}
|
||||
|
||||
if (note.event?.matchTag1With(text) == true ||
|
||||
note.idHex.startsWith(text, true) ||
|
||||
note.idNote().startsWith(text, true)
|
||||
) {
|
||||
return@filter true
|
||||
}
|
||||
|
||||
if (note.event?.isContentEncoded() == false) {
|
||||
return@filter note.event?.content()?.contains(text, true) ?: false
|
||||
}
|
||||
|
||||
return@filter false
|
||||
} +
|
||||
addressables.filter { _, addressable ->
|
||||
(
|
||||
addressable.event !is GenericRepostEvent &&
|
||||
addressable.event !is RepostEvent &&
|
||||
addressable.event !is CommunityPostApprovalEvent &&
|
||||
addressable.event !is ReactionEvent &&
|
||||
addressable.event !is GiftWrapEvent &&
|
||||
addressable.event !is LnZapEvent &&
|
||||
addressable.event !is LnZapRequestEvent
|
||||
) &&
|
||||
(
|
||||
addressable.event?.content()?.contains(text, true)
|
||||
?: false || addressable.event?.matchTag1With(text) ?: false || addressable.idHex.startsWith(text, true)
|
||||
)
|
||||
if (addressable.event is GenericRepostEvent ||
|
||||
addressable.event is RepostEvent ||
|
||||
addressable.event is CommunityPostApprovalEvent ||
|
||||
addressable.event is ReactionEvent ||
|
||||
addressable.event is LnZapEvent ||
|
||||
addressable.event is LnZapRequestEvent
|
||||
) {
|
||||
return@filter false
|
||||
}
|
||||
|
||||
if (addressable.event?.matchTag1With(text) == true ||
|
||||
addressable.idHex.startsWith(text, true)
|
||||
) {
|
||||
return@filter true
|
||||
}
|
||||
|
||||
if (addressable.event?.isContentEncoded() == false) {
|
||||
return@filter addressable.event?.content()?.contains(text, true) ?: false
|
||||
}
|
||||
|
||||
return@filter false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -317,6 +317,12 @@ open class Note(val idHex: String) {
|
||||
}
|
||||
|
||||
fun removeAllChildNotes(): List<Note> {
|
||||
val repliesChanged = replies.isNotEmpty()
|
||||
val reactionsChanged = reactions.isNotEmpty()
|
||||
val zapsChanged = zaps.isNotEmpty() || zapPayments.isNotEmpty()
|
||||
val boostsChanged = boosts.isNotEmpty()
|
||||
val reportsChanged = reports.isNotEmpty()
|
||||
|
||||
val toBeRemoved =
|
||||
replies +
|
||||
reactions.values.flatten() +
|
||||
@ -337,11 +343,11 @@ open class Note(val idHex: String) {
|
||||
relays = listOf<RelayBriefInfoCache.RelayBriefInfo>()
|
||||
lastReactionsDownloadTime = emptyMap()
|
||||
|
||||
liveSet?.innerReplies?.invalidateData()
|
||||
liveSet?.innerReactions?.invalidateData()
|
||||
liveSet?.innerBoosts?.invalidateData()
|
||||
liveSet?.innerReports?.invalidateData()
|
||||
liveSet?.innerZaps?.invalidateData()
|
||||
if (repliesChanged) liveSet?.innerReplies?.invalidateData()
|
||||
if (reactionsChanged) liveSet?.innerReactions?.invalidateData()
|
||||
if (boostsChanged) liveSet?.innerBoosts?.invalidateData()
|
||||
if (reportsChanged) liveSet?.innerReports?.invalidateData()
|
||||
if (zapsChanged) liveSet?.innerZaps?.invalidateData()
|
||||
|
||||
return toBeRemoved
|
||||
}
|
||||
@ -536,7 +542,7 @@ open class Note(val idHex: String) {
|
||||
option: Int?,
|
||||
user: User,
|
||||
account: Account,
|
||||
remainingZapEvents: List<Pair<Note, Note?>>,
|
||||
remainingZapEvents: Map<Note, Note?>,
|
||||
onWasZappedByAuthor: () -> Unit,
|
||||
) {
|
||||
if (remainingZapEvents.isEmpty()) {
|
||||
@ -544,8 +550,8 @@ open class Note(val idHex: String) {
|
||||
}
|
||||
|
||||
remainingZapEvents.forEach { next ->
|
||||
val zapRequest = next.first.event as LnZapRequestEvent
|
||||
val zapEvent = next.second?.event as? LnZapEvent
|
||||
val zapRequest = next.key.event as LnZapRequestEvent
|
||||
val zapEvent = next.value?.event as? LnZapEvent
|
||||
|
||||
if (!zapRequest.isPrivateZap()) {
|
||||
// public events
|
||||
@ -589,7 +595,7 @@ open class Note(val idHex: String) {
|
||||
account: Account,
|
||||
onWasZappedByAuthor: () -> Unit,
|
||||
) {
|
||||
isZappedByCalculation(null, user, account, zaps.toList(), onWasZappedByAuthor)
|
||||
isZappedByCalculation(null, user, account, zaps, onWasZappedByAuthor)
|
||||
if (account.userProfile() == user) {
|
||||
recursiveIsPaidByCalculation(account, zapPayments.toList(), onWasZappedByAuthor)
|
||||
}
|
||||
@ -601,7 +607,7 @@ open class Note(val idHex: String) {
|
||||
account: Account,
|
||||
onWasZappedByAuthor: () -> Unit,
|
||||
) {
|
||||
isZappedByCalculation(option, user, account, zaps.toList(), onWasZappedByAuthor)
|
||||
isZappedByCalculation(option, user, account, zaps, onWasZappedByAuthor)
|
||||
}
|
||||
|
||||
fun getReactionBy(user: User): String? {
|
||||
|
@ -34,7 +34,7 @@ data class Settings(
|
||||
val automaticallyShowProfilePictures: ConnectivityType = ConnectivityType.ALWAYS,
|
||||
val dontShowPushNotificationSelector: Boolean = false,
|
||||
val dontAskForNotificationPermissions: Boolean = false,
|
||||
val featureSet: FeatureSetType = FeatureSetType.SIMPLIFIED,
|
||||
val featureSet: FeatureSetType = FeatureSetType.COMPLETE,
|
||||
)
|
||||
|
||||
enum class ThemeType(val screenCode: Int, val resourceId: Int) {
|
||||
@ -92,7 +92,7 @@ fun parseFeatureSetType(screenCode: Int): FeatureSetType {
|
||||
FeatureSetType.COMPLETE.screenCode -> FeatureSetType.COMPLETE
|
||||
FeatureSetType.SIMPLIFIED.screenCode -> FeatureSetType.SIMPLIFIED
|
||||
else -> {
|
||||
FeatureSetType.SIMPLIFIED
|
||||
FeatureSetType.COMPLETE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,8 +32,7 @@ object Nip96MediaServers {
|
||||
listOf(
|
||||
ServerName("Nostr.Build", "https://nostr.build"),
|
||||
ServerName("NostrCheck.me", "https://nostrcheck.me"),
|
||||
ServerName("Nostrage", "https://nostrage.com"),
|
||||
ServerName("Sove", "https://sove.rent"),
|
||||
ServerName("NostPic", "https://nostpic.com"),
|
||||
ServerName("Sovbit", "https://files.sovbit.host"),
|
||||
ServerName("Void.cat", "https://void.cat"),
|
||||
)
|
||||
|
@ -268,6 +268,7 @@ fun EditPostView(
|
||||
makeItShort = true,
|
||||
unPackReply = false,
|
||||
isQuotedNote = true,
|
||||
quotesLeft = 1,
|
||||
modifier = MaterialTheme.colorScheme.replyModifier,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
@ -312,11 +313,12 @@ fun EditPostView(
|
||||
val backgroundColor = remember { mutableStateOf(bgColor) }
|
||||
|
||||
BechLink(
|
||||
myUrlPreview,
|
||||
true,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
word = myUrlPreview,
|
||||
canPreview = true,
|
||||
quotesLeft = 1,
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
} else if (RichTextParser.isUrlWithoutScheme(myUrlPreview)) {
|
||||
LoadUrlPreview("https://$myUrlPreview", myUrlPreview, accountViewModel)
|
||||
|
@ -350,6 +350,7 @@ fun NewPostView(
|
||||
makeItShort = true,
|
||||
unPackReply = false,
|
||||
isQuotedNote = true,
|
||||
quotesLeft = 1,
|
||||
modifier = MaterialTheme.colorScheme.replyModifier,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
@ -427,11 +428,12 @@ fun NewPostView(
|
||||
val backgroundColor = remember { mutableStateOf(bgColor) }
|
||||
|
||||
BechLink(
|
||||
myUrlPreview,
|
||||
true,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
word = myUrlPreview,
|
||||
canPreview = true,
|
||||
quotesLeft = 1,
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
} else if (RichTextParser.isUrlWithoutScheme(myUrlPreview)) {
|
||||
LoadUrlPreview("https://$myUrlPreview", myUrlPreview, accountViewModel)
|
||||
|
@ -65,6 +65,7 @@ fun NotifyRequestDialog(
|
||||
TranslatableRichTextViewer(
|
||||
textContent,
|
||||
canPreview = true,
|
||||
quotesLeft = 1,
|
||||
Modifier.fillMaxWidth(),
|
||||
EmptyTagList,
|
||||
background,
|
||||
|
@ -26,13 +26,18 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@Composable
|
||||
fun ClickableNoteTag(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val route = routeFor(baseNote, accountViewModel.userProfile())
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString("@${baseNote.idNote().toShortenHex()}"),
|
||||
onClick = { nav("Note/${baseNote.idHex}") },
|
||||
|
@ -60,6 +60,7 @@ object ShowFullTextCache {
|
||||
fun ExpandableRichTextViewer(
|
||||
content: String,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
modifier: Modifier,
|
||||
tags: ImmutableListOfLists<String>,
|
||||
backgroundColor: MutableState<Color>,
|
||||
@ -94,6 +95,7 @@ fun ExpandableRichTextViewer(
|
||||
RichTextViewer(
|
||||
text,
|
||||
canPreview,
|
||||
quotesLeft,
|
||||
modifier.align(Alignment.TopStart),
|
||||
tags,
|
||||
backgroundColor,
|
||||
|
@ -69,6 +69,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.halilibo.richtext.markdown.Markdown
|
||||
import com.halilibo.richtext.markdown.MarkdownParseOptions
|
||||
import com.halilibo.richtext.ui.material3.Material3RichText
|
||||
import com.vitorpamplona.amethyst.commons.compose.produceCachedState
|
||||
import com.vitorpamplona.amethyst.commons.richtext.BechSegment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.CashuSegment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.EmailSegment
|
||||
@ -98,7 +99,6 @@ import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadedBechLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font17SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.MarkdownTextStyle
|
||||
@ -129,6 +129,7 @@ fun isMarkdown(content: String): Boolean {
|
||||
fun RichTextViewer(
|
||||
content: String,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
modifier: Modifier,
|
||||
tags: ImmutableListOfLists<String>,
|
||||
backgroundColor: MutableState<Color>,
|
||||
@ -139,7 +140,7 @@ fun RichTextViewer(
|
||||
if (remember(content) { isMarkdown(content) }) {
|
||||
RenderContentAsMarkdown(content, tags, accountViewModel, nav)
|
||||
} else {
|
||||
RenderRegular(content, tags, canPreview, backgroundColor, accountViewModel, nav)
|
||||
RenderRegular(content, tags, canPreview, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -277,6 +278,7 @@ private fun RenderRegular(
|
||||
content: String,
|
||||
tags: ImmutableListOfLists<String>,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
@ -287,6 +289,7 @@ private fun RenderRegular(
|
||||
word,
|
||||
state,
|
||||
backgroundColor,
|
||||
quotesLeft,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
@ -394,10 +397,10 @@ private fun RenderWordWithoutPreview(
|
||||
is CashuSegment -> Text(word.segmentText)
|
||||
is EmailSegment -> ClickableEmail(word.segmentText)
|
||||
is PhoneSegment -> ClickablePhone(word.segmentText)
|
||||
is BechSegment -> BechLink(word.segmentText, false, backgroundColor, accountViewModel, nav)
|
||||
is BechSegment -> BechLink(word.segmentText, false, 0, backgroundColor, accountViewModel, nav)
|
||||
is HashTagSegment -> HashTag(word, nav)
|
||||
is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
|
||||
is HashIndexEventSegment -> TagLink(word, false, backgroundColor, accountViewModel, nav)
|
||||
is HashIndexEventSegment -> TagLink(word, false, 0, backgroundColor, accountViewModel, nav)
|
||||
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
|
||||
is RegularTextSegment -> Text(word.segmentText)
|
||||
}
|
||||
@ -408,6 +411,7 @@ private fun RenderWordWithPreview(
|
||||
word: Segment,
|
||||
state: RichTextViewerState,
|
||||
backgroundColor: MutableState<Color>,
|
||||
quotesLeft: Int,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
@ -420,10 +424,10 @@ private fun RenderWordWithPreview(
|
||||
is CashuSegment -> CashuPreview(word.segmentText, accountViewModel)
|
||||
is EmailSegment -> ClickableEmail(word.segmentText)
|
||||
is PhoneSegment -> ClickablePhone(word.segmentText)
|
||||
is BechSegment -> BechLink(word.segmentText, true, backgroundColor, accountViewModel, nav)
|
||||
is BechSegment -> BechLink(word.segmentText, true, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||
is HashTagSegment -> HashTag(word, nav)
|
||||
is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
|
||||
is HashIndexEventSegment -> TagLink(word, true, backgroundColor, accountViewModel, nav)
|
||||
is HashIndexEventSegment -> TagLink(word, true, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
|
||||
is RegularTextSegment -> Text(word.segmentText)
|
||||
}
|
||||
@ -558,15 +562,15 @@ fun ObserveNIP19(
|
||||
accountViewModel: AccountViewModel,
|
||||
onRefresh: () -> Unit,
|
||||
) {
|
||||
when (val parsed = entity) {
|
||||
is Nip19Bech32.NPub -> ObserveNIP19User(parsed.hex, accountViewModel, onRefresh)
|
||||
is Nip19Bech32.NProfile -> ObserveNIP19User(parsed.hex, accountViewModel, onRefresh)
|
||||
when (entity) {
|
||||
is Nip19Bech32.NPub -> ObserveNIP19User(entity.hex, accountViewModel, onRefresh)
|
||||
is Nip19Bech32.NProfile -> ObserveNIP19User(entity.hex, accountViewModel, onRefresh)
|
||||
|
||||
is Nip19Bech32.Note -> ObserveNIP19Event(parsed.hex, accountViewModel, onRefresh)
|
||||
is Nip19Bech32.NEvent -> ObserveNIP19Event(parsed.hex, accountViewModel, onRefresh)
|
||||
is Nip19Bech32.NEmbed -> ObserveNIP19Event(parsed.event.id, accountViewModel, onRefresh)
|
||||
is Nip19Bech32.Note -> ObserveNIP19Event(entity.hex, accountViewModel, onRefresh)
|
||||
is Nip19Bech32.NEvent -> ObserveNIP19Event(entity.hex, accountViewModel, onRefresh)
|
||||
is Nip19Bech32.NEmbed -> ObserveNIP19Event(entity.event.id, accountViewModel, onRefresh)
|
||||
|
||||
is Nip19Bech32.NAddress -> ObserveNIP19Event(parsed.atag, accountViewModel, onRefresh)
|
||||
is Nip19Bech32.NAddress -> ObserveNIP19Event(entity.atag, accountViewModel, onRefresh)
|
||||
|
||||
is Nip19Bech32.NSec -> {}
|
||||
is Nip19Bech32.NRelay -> {}
|
||||
@ -643,26 +647,24 @@ private fun ObserveUser(
|
||||
fun BechLink(
|
||||
word: String,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
var loadedLink by remember { mutableStateOf<LoadedBechLink?>(null) }
|
||||
val loadedLink by produceCachedState(cache = accountViewModel.bechLinkCache, key = word)
|
||||
|
||||
if (loadedLink == null) {
|
||||
LaunchedEffect(key1 = word) {
|
||||
accountViewModel.parseNIP19(word) { loadedLink = it }
|
||||
}
|
||||
}
|
||||
val baseNote = loadedLink?.baseNote
|
||||
|
||||
if (canPreview && loadedLink?.baseNote != null) {
|
||||
if (canPreview && quotesLeft > 0 && baseNote != null) {
|
||||
Row {
|
||||
DisplayFullNote(
|
||||
loadedLink?.baseNote!!,
|
||||
accountViewModel,
|
||||
backgroundColor,
|
||||
nav,
|
||||
loadedLink?.nip19?.additionalChars?.ifBlank { null },
|
||||
note = baseNote,
|
||||
extraChars = loadedLink?.nip19?.additionalChars?.ifBlank { null },
|
||||
quotesLeft = quotesLeft,
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
} else if (loadedLink?.nip19 != null) {
|
||||
@ -683,17 +685,19 @@ fun BechLink(
|
||||
|
||||
@Composable
|
||||
private fun DisplayFullNote(
|
||||
it: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
backgroundColor: MutableState<Color>,
|
||||
nav: (String) -> Unit,
|
||||
note: Note,
|
||||
extraChars: String?,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
NoteCompose(
|
||||
baseNote = it,
|
||||
baseNote = note,
|
||||
accountViewModel = accountViewModel,
|
||||
modifier = MaterialTheme.colorScheme.innerPostModifier,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
quotesLeft = quotesLeft - 1,
|
||||
isQuotedNote = true,
|
||||
nav = nav,
|
||||
)
|
||||
@ -806,6 +810,7 @@ fun LoadNote(
|
||||
fun TagLink(
|
||||
word: HashIndexEventSegment,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
@ -819,6 +824,7 @@ fun TagLink(
|
||||
it,
|
||||
word.extras,
|
||||
canPreview,
|
||||
quotesLeft,
|
||||
accountViewModel,
|
||||
backgroundColor,
|
||||
nav,
|
||||
@ -833,21 +839,23 @@ private fun DisplayNoteFromTag(
|
||||
baseNote: Note,
|
||||
addedChars: String?,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
accountViewModel: AccountViewModel,
|
||||
backgroundColor: MutableState<Color>,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
if (canPreview) {
|
||||
if (canPreview && quotesLeft > 0) {
|
||||
NoteCompose(
|
||||
baseNote = baseNote,
|
||||
accountViewModel = accountViewModel,
|
||||
modifier = MaterialTheme.colorScheme.innerPostModifier,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
isQuotedNote = true,
|
||||
quotesLeft = quotesLeft - 1,
|
||||
nav = nav,
|
||||
)
|
||||
} else {
|
||||
ClickableNoteTag(baseNote, nav)
|
||||
ClickableNoteTag(baseNote, accountViewModel, nav)
|
||||
}
|
||||
|
||||
addedChars?.ifBlank { null }?.let { Text(text = it) }
|
||||
|
@ -669,43 +669,6 @@ private fun RenderVideoPlayer(
|
||||
}
|
||||
}
|
||||
|
||||
val factory =
|
||||
remember(controller) {
|
||||
{ context: Context ->
|
||||
PlayerView(context).apply {
|
||||
player = controller
|
||||
layoutParams =
|
||||
FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
)
|
||||
setBackgroundColor(Color.Transparent.toArgb())
|
||||
setShutterBackgroundColor(Color.Transparent.toArgb())
|
||||
controllerAutoShow = false
|
||||
thumbData?.thumb?.let { defaultArtwork = it }
|
||||
hideController()
|
||||
resizeMode =
|
||||
if (maxHeight.isFinite) {
|
||||
AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||
} else {
|
||||
AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
|
||||
}
|
||||
onDialog?.let { innerOnDialog ->
|
||||
setFullscreenButtonClickListener {
|
||||
controller.pause()
|
||||
innerOnDialog(it)
|
||||
}
|
||||
}
|
||||
setControllerVisibilityListener(
|
||||
PlayerView.ControllerVisibilityListener { visible ->
|
||||
controllerVisible.value = visible == View.VISIBLE
|
||||
onControllerVisibilityChanged?.let { callback -> callback(visible == View.VISIBLE) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val ratio = remember { aspectRatio(dimensions) }
|
||||
|
||||
if (ratio != null) {
|
||||
@ -719,7 +682,39 @@ private fun RenderVideoPlayer(
|
||||
|
||||
AndroidView(
|
||||
modifier = myModifier,
|
||||
factory = factory,
|
||||
factory = { context: Context ->
|
||||
PlayerView(context).apply {
|
||||
player = controller
|
||||
layoutParams =
|
||||
FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
)
|
||||
setBackgroundColor(Color.Transparent.toArgb())
|
||||
setShutterBackgroundColor(Color.Transparent.toArgb())
|
||||
controllerAutoShow = false
|
||||
thumbData?.thumb?.let { defaultArtwork = it }
|
||||
hideController()
|
||||
resizeMode =
|
||||
if (maxHeight.isFinite) {
|
||||
AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||
} else {
|
||||
AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
|
||||
}
|
||||
onDialog?.let { innerOnDialog ->
|
||||
setFullscreenButtonClickListener {
|
||||
controller.pause()
|
||||
innerOnDialog(it)
|
||||
}
|
||||
}
|
||||
setControllerVisibilityListener(
|
||||
PlayerView.ControllerVisibilityListener { visible ->
|
||||
controllerVisible.value = visible == View.VISIBLE
|
||||
onControllerVisibilityChanged?.let { callback -> callback(visible == View.VISIBLE) }
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
waveform?.let { Waveform(it, controller, remember { Modifier.align(Alignment.Center) }) }
|
||||
|
@ -32,13 +32,11 @@ import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.MilitaryTech
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@ -46,8 +44,6 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@ -55,11 +51,10 @@ import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.NoteDropDownMenu
|
||||
import com.vitorpamplona.amethyst.ui.note.types.BadgeDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.BadgeCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@ -86,24 +81,12 @@ fun BadgeCompose(
|
||||
if (note == null) {
|
||||
BlankNote(Modifier, !isInnerNote)
|
||||
} else {
|
||||
val defaultBackgroundColor = MaterialTheme.colorScheme.background
|
||||
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) }
|
||||
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
|
||||
|
||||
LaunchedEffect(key1 = likeSetCard) {
|
||||
accountViewModel.loadAndMarkAsRead(routeForLastRead, likeSetCard.createdAt()) { isNew ->
|
||||
val newBackgroundColor =
|
||||
if (isNew) {
|
||||
newItemColor.compositeOver(defaultBackgroundColor)
|
||||
} else {
|
||||
defaultBackgroundColor
|
||||
}
|
||||
|
||||
if (backgroundColor.value != newBackgroundColor) {
|
||||
backgroundColor.value = newBackgroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
val backgroundColor =
|
||||
calculateBackgroundColor(
|
||||
createdAt = likeSetCard.createdAt(),
|
||||
routeForLastRead = routeForLastRead,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
@ -173,21 +156,8 @@ fun BadgeCompose(
|
||||
}
|
||||
|
||||
note.replyTo?.firstOrNull()?.let {
|
||||
NoteCompose(
|
||||
baseNote = it,
|
||||
routeForLastRead = null,
|
||||
isBoostedNote = true,
|
||||
showHidden = showHidden,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
BadgeDisplay(baseNote = it)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(top = 10.dp),
|
||||
thickness = DividerThickness,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -121,13 +121,12 @@ fun HiddenNote(
|
||||
isHiddenAuthor: Boolean,
|
||||
accountViewModel: AccountViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
isQuote: Boolean = false,
|
||||
nav: (String) -> Unit,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Row(
|
||||
modifier = Modifier.padding(start = if (!isQuote) 30.dp else 25.dp, end = 20.dp),
|
||||
modifier = Modifier.padding(horizontal = 20.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
|
@ -56,7 +56,6 @@ import androidx.compose.ui.Alignment.Companion.TopEnd
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@ -91,7 +90,6 @@ import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||
@ -258,7 +256,6 @@ fun RenderChannelCardReportState(
|
||||
state.isHiddenAuthor,
|
||||
accountViewModel,
|
||||
modifier,
|
||||
false,
|
||||
nav,
|
||||
onClick = { showReportedNote = true },
|
||||
)
|
||||
@ -307,41 +304,13 @@ private fun CheckNewAndRenderChannelCard(
|
||||
showPopup: () -> Unit,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
|
||||
val defaultBackgroundColor = MaterialTheme.colorScheme.background
|
||||
val backgroundColor =
|
||||
remember {
|
||||
mutableStateOf<Color>(
|
||||
parentBackgroundColor?.value ?: defaultBackgroundColor,
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = routeForLastRead, key2 = parentBackgroundColor?.value) {
|
||||
routeForLastRead?.let {
|
||||
accountViewModel.loadAndMarkAsRead(routeForLastRead, baseNote.createdAt()) { isNew ->
|
||||
val newBackgroundColor =
|
||||
if (isNew) {
|
||||
if (parentBackgroundColor != null) {
|
||||
newItemColor.compositeOver(parentBackgroundColor.value)
|
||||
} else {
|
||||
newItemColor.compositeOver(defaultBackgroundColor)
|
||||
}
|
||||
} else {
|
||||
parentBackgroundColor?.value ?: defaultBackgroundColor
|
||||
}
|
||||
|
||||
if (newBackgroundColor != backgroundColor.value) {
|
||||
launch(Dispatchers.Main) { backgroundColor.value = newBackgroundColor }
|
||||
}
|
||||
}
|
||||
}
|
||||
?: run {
|
||||
val newBackgroundColor = parentBackgroundColor?.value ?: defaultBackgroundColor
|
||||
if (newBackgroundColor != backgroundColor.value) {
|
||||
launch(Dispatchers.Main) { backgroundColor.value = newBackgroundColor }
|
||||
}
|
||||
}
|
||||
}
|
||||
calculateBackgroundColor(
|
||||
createdAt = baseNote.createdAt(),
|
||||
routeForLastRead = routeForLastRead,
|
||||
parentBackgroundColor = parentBackgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
|
||||
ClickableNote(
|
||||
baseNote = baseNote,
|
||||
|
@ -77,8 +77,6 @@ import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||
import com.vitorpamplona.quartz.events.ChatroomKeyable
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun ChatroomHeaderCompose(
|
||||
@ -408,11 +406,8 @@ private fun WatchNotificationChanges(
|
||||
onNewStatus: (Boolean) -> Unit,
|
||||
) {
|
||||
LaunchedEffect(key1 = note, accountViewModel.accountMarkAsReadUpdates.intValue) {
|
||||
launch(Dispatchers.IO) {
|
||||
note.event?.createdAt()?.let {
|
||||
val lastTime = accountViewModel.account.loadLastRead(route)
|
||||
onNewStatus(it > lastTime)
|
||||
}
|
||||
note.event?.createdAt()?.let {
|
||||
onNewStatus(it > accountViewModel.account.loadLastRead(route))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -204,7 +204,6 @@ fun LoadedChatMessageCompose(
|
||||
state.isHiddenAuthor,
|
||||
accountViewModel,
|
||||
Modifier,
|
||||
innerQuote,
|
||||
nav,
|
||||
onClick = { showReportedNote = true },
|
||||
)
|
||||
@ -290,7 +289,7 @@ fun NormalChatNote(
|
||||
|
||||
if (routeForLastRead != null) {
|
||||
LaunchedEffect(key1 = routeForLastRead) {
|
||||
accountViewModel.loadAndMarkAsRead(routeForLastRead, note.createdAt()) {}
|
||||
accountViewModel.loadAndMarkAsRead(routeForLastRead, note.createdAt())
|
||||
}
|
||||
}
|
||||
|
||||
@ -445,6 +444,7 @@ private fun MessageBubbleLines(
|
||||
NoteRow(
|
||||
note = baseNote,
|
||||
canPreview = canPreview,
|
||||
innerQuote = innerQuote,
|
||||
backgroundBubbleColor = backgroundBubbleColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
@ -531,6 +531,7 @@ private fun RenderReply(
|
||||
private fun NoteRow(
|
||||
note: Note,
|
||||
canPreview: Boolean,
|
||||
innerQuote: Boolean,
|
||||
backgroundBubbleColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
@ -547,6 +548,7 @@ private fun NoteRow(
|
||||
RenderRegularTextNote(
|
||||
note,
|
||||
canPreview,
|
||||
innerQuote,
|
||||
backgroundBubbleColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
@ -643,6 +645,7 @@ fun ChatTimeAgo(baseNote: Note) {
|
||||
private fun RenderRegularTextNote(
|
||||
note: Note,
|
||||
canPreview: Boolean,
|
||||
innerQuote: Boolean,
|
||||
backgroundBubbleColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
@ -658,6 +661,7 @@ private fun RenderRegularTextNote(
|
||||
TranslatableRichTextViewer(
|
||||
content = eventContent,
|
||||
canPreview = canPreview,
|
||||
quotesLeft = if (innerQuote) 0 else 1,
|
||||
modifier = HalfTopPadding,
|
||||
tags = tags,
|
||||
backgroundColor = backgroundBubbleColor,
|
||||
@ -670,6 +674,7 @@ private fun RenderRegularTextNote(
|
||||
TranslatableRichTextViewer(
|
||||
content = stringResource(id = R.string.could_not_decrypt_the_message),
|
||||
canPreview = true,
|
||||
quotesLeft = 0,
|
||||
modifier = HalfTopPadding,
|
||||
tags = EmptyTagList,
|
||||
backgroundColor = backgroundBubbleColor,
|
||||
|
@ -30,24 +30,17 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.NoteDropDownMenu
|
||||
import com.vitorpamplona.amethyst.ui.screen.MessageSetCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@ -66,24 +59,12 @@ fun MessageSetCompose(
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val defaultBackgroundColor = MaterialTheme.colorScheme.background
|
||||
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) }
|
||||
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
|
||||
|
||||
LaunchedEffect(key1 = messageSetCard) {
|
||||
accountViewModel.loadAndMarkAsRead(routeForLastRead, messageSetCard.createdAt()) { isNew ->
|
||||
val newBackgroundColor =
|
||||
if (isNew) {
|
||||
newItemColor.compositeOver(defaultBackgroundColor)
|
||||
} else {
|
||||
defaultBackgroundColor
|
||||
}
|
||||
|
||||
if (backgroundColor.value != newBackgroundColor) {
|
||||
backgroundColor.value = newBackgroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
val backgroundColor =
|
||||
calculateBackgroundColor(
|
||||
createdAt = messageSetCard.createdAt(),
|
||||
routeForLastRead = routeForLastRead,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
|
||||
val columnModifier =
|
||||
remember(backgroundColor.value) {
|
||||
@ -123,8 +104,8 @@ fun MessageSetCompose(
|
||||
baseNote = baseNote,
|
||||
routeForLastRead = null,
|
||||
isBoostedNote = true,
|
||||
addMarginTop = false,
|
||||
showHidden = showHidden,
|
||||
quotesLeft = 1,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
@ -133,9 +114,5 @@ fun MessageSetCompose(
|
||||
NoteDropDownMenu(baseNote, popupExpanded, null, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
thickness = DividerThickness,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,6 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@ -54,7 +53,6 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
@ -74,7 +72,6 @@ import com.vitorpamplona.amethyst.ui.note.elements.NoteDropDownMenu
|
||||
import com.vitorpamplona.amethyst.ui.screen.CombinedZap
|
||||
import com.vitorpamplona.amethyst.ui.screen.MultiSetCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfTopPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.NotificationIconModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.NotificationIconModifierSmaller
|
||||
@ -88,14 +85,12 @@ import com.vitorpamplona.amethyst.ui.theme.StdStartPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.WidthAuthorPictureModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.WidthAuthorPictureModifierWithPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.bitcoinColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.overPictureBackground
|
||||
import com.vitorpamplona.amethyst.ui.theme.profile35dpModifier
|
||||
import com.vitorpamplona.quartz.encoders.Nip30CustomEmoji
|
||||
import com.vitorpamplona.quartz.events.EmptyTagList
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.ExperimentalTime
|
||||
|
||||
@ -115,24 +110,12 @@ fun MultiSetCompose(
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val defaultBackgroundColor = MaterialTheme.colorScheme.background
|
||||
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) }
|
||||
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
|
||||
|
||||
LaunchedEffect(key1 = multiSetCard) {
|
||||
accountViewModel.loadAndMarkAsRead(routeForLastRead, multiSetCard.maxCreatedAt) { isNew ->
|
||||
val newBackgroundColor =
|
||||
if (isNew) {
|
||||
newItemColor.compositeOver(defaultBackgroundColor)
|
||||
} else {
|
||||
defaultBackgroundColor
|
||||
}
|
||||
|
||||
if (backgroundColor.value != newBackgroundColor) {
|
||||
launch(Dispatchers.Main) { backgroundColor.value = newBackgroundColor }
|
||||
}
|
||||
}
|
||||
}
|
||||
val backgroundColor =
|
||||
calculateBackgroundColor(
|
||||
createdAt = multiSetCard.maxCreatedAt,
|
||||
routeForLastRead = routeForLastRead,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
|
||||
val columnModifier =
|
||||
remember(backgroundColor.value) {
|
||||
@ -163,6 +146,7 @@ fun MultiSetCompose(
|
||||
modifier = HalfTopPadding,
|
||||
isBoostedNote = true,
|
||||
showHidden = showHidden,
|
||||
quotesLeft = 1,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
@ -170,10 +154,6 @@ fun MultiSetCompose(
|
||||
|
||||
NoteDropDownMenu(baseNote, popupExpanded, null, accountViewModel, nav)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
thickness = DividerThickness,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -471,6 +451,7 @@ fun CrossfadeToDisplayComment(
|
||||
TranslatableRichTextViewer(
|
||||
content = comment,
|
||||
canPreview = true,
|
||||
quotesLeft = 1,
|
||||
tags = EmptyTagList,
|
||||
modifier = textBoxModifier,
|
||||
backgroundColor = backgroundColor,
|
||||
|
@ -31,7 +31,6 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@ -58,7 +57,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
@ -80,8 +78,6 @@ import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.Reward
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.ShowForkInformation
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo
|
||||
import com.vitorpamplona.amethyst.ui.note.types.BadgeDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.types.CommunityHeader
|
||||
import com.vitorpamplona.amethyst.ui.note.types.DisplayPeopleList
|
||||
import com.vitorpamplona.amethyst.ui.note.types.DisplayRelaySet
|
||||
import com.vitorpamplona.amethyst.ui.note.types.EditState
|
||||
@ -112,7 +108,6 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderWikiContent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font12SP
|
||||
@ -134,7 +129,6 @@ import com.vitorpamplona.amethyst.ui.theme.boostedNoteModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.channelNotePictureModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.normalNoteModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.normalWithTopMarginNoteModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyBackground
|
||||
@ -143,7 +137,6 @@ import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
||||
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.events.BadgeAwardEvent
|
||||
import com.vitorpamplona.quartz.events.BadgeDefinitionEvent
|
||||
import com.vitorpamplona.quartz.events.BaseTextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
@ -178,10 +171,8 @@ import com.vitorpamplona.quartz.events.VideoVerticalEvent
|
||||
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun NoteCompose(
|
||||
baseNote: Note,
|
||||
@ -191,41 +182,74 @@ fun NoteCompose(
|
||||
isQuotedNote: Boolean = false,
|
||||
unPackReply: Boolean = true,
|
||||
makeItShort: Boolean = false,
|
||||
addMarginTop: Boolean = true,
|
||||
showHidden: Boolean = false,
|
||||
quotesLeft: Int,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val hasEvent by baseNote.live().hasEvent.observeAsState(baseNote.event != null)
|
||||
|
||||
Crossfade(targetState = hasEvent, label = "Event presence") {
|
||||
if (it) {
|
||||
CheckHiddenNoteCompose(
|
||||
note = baseNote,
|
||||
WatchNoteEvent(
|
||||
baseNote = baseNote,
|
||||
accountViewModel = accountViewModel,
|
||||
showDivider = !isBoostedNote && !isQuotedNote,
|
||||
modifier,
|
||||
) {
|
||||
CheckHiddenNoteCompose(
|
||||
note = baseNote,
|
||||
modifier = modifier,
|
||||
showHidden = showHidden,
|
||||
showHiddenWarning = isQuotedNote || isBoostedNote,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
) { canPreview ->
|
||||
AcceptableNote(
|
||||
baseNote = baseNote,
|
||||
routeForLastRead = routeForLastRead,
|
||||
modifier = modifier,
|
||||
isBoostedNote = isBoostedNote,
|
||||
isQuotedNote = isQuotedNote,
|
||||
unPackReply = unPackReply,
|
||||
makeItShort = makeItShort,
|
||||
addMarginTop = addMarginTop,
|
||||
showHidden = showHidden,
|
||||
canPreview = canPreview,
|
||||
quotesLeft = quotesLeft,
|
||||
parentBackgroundColor = parentBackgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
} else {
|
||||
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
|
||||
BlankNote(
|
||||
remember {
|
||||
modifier.combinedClickable(
|
||||
onClick = {},
|
||||
onLongClick = showPopup,
|
||||
)
|
||||
},
|
||||
!isBoostedNote && !isQuotedNote,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun WatchNoteEvent(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
showDivider: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onNoteEventFound: @Composable () -> Unit,
|
||||
) {
|
||||
if (baseNote.event != null) {
|
||||
onNoteEventFound()
|
||||
} else {
|
||||
// avoid observing costs if already has an event.
|
||||
|
||||
val hasEvent by baseNote.live().hasEvent.observeAsState(baseNote.event != null)
|
||||
Crossfade(targetState = hasEvent, label = "Event presence") {
|
||||
if (it) {
|
||||
onNoteEventFound()
|
||||
} else {
|
||||
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
|
||||
BlankNote(
|
||||
remember {
|
||||
modifier.combinedClickable(
|
||||
onClick = {},
|
||||
onLongClick = showPopup,
|
||||
)
|
||||
},
|
||||
showDivider,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -234,93 +258,68 @@ fun NoteCompose(
|
||||
@Composable
|
||||
fun CheckHiddenNoteCompose(
|
||||
note: Note,
|
||||
routeForLastRead: String? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
isBoostedNote: Boolean = false,
|
||||
isQuotedNote: Boolean = false,
|
||||
unPackReply: Boolean = true,
|
||||
makeItShort: Boolean = false,
|
||||
addMarginTop: Boolean = true,
|
||||
showHiddenWarning: Boolean,
|
||||
showHidden: Boolean = false,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
normalNote: @Composable (canPreview: Boolean) -> Unit,
|
||||
) {
|
||||
if (showHidden) {
|
||||
// Ignores reports as well
|
||||
val state by
|
||||
remember(note) {
|
||||
mutableStateOf(
|
||||
AccountViewModel.NoteComposeReportState(),
|
||||
)
|
||||
}
|
||||
|
||||
RenderReportState(
|
||||
state = state,
|
||||
note = note,
|
||||
routeForLastRead = routeForLastRead,
|
||||
modifier = modifier,
|
||||
isBoostedNote = isBoostedNote,
|
||||
isQuotedNote = isQuotedNote,
|
||||
unPackReply = unPackReply,
|
||||
makeItShort = makeItShort,
|
||||
addMarginTop = addMarginTop,
|
||||
parentBackgroundColor = parentBackgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
normalNote(true)
|
||||
} else {
|
||||
val isHidden by
|
||||
remember(note) {
|
||||
accountViewModel.account.liveHiddenUsers
|
||||
.map { note.isHiddenFor(it) }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
.observeAsState(accountViewModel.isNoteHidden(note))
|
||||
|
||||
val showAnyway =
|
||||
remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
Crossfade(targetState = isHidden, label = "CheckHiddenNoteCompose") {
|
||||
if (!it || showAnyway.value) {
|
||||
LoadedNoteCompose(
|
||||
note = note,
|
||||
routeForLastRead = routeForLastRead,
|
||||
modifier = modifier,
|
||||
isBoostedNote = isBoostedNote,
|
||||
isQuotedNote = isQuotedNote,
|
||||
unPackReply = unPackReply,
|
||||
makeItShort = makeItShort,
|
||||
addMarginTop = addMarginTop,
|
||||
parentBackgroundColor = parentBackgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
} else if (isQuotedNote || isBoostedNote) {
|
||||
HiddenNoteByMe(
|
||||
isQuote = true,
|
||||
onClick = { showAnyway.value = true },
|
||||
)
|
||||
}
|
||||
WatchIsHidden(note, showHiddenWarning, modifier, accountViewModel, nav) { canPreview ->
|
||||
normalNote(canPreview)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadedNoteCompose(
|
||||
fun WatchIsHidden(
|
||||
note: Note,
|
||||
routeForLastRead: String? = null,
|
||||
showHiddenWarning: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
isBoostedNote: Boolean = false,
|
||||
isQuotedNote: Boolean = false,
|
||||
unPackReply: Boolean = true,
|
||||
makeItShort: Boolean = false,
|
||||
addMarginTop: Boolean = true,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
normalNote: @Composable (canPreview: Boolean) -> Unit,
|
||||
) {
|
||||
val isHiddenState by remember(note) {
|
||||
accountViewModel.account.liveHiddenUsers
|
||||
.map { note.isHiddenFor(it) }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
.observeAsState(accountViewModel.isNoteHidden(note))
|
||||
|
||||
val showAnyway =
|
||||
remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
Crossfade(targetState = isHiddenState, label = "CheckHiddenNoteCompose") { isHidden ->
|
||||
if (showAnyway.value) {
|
||||
normalNote(true)
|
||||
} else if (!isHidden) {
|
||||
LoadReportsNoteCompose(note, modifier, accountViewModel, nav) { canPreview ->
|
||||
normalNote(canPreview)
|
||||
}
|
||||
} else if (showHiddenWarning) {
|
||||
// if it is a quoted or boosted note, how the hidden warning.
|
||||
HiddenNoteByMe(
|
||||
isQuote = true,
|
||||
onClick = { showAnyway.value = true },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadReportsNoteCompose(
|
||||
note: Note,
|
||||
modifier: Modifier = Modifier,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
normalNote: @Composable (canPreview: Boolean) -> Unit,
|
||||
) {
|
||||
var state by
|
||||
remember(note) {
|
||||
@ -336,20 +335,9 @@ fun LoadedNoteCompose(
|
||||
}
|
||||
|
||||
Crossfade(targetState = state, label = "LoadedNoteCompose") {
|
||||
RenderReportState(
|
||||
it,
|
||||
note,
|
||||
routeForLastRead,
|
||||
modifier,
|
||||
isBoostedNote,
|
||||
isQuotedNote,
|
||||
unPackReply,
|
||||
makeItShort,
|
||||
addMarginTop,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
RenderReportState(state = it, note = note, modifier = modifier, accountViewModel = accountViewModel, nav = nav) { canPreview ->
|
||||
normalNote(canPreview)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -357,16 +345,10 @@ fun LoadedNoteCompose(
|
||||
fun RenderReportState(
|
||||
state: AccountViewModel.NoteComposeReportState,
|
||||
note: Note,
|
||||
routeForLastRead: String? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
isBoostedNote: Boolean = false,
|
||||
isQuotedNote: Boolean = false,
|
||||
unPackReply: Boolean = true,
|
||||
makeItShort: Boolean = false,
|
||||
addMarginTop: Boolean = true,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
normalNote: @Composable (canPreview: Boolean) -> Unit,
|
||||
) {
|
||||
var showReportedNote by remember(note) { mutableStateOf(false) }
|
||||
|
||||
@ -377,27 +359,13 @@ fun RenderReportState(
|
||||
state.isHiddenAuthor,
|
||||
accountViewModel,
|
||||
modifier,
|
||||
isBoostedNote,
|
||||
nav,
|
||||
onClick = { showReportedNote = true },
|
||||
)
|
||||
} else {
|
||||
val canPreview = (!state.isAcceptable && showReportedNote) || state.canPreview
|
||||
|
||||
NormalNote(
|
||||
baseNote = note,
|
||||
routeForLastRead = routeForLastRead,
|
||||
modifier = modifier,
|
||||
isBoostedNote = isBoostedNote,
|
||||
isQuotedNote = isQuotedNote,
|
||||
unPackReply = unPackReply,
|
||||
makeItShort = makeItShort,
|
||||
addMarginTop = addMarginTop,
|
||||
canPreview = canPreview,
|
||||
parentBackgroundColor = parentBackgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
normalNote(canPreview)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -418,7 +386,7 @@ fun WatchForReports(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NormalNote(
|
||||
fun AcceptableNote(
|
||||
baseNote: Note,
|
||||
routeForLastRead: String? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
@ -426,8 +394,8 @@ fun NormalNote(
|
||||
isQuotedNote: Boolean = false,
|
||||
unPackReply: Boolean = true,
|
||||
makeItShort: Boolean = false,
|
||||
addMarginTop: Boolean = true,
|
||||
canPreview: Boolean = true,
|
||||
quotesLeft: Int,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
@ -445,35 +413,24 @@ fun NormalNote(
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
is CommunityDefinitionEvent ->
|
||||
(baseNote as? AddressableNote)?.let {
|
||||
CommunityHeader(
|
||||
baseNote = it,
|
||||
showBottomDiviser = true,
|
||||
sendToCommunity = true,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote)
|
||||
else ->
|
||||
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) {
|
||||
showPopup,
|
||||
->
|
||||
CheckNewAndRenderNote(
|
||||
baseNote,
|
||||
routeForLastRead,
|
||||
modifier,
|
||||
isBoostedNote,
|
||||
isQuotedNote,
|
||||
unPackReply,
|
||||
makeItShort,
|
||||
addMarginTop,
|
||||
canPreview,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
showPopup,
|
||||
nav,
|
||||
baseNote = baseNote,
|
||||
routeForLastRead = routeForLastRead,
|
||||
modifier = modifier,
|
||||
isBoostedNote = isBoostedNote,
|
||||
isQuotedNote = isQuotedNote,
|
||||
unPackReply = unPackReply,
|
||||
makeItShort = makeItShort,
|
||||
canPreview = canPreview,
|
||||
quotesLeft = quotesLeft,
|
||||
parentBackgroundColor = parentBackgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
showPopup = showPopup,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -490,17 +447,6 @@ fun NormalNote(
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
is CommunityDefinitionEvent ->
|
||||
(baseNote as? AddressableNote)?.let {
|
||||
CommunityHeader(
|
||||
baseNote = it,
|
||||
showBottomDiviser = true,
|
||||
sendToCommunity = true,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote)
|
||||
is FileHeaderEvent -> FileHeaderDisplay(baseNote, false, accountViewModel)
|
||||
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, false, accountViewModel)
|
||||
else ->
|
||||
@ -515,8 +461,8 @@ fun NormalNote(
|
||||
isQuotedNote = isQuotedNote,
|
||||
unPackReply = unPackReply,
|
||||
makeItShort = makeItShort,
|
||||
addMarginTop = addMarginTop,
|
||||
canPreview = canPreview,
|
||||
quotesLeft = quotesLeft,
|
||||
parentBackgroundColor = parentBackgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
showPopup = showPopup,
|
||||
@ -527,6 +473,36 @@ fun NormalNote(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun calculateBackgroundColor(
|
||||
createdAt: Long?,
|
||||
routeForLastRead: String? = null,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
): MutableState<Color> {
|
||||
val defaultBackgroundColor = MaterialTheme.colorScheme.background
|
||||
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
|
||||
return remember(createdAt) {
|
||||
mutableStateOf<Color>(
|
||||
if (routeForLastRead != null) {
|
||||
val isNew = accountViewModel.loadAndMarkAsRead(routeForLastRead, createdAt)
|
||||
|
||||
if (isNew) {
|
||||
if (parentBackgroundColor != null) {
|
||||
newItemColor.compositeOver(parentBackgroundColor.value)
|
||||
} else {
|
||||
newItemColor.compositeOver(defaultBackgroundColor)
|
||||
}
|
||||
} else {
|
||||
parentBackgroundColor?.value ?: defaultBackgroundColor
|
||||
}
|
||||
} else {
|
||||
parentBackgroundColor?.value ?: defaultBackgroundColor
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CheckNewAndRenderNote(
|
||||
baseNote: Note,
|
||||
@ -536,47 +512,20 @@ private fun CheckNewAndRenderNote(
|
||||
isQuotedNote: Boolean = false,
|
||||
unPackReply: Boolean = true,
|
||||
makeItShort: Boolean = false,
|
||||
addMarginTop: Boolean = true,
|
||||
canPreview: Boolean = true,
|
||||
quotesLeft: Int,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
showPopup: () -> Unit,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
|
||||
val defaultBackgroundColor = MaterialTheme.colorScheme.background
|
||||
val backgroundColor =
|
||||
remember(baseNote) {
|
||||
mutableStateOf<Color>(parentBackgroundColor?.value ?: defaultBackgroundColor)
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = routeForLastRead, key2 = parentBackgroundColor?.value) {
|
||||
routeForLastRead?.let {
|
||||
accountViewModel.loadAndMarkAsRead(it, baseNote.createdAt()) { isNew ->
|
||||
val newBackgroundColor =
|
||||
if (isNew) {
|
||||
if (parentBackgroundColor != null) {
|
||||
newItemColor.compositeOver(parentBackgroundColor.value)
|
||||
} else {
|
||||
newItemColor.compositeOver(defaultBackgroundColor)
|
||||
}
|
||||
} else {
|
||||
parentBackgroundColor?.value ?: defaultBackgroundColor
|
||||
}
|
||||
|
||||
if (newBackgroundColor != backgroundColor.value) {
|
||||
launch(Dispatchers.Main) { backgroundColor.value = newBackgroundColor }
|
||||
}
|
||||
}
|
||||
}
|
||||
?: run {
|
||||
val newBackgroundColor = parentBackgroundColor?.value ?: defaultBackgroundColor
|
||||
|
||||
if (newBackgroundColor != backgroundColor.value) {
|
||||
launch(Dispatchers.Main) { backgroundColor.value = newBackgroundColor }
|
||||
}
|
||||
}
|
||||
}
|
||||
calculateBackgroundColor(
|
||||
baseNote.createdAt(),
|
||||
routeForLastRead,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
)
|
||||
|
||||
ClickableNote(
|
||||
baseNote = baseNote,
|
||||
@ -591,10 +540,10 @@ private fun CheckNewAndRenderNote(
|
||||
backgroundColor = backgroundColor,
|
||||
isBoostedNote = isBoostedNote,
|
||||
isQuotedNote = isQuotedNote,
|
||||
addMarginTop = addMarginTop,
|
||||
unPackReply = unPackReply,
|
||||
makeItShort = makeItShort,
|
||||
canPreview = canPreview,
|
||||
quotesLeft = quotesLeft,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
@ -643,10 +592,10 @@ fun InnerNoteWithReactions(
|
||||
backgroundColor: MutableState<Color>,
|
||||
isBoostedNote: Boolean,
|
||||
isQuotedNote: Boolean,
|
||||
addMarginTop: Boolean,
|
||||
unPackReply: Boolean,
|
||||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
@ -655,10 +604,8 @@ fun InnerNoteWithReactions(
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
if (!isBoostedNote && addMarginTop) {
|
||||
if (!isBoostedNote) {
|
||||
normalWithTopMarginNoteModifier
|
||||
} else if (!isBoostedNote) {
|
||||
normalNoteModifier
|
||||
} else {
|
||||
boostedNoteModifier
|
||||
},
|
||||
@ -681,6 +628,7 @@ fun InnerNoteWithReactions(
|
||||
makeItShort = makeItShort,
|
||||
canPreview = canPreview,
|
||||
showSecondRow = showSecondRow,
|
||||
quotesLeft = quotesLeft,
|
||||
backgroundColor = backgroundColor,
|
||||
editState = editState,
|
||||
accountViewModel = accountViewModel,
|
||||
@ -707,12 +655,6 @@ fun InnerNoteWithReactions(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (notBoostedNorQuote) {
|
||||
HorizontalDivider(
|
||||
thickness = DividerThickness,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ -723,6 +665,7 @@ fun NoteBody(
|
||||
makeItShort: Boolean = false,
|
||||
canPreview: Boolean = true,
|
||||
showSecondRow: Boolean,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
editState: State<GenericLoadable<EditState>>,
|
||||
accountViewModel: AccountViewModel,
|
||||
@ -739,6 +682,7 @@ fun NoteBody(
|
||||
if (showSecondRow) {
|
||||
SecondUserInfoRow(
|
||||
baseNote,
|
||||
editState,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
@ -764,6 +708,7 @@ fun NoteBody(
|
||||
makeItShort = makeItShort,
|
||||
canPreview = canPreview,
|
||||
editState = editState,
|
||||
quotesLeft = quotesLeft,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
@ -782,68 +727,36 @@ private fun RenderNoteRow(
|
||||
backgroundColor: MutableState<Color>,
|
||||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
editState: State<GenericLoadable<EditState>>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val noteEvent = baseNote.event
|
||||
when (noteEvent) {
|
||||
is AppDefinitionEvent -> {
|
||||
RenderAppDefinition(baseNote, accountViewModel, nav)
|
||||
}
|
||||
is AudioTrackEvent -> {
|
||||
RenderAudioTrack(baseNote, accountViewModel, nav)
|
||||
}
|
||||
is AudioHeaderEvent -> {
|
||||
RenderAudioHeader(baseNote, accountViewModel, nav)
|
||||
}
|
||||
is ReactionEvent -> {
|
||||
RenderReaction(baseNote, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
is RepostEvent -> {
|
||||
RenderRepost(baseNote, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
is GenericRepostEvent -> {
|
||||
RenderRepost(baseNote, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
is ReportEvent -> {
|
||||
RenderReport(baseNote, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
is LongTextNoteEvent -> {
|
||||
RenderLongFormContent(baseNote, accountViewModel, nav)
|
||||
}
|
||||
is WikiNoteEvent -> {
|
||||
RenderWikiContent(baseNote, accountViewModel, nav)
|
||||
}
|
||||
is BadgeAwardEvent -> {
|
||||
RenderBadgeAward(baseNote, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
is FhirResourceEvent -> {
|
||||
RenderFhirResource(baseNote, accountViewModel, nav)
|
||||
}
|
||||
is PeopleListEvent -> {
|
||||
DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
is RelaySetEvent -> {
|
||||
DisplayRelaySet(baseNote, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
is PinListEvent -> {
|
||||
RenderPinListEvent(baseNote, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
is EmojiPackEvent -> {
|
||||
RenderEmojiPack(baseNote, true, backgroundColor, accountViewModel)
|
||||
}
|
||||
is LiveActivitiesEvent -> {
|
||||
RenderLiveActivityEvent(baseNote, accountViewModel, nav)
|
||||
}
|
||||
is GitRepositoryEvent -> {
|
||||
RenderGitRepositoryEvent(baseNote, accountViewModel, nav)
|
||||
}
|
||||
is AppDefinitionEvent -> RenderAppDefinition(baseNote, accountViewModel, nav)
|
||||
is AudioTrackEvent -> RenderAudioTrack(baseNote, accountViewModel, nav)
|
||||
is AudioHeaderEvent -> RenderAudioHeader(baseNote, accountViewModel, nav)
|
||||
is ReactionEvent -> RenderReaction(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||
is RepostEvent -> RenderRepost(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||
is GenericRepostEvent -> RenderRepost(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||
is ReportEvent -> RenderReport(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||
is LongTextNoteEvent -> RenderLongFormContent(baseNote, accountViewModel, nav)
|
||||
is WikiNoteEvent -> RenderWikiContent(baseNote, accountViewModel, nav)
|
||||
is BadgeAwardEvent -> RenderBadgeAward(baseNote, backgroundColor, accountViewModel, nav)
|
||||
is FhirResourceEvent -> RenderFhirResource(baseNote, accountViewModel, nav)
|
||||
is PeopleListEvent -> DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav)
|
||||
is RelaySetEvent -> DisplayRelaySet(baseNote, backgroundColor, accountViewModel, nav)
|
||||
is PinListEvent -> RenderPinListEvent(baseNote, backgroundColor, accountViewModel, nav)
|
||||
is EmojiPackEvent -> RenderEmojiPack(baseNote, true, backgroundColor, accountViewModel)
|
||||
is LiveActivitiesEvent -> RenderLiveActivityEvent(baseNote, accountViewModel, nav)
|
||||
is GitRepositoryEvent -> RenderGitRepositoryEvent(baseNote, accountViewModel, nav)
|
||||
is GitPatchEvent -> {
|
||||
RenderGitPatchEvent(
|
||||
baseNote,
|
||||
makeItShort,
|
||||
canPreview,
|
||||
quotesLeft,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
@ -854,6 +767,7 @@ private fun RenderNoteRow(
|
||||
baseNote,
|
||||
makeItShort,
|
||||
canPreview,
|
||||
quotesLeft,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
@ -864,6 +778,7 @@ private fun RenderNoteRow(
|
||||
baseNote,
|
||||
makeItShort,
|
||||
canPreview,
|
||||
quotesLeft,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
@ -882,6 +797,7 @@ private fun RenderNoteRow(
|
||||
baseNote,
|
||||
makeItShort,
|
||||
canPreview,
|
||||
quotesLeft,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
@ -892,28 +808,20 @@ private fun RenderNoteRow(
|
||||
baseNote,
|
||||
makeItShort,
|
||||
canPreview,
|
||||
quotesLeft,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
is FileHeaderEvent -> {
|
||||
FileHeaderDisplay(baseNote, true, accountViewModel)
|
||||
}
|
||||
is VideoHorizontalEvent -> {
|
||||
VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
is VideoVerticalEvent -> {
|
||||
VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
is FileStorageHeaderEvent -> {
|
||||
FileStorageHeaderDisplay(baseNote, true, accountViewModel)
|
||||
}
|
||||
is FileHeaderEvent -> FileHeaderDisplay(baseNote, true, accountViewModel)
|
||||
is VideoHorizontalEvent -> VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, accountViewModel, nav)
|
||||
is VideoVerticalEvent -> VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, accountViewModel, nav)
|
||||
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, true, accountViewModel)
|
||||
is CommunityPostApprovalEvent -> {
|
||||
RenderPostApproval(
|
||||
baseNote,
|
||||
makeItShort,
|
||||
canPreview,
|
||||
quotesLeft,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
@ -924,6 +832,7 @@ private fun RenderNoteRow(
|
||||
baseNote,
|
||||
makeItShort,
|
||||
canPreview,
|
||||
quotesLeft,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
@ -934,6 +843,7 @@ private fun RenderNoteRow(
|
||||
baseNote,
|
||||
makeItShort,
|
||||
canPreview,
|
||||
quotesLeft,
|
||||
backgroundColor,
|
||||
editState,
|
||||
accountViewModel,
|
||||
@ -946,18 +856,18 @@ private fun RenderNoteRow(
|
||||
@Composable
|
||||
fun RenderRepost(
|
||||
note: Note,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val boostedNote = remember { note.replyTo?.lastOrNull() }
|
||||
|
||||
boostedNote?.let {
|
||||
note.replyTo?.lastOrNull()?.let {
|
||||
NoteCompose(
|
||||
it,
|
||||
modifier = Modifier,
|
||||
isBoostedNote = true,
|
||||
unPackReply = false,
|
||||
quotesLeft = quotesLeft - 1,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
@ -1054,21 +964,19 @@ private fun ReplyNoteComposition(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val replyBackgroundColor = remember { mutableStateOf(backgroundColor.value) }
|
||||
val defaultReplyBackground = MaterialTheme.colorScheme.replyBackground
|
||||
|
||||
LaunchedEffect(key1 = backgroundColor.value, key2 = defaultReplyBackground) {
|
||||
launch(Dispatchers.Default) {
|
||||
val newReplyBackgroundColor = defaultReplyBackground.compositeOver(backgroundColor.value)
|
||||
if (replyBackgroundColor.value != newReplyBackgroundColor) {
|
||||
replyBackgroundColor.value = newReplyBackgroundColor
|
||||
}
|
||||
val replyBackgroundColor =
|
||||
remember {
|
||||
mutableStateOf(
|
||||
defaultReplyBackground.compositeOver(backgroundColor.value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
NoteCompose(
|
||||
baseNote = replyingDirectlyTo,
|
||||
isQuotedNote = true,
|
||||
quotesLeft = 0,
|
||||
modifier = MaterialTheme.colorScheme.replyModifier,
|
||||
unPackReply = false,
|
||||
makeItShort = true,
|
||||
@ -1081,6 +989,7 @@ private fun ReplyNoteComposition(
|
||||
@Composable
|
||||
fun SecondUserInfoRow(
|
||||
note: Note,
|
||||
editState: State<GenericLoadable<EditState>>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
@ -1115,7 +1024,22 @@ fun SecondUserInfoRow(
|
||||
DisplayPoW(pow)
|
||||
}
|
||||
|
||||
DisplayOts(note, accountViewModel)
|
||||
DisplayOtsIfInOriginal(note, editState, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayOtsIfInOriginal(
|
||||
note: Note,
|
||||
editState: State<GenericLoadable<EditState>>,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val editState = (editState.value as? GenericLoadable.Loaded<EditState>)?.loaded?.modificationToShow?.value
|
||||
|
||||
if (editState == null) {
|
||||
DisplayOts(note = note, accountViewModel = accountViewModel)
|
||||
} else {
|
||||
DisplayOts(note = editState, accountViewModel = accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,6 +76,7 @@ import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.StringToastMsg
|
||||
import com.vitorpamplona.amethyst.ui.theme.BigPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
@ -271,6 +272,7 @@ private fun RenderOptionAfterVote(
|
||||
TranslatableRichTextViewer(
|
||||
poolOption.descriptor,
|
||||
canPreview,
|
||||
quotesLeft = 1,
|
||||
Modifier,
|
||||
tags,
|
||||
backgroundColor,
|
||||
@ -303,14 +305,15 @@ private fun RenderOptionBeforeVote(
|
||||
),
|
||||
) {
|
||||
TranslatableRichTextViewer(
|
||||
description,
|
||||
canPreview,
|
||||
remember { Modifier.padding(15.dp) },
|
||||
tags,
|
||||
backgroundColor,
|
||||
content = description,
|
||||
canPreview = canPreview,
|
||||
quotesLeft = 1,
|
||||
modifier = BigPadding,
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
id = description,
|
||||
accountViewModel,
|
||||
nav,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -67,6 +67,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
@ -98,6 +99,7 @@ import coil.request.CachePolicy
|
||||
import coil.request.ImageRequest
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.zap
|
||||
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
@ -663,11 +665,11 @@ fun TextCount(
|
||||
|
||||
@Composable
|
||||
fun SlidingAnimationAmount(
|
||||
amount: MutableState<String>,
|
||||
amount: String,
|
||||
textColor: Color,
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = amount.value,
|
||||
targetState = amount,
|
||||
transitionSpec = AnimatedContentTransitionScope<String>::transitionSpec,
|
||||
label = "SlidingAnimationAmount",
|
||||
) { count ->
|
||||
@ -788,7 +790,7 @@ fun LikeReaction(
|
||||
),
|
||||
) {
|
||||
ObserveLikeIcon(baseNote, accountViewModel) { reactionType ->
|
||||
Crossfade(targetState = reactionType.value, label = "LikeIcon") {
|
||||
Crossfade(targetState = reactionType, label = "LikeIcon") {
|
||||
if (it != null) {
|
||||
RenderReactionType(it, heartSizeModifier, iconFontSize)
|
||||
} else {
|
||||
@ -826,19 +828,17 @@ fun LikeReaction(
|
||||
fun ObserveLikeIcon(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
inner: @Composable (MutableState<String?>) -> Unit,
|
||||
inner: @Composable (String?) -> Unit,
|
||||
) {
|
||||
val reactionType = remember(baseNote) { mutableStateOf<String?>(null) }
|
||||
|
||||
val reactionsState by baseNote.live().reactions.observeAsState()
|
||||
|
||||
LaunchedEffect(key1 = reactionsState) {
|
||||
accountViewModel.loadReactionTo(reactionsState?.note) { newReactionType ->
|
||||
if (reactionType.value != newReactionType) {
|
||||
reactionType.value = newReactionType
|
||||
val reactionType by
|
||||
produceState(initialValue = null as String?, key1 = reactionsState) {
|
||||
val newReactionType = accountViewModel.loadReactionTo(reactionsState?.note)
|
||||
if (value != newReactionType) {
|
||||
value = newReactionType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner(reactionType)
|
||||
}
|
||||
@ -1119,9 +1119,11 @@ fun ObserveZapIcon(
|
||||
val zapsState by baseNote.live().zaps.observeAsState()
|
||||
|
||||
LaunchedEffect(key1 = zapsState) {
|
||||
accountViewModel.calculateIfNoteWasZappedByAccount(baseNote) { newWasZapped ->
|
||||
if (wasZappedByLoggedInUser.value != newWasZapped) {
|
||||
wasZappedByLoggedInUser.value = newWasZapped
|
||||
if (zapsState?.note?.zapPayments?.isNotEmpty() == true || zapsState?.note?.zaps?.isNotEmpty() == true) {
|
||||
accountViewModel.calculateIfNoteWasZappedByAccount(baseNote) { newWasZapped ->
|
||||
if (wasZappedByLoggedInUser.value != newWasZapped) {
|
||||
wasZappedByLoggedInUser.value = newWasZapped
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1134,20 +1136,24 @@ fun ObserveZapIcon(
|
||||
fun ObserveZapAmountText(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
inner: @Composable (MutableState<String>) -> Unit,
|
||||
inner: @Composable (String) -> Unit,
|
||||
) {
|
||||
val zapAmountTxt = remember(baseNote) { mutableStateOf(showAmount(baseNote.zapsAmount)) }
|
||||
val zapsState by baseNote.live().zaps.observeAsState()
|
||||
|
||||
LaunchedEffect(key1 = zapsState) {
|
||||
accountViewModel.calculateZapAmount(baseNote) { newZapAmount ->
|
||||
if (zapAmountTxt.value != newZapAmount) {
|
||||
zapAmountTxt.value = newZapAmount
|
||||
if (zapsState?.note?.zapPayments?.isNotEmpty() == true) {
|
||||
val zapAmountTxt by
|
||||
produceState(initialValue = showAmount(baseNote.zapsAmount), key1 = baseNote) {
|
||||
accountViewModel.calculateZapAmount(baseNote) { newZapAmount ->
|
||||
if (value != newZapAmount) {
|
||||
value = newZapAmount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner(zapAmountTxt)
|
||||
inner(zapAmountTxt)
|
||||
} else {
|
||||
inner(showAmount(zapsState?.note?.zapsAmount))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
@ -444,11 +444,12 @@ fun payViaIntent(
|
||||
ContextCompat.startActivity(context, intent, null)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
if (e.message != null) {
|
||||
onError(context.getString(R.string.no_wallet_found_with_error, e.message!!))
|
||||
} else {
|
||||
onError(context.getString(R.string.no_wallet_found))
|
||||
}
|
||||
// don't display ugly error messages
|
||||
// if (e.message != null) {
|
||||
// onError(context.getString(R.string.no_wallet_found_with_error, e.message!!))
|
||||
// } else {
|
||||
onError(context.getString(R.string.no_wallet_found))
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,25 +29,17 @@ 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.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.ui.screen.ZapUserSetCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
|
||||
@Composable
|
||||
fun ZapUserSetCompose(
|
||||
@ -57,24 +49,12 @@ fun ZapUserSetCompose(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val defaultBackgroundColor = MaterialTheme.colorScheme.background
|
||||
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) }
|
||||
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
|
||||
|
||||
LaunchedEffect(key1 = zapSetCard.createdAt()) {
|
||||
accountViewModel.loadAndMarkAsRead(routeForLastRead, zapSetCard.createdAt) { isNew ->
|
||||
val newBackgroundColor =
|
||||
if (isNew) {
|
||||
newItemColor.compositeOver(defaultBackgroundColor)
|
||||
} else {
|
||||
defaultBackgroundColor
|
||||
}
|
||||
|
||||
if (backgroundColor.value != newBackgroundColor) {
|
||||
backgroundColor.value = newBackgroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
val backgroundColor =
|
||||
calculateBackgroundColor(
|
||||
createdAt = zapSetCard.createdAt,
|
||||
routeForLastRead = routeForLastRead,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
@ -131,9 +111,5 @@ fun ZapUserSetCompose(
|
||||
Spacer(DoubleVertSpacer)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
thickness = DividerThickness,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -232,6 +232,7 @@ fun RenderAppDefinition(
|
||||
TranslatableRichTextViewer(
|
||||
content = it,
|
||||
canPreview = false,
|
||||
quotesLeft = 1,
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
id = note.idHex,
|
||||
|
@ -216,6 +216,7 @@ fun AudioHeader(
|
||||
TranslatableRichTextViewer(
|
||||
content = it,
|
||||
canPreview = true,
|
||||
quotesLeft = 1,
|
||||
tags = tags,
|
||||
backgroundColor = background,
|
||||
id = note.idHex,
|
||||
|
@ -62,7 +62,6 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
||||
@ -219,15 +218,6 @@ fun RenderBadgeAward(
|
||||
}
|
||||
|
||||
note.replyTo?.firstOrNull()?.let {
|
||||
NoteCompose(
|
||||
it,
|
||||
modifier = Modifier,
|
||||
isBoostedNote = false,
|
||||
isQuotedNote = true,
|
||||
unPackReply = false,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
BadgeDisplay(baseNote = it)
|
||||
}
|
||||
}
|
||||
|
@ -180,6 +180,7 @@ fun LongCommunityHeader(
|
||||
TranslatableRichTextViewer(
|
||||
content = summary ?: stringResource(id = R.string.community_no_descriptor),
|
||||
canPreview = false,
|
||||
quotesLeft = 1,
|
||||
tags = EmptyTagList,
|
||||
backgroundColor = background,
|
||||
id = baseNote.idHex,
|
||||
|
@ -74,6 +74,7 @@ fun RenderGitPatchEvent(
|
||||
baseNote: Note,
|
||||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
@ -85,6 +86,7 @@ fun RenderGitPatchEvent(
|
||||
baseNote,
|
||||
makeItShort,
|
||||
canPreview,
|
||||
quotesLeft,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
@ -129,6 +131,7 @@ private fun RenderGitPatchEvent(
|
||||
note: Note,
|
||||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
@ -180,6 +183,7 @@ private fun RenderGitPatchEvent(
|
||||
TranslatableRichTextViewer(
|
||||
content = eventContent,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
quotesLeft = quotesLeft,
|
||||
modifier = modifier,
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
@ -205,6 +209,7 @@ fun RenderGitIssueEvent(
|
||||
baseNote: Note,
|
||||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
@ -216,6 +221,7 @@ fun RenderGitIssueEvent(
|
||||
baseNote,
|
||||
makeItShort,
|
||||
canPreview,
|
||||
quotesLeft,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
@ -228,6 +234,7 @@ private fun RenderGitIssueEvent(
|
||||
note: Note,
|
||||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
@ -279,6 +286,7 @@ private fun RenderGitIssueEvent(
|
||||
TranslatableRichTextViewer(
|
||||
content = eventContent,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
quotesLeft = quotesLeft,
|
||||
modifier = modifier,
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
@ -317,10 +325,10 @@ private fun RenderGitRepositoryEvent(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val title = remember(noteEvent) { noteEvent.name() ?: noteEvent.dTag() }
|
||||
val summary = remember(noteEvent) { noteEvent.description() }
|
||||
val web = remember(noteEvent) { noteEvent.web() }
|
||||
val clone = remember(noteEvent) { noteEvent.clone() }
|
||||
val title = noteEvent.name() ?: noteEvent.dTag()
|
||||
val summary = noteEvent.description()
|
||||
val web = noteEvent.web()
|
||||
val clone = noteEvent.clone()
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
|
@ -40,8 +40,6 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
|
||||
@ -62,22 +60,21 @@ fun RenderHighlight(
|
||||
note: Note,
|
||||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val quote = remember { (note.event as? HighlightEvent)?.quote() ?: "" }
|
||||
val author = remember { (note.event as? HighlightEvent)?.author() }
|
||||
val url = remember { (note.event as? HighlightEvent)?.inUrl() }
|
||||
val postHex = remember { (note.event as? HighlightEvent)?.taggedAddresses()?.firstOrNull() }
|
||||
val noteEvent = note.event as? HighlightEvent ?: return
|
||||
|
||||
DisplayHighlight(
|
||||
highlight = quote,
|
||||
authorHex = author,
|
||||
url = url,
|
||||
postAddress = postHex,
|
||||
highlight = noteEvent.quote(),
|
||||
authorHex = noteEvent.author(),
|
||||
url = noteEvent.inUrl(),
|
||||
postAddress = noteEvent.inPost(),
|
||||
makeItShort = makeItShort,
|
||||
canPreview = canPreview,
|
||||
quotesLeft = quotesLeft,
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
@ -92,6 +89,7 @@ fun DisplayHighlight(
|
||||
postAddress: ATag?,
|
||||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
@ -104,7 +102,8 @@ fun DisplayHighlight(
|
||||
TranslatableRichTextViewer(
|
||||
quote,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
remember { Modifier.fillMaxWidth() },
|
||||
quotesLeft,
|
||||
Modifier.fillMaxWidth(),
|
||||
EmptyTagList,
|
||||
backgroundColor,
|
||||
id = quote,
|
||||
@ -171,7 +170,7 @@ fun LoadAndDisplayUrl(url: String) {
|
||||
}
|
||||
|
||||
validatedUrl?.host?.let { host ->
|
||||
Text(remember { "-" }, maxLines = 1)
|
||||
Text("-", maxLines = 1)
|
||||
ClickableUrl(urlText = host, url = url)
|
||||
}
|
||||
}
|
||||
@ -182,17 +181,15 @@ private fun LoadAndDisplayPost(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
LoadAddressableNote(aTag = postAddress, accountViewModel) {
|
||||
it?.let { note ->
|
||||
val noteEvent by
|
||||
note.live().metadata.map { it.note.event }.distinctUntilChanged().observeAsState(note.event)
|
||||
LoadAddressableNote(aTag = postAddress, accountViewModel) { aTag ->
|
||||
aTag?.let { note ->
|
||||
val noteState by note.live().metadata.observeAsState()
|
||||
val noteEvent = noteState?.note?.event as? LongTextNoteEvent ?: return@LoadAddressableNote
|
||||
|
||||
val title = remember(noteEvent) { (noteEvent as? LongTextNoteEvent)?.title() }
|
||||
|
||||
title?.let {
|
||||
Text(remember { "-" }, maxLines = 1)
|
||||
noteEvent.title()?.let {
|
||||
Text("-", maxLines = 1)
|
||||
ClickableText(
|
||||
text = AnnotatedString(title),
|
||||
text = AnnotatedString(it),
|
||||
onClick = { routeFor(note, accountViewModel.userProfile())?.let { nav(it) } },
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary),
|
||||
)
|
||||
|
@ -105,6 +105,7 @@ fun RenderPinListEvent(
|
||||
TranslatableRichTextViewer(
|
||||
content = pin,
|
||||
canPreview = true,
|
||||
quotesLeft = 1,
|
||||
tags = EmptyTagList,
|
||||
backgroundColor = backgroundColor,
|
||||
id = baseNote.idHex,
|
||||
|
@ -46,6 +46,7 @@ fun RenderPoll(
|
||||
note: Note,
|
||||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
@ -70,7 +71,8 @@ fun RenderPoll(
|
||||
TranslatableRichTextViewer(
|
||||
content = eventContent,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
modifier = remember { Modifier.fillMaxWidth() },
|
||||
quotesLeft = quotesLeft,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
id = note.idHex,
|
||||
|
@ -50,6 +50,7 @@ fun RenderPrivateMessage(
|
||||
note: Note,
|
||||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
@ -81,6 +82,7 @@ fun RenderPrivateMessage(
|
||||
TranslatableRichTextViewer(
|
||||
content = eventContent,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
quotesLeft = quotesLeft,
|
||||
modifier = modifier,
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
@ -109,12 +111,13 @@ fun RenderPrivateMessage(
|
||||
"@$recipient",
|
||||
),
|
||||
canPreview = !makeItShort,
|
||||
Modifier.fillMaxWidth(),
|
||||
EmptyTagList,
|
||||
backgroundColor,
|
||||
quotesLeft = 0,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
tags = EmptyTagList,
|
||||
backgroundColor = backgroundColor,
|
||||
id = note.idHex,
|
||||
accountViewModel,
|
||||
nav,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@Composable
|
||||
fun RenderReaction(
|
||||
note: Note,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
@ -42,6 +43,7 @@ fun RenderReaction(
|
||||
modifier = Modifier,
|
||||
isBoostedNote = true,
|
||||
unPackReply = false,
|
||||
quotesLeft = quotesLeft - 1,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
|
@ -45,8 +45,7 @@ import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
||||
@Composable
|
||||
fun RenderPostApproval(
|
||||
note: Note,
|
||||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
@ -56,12 +55,13 @@ fun RenderPostApproval(
|
||||
val noteEvent = note.event as? CommunityPostApprovalEvent ?: return
|
||||
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
noteEvent.communities().forEach {
|
||||
LoadAddressableNote(it, accountViewModel) {
|
||||
it?.let {
|
||||
NoteCompose(
|
||||
it,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
noteEvent.communities().forEach { tag ->
|
||||
LoadAddressableNote(tag, accountViewModel) { baseNote ->
|
||||
baseNote?.let {
|
||||
CommunityHeader(
|
||||
baseNote = it,
|
||||
showBottomDiviser = false,
|
||||
sendToCommunity = true,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
@ -88,6 +88,7 @@ fun RenderPostApproval(
|
||||
unPackReply = false,
|
||||
makeItShort = true,
|
||||
isQuotedNote = true,
|
||||
quotesLeft = quotesLeft - 1,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
|
@ -20,31 +20,26 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.types
|
||||
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.quartz.events.EmptyTagList
|
||||
import com.vitorpamplona.quartz.events.ReportEvent
|
||||
|
||||
@Composable
|
||||
fun RenderReport(
|
||||
note: Note,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
@ -81,6 +76,7 @@ fun RenderReport(
|
||||
tags = EmptyTagList,
|
||||
backgroundColor = backgroundColor,
|
||||
id = note.idHex,
|
||||
quotesLeft = 1,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
@ -89,18 +85,10 @@ fun RenderReport(
|
||||
NoteCompose(
|
||||
baseNote = it,
|
||||
isQuotedNote = true,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(top = 5.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(
|
||||
1.dp,
|
||||
MaterialTheme.colorScheme.subtleBorder,
|
||||
QuoteBorder,
|
||||
),
|
||||
modifier = MaterialTheme.colorScheme.replyModifier,
|
||||
unPackReply = false,
|
||||
makeItShort = true,
|
||||
quotesLeft = quotesLeft - 1,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
|
@ -51,6 +51,7 @@ fun RenderTextEvent(
|
||||
note: Note,
|
||||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
editState: State<GenericLoadable<EditState>>,
|
||||
accountViewModel: AccountViewModel,
|
||||
@ -103,6 +104,7 @@ fun RenderTextEvent(
|
||||
TranslatableRichTextViewer(
|
||||
content = eventContent,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
quotesLeft = quotesLeft,
|
||||
modifier = modifier,
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
|
@ -67,6 +67,7 @@ fun RenderTextModificationEvent(
|
||||
note: Note,
|
||||
makeItShort: Boolean,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
@ -113,6 +114,7 @@ fun RenderTextModificationEvent(
|
||||
TranslatableRichTextViewer(
|
||||
content = it,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
quotesLeft = quotesLeft,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
tags = EmptyTagList,
|
||||
backgroundColor = backgroundColor,
|
||||
@ -182,6 +184,7 @@ fun RenderTextModificationEvent(
|
||||
makeItShort = false,
|
||||
canPreview = true,
|
||||
showSecondRow = false,
|
||||
quotesLeft = quotesLeft,
|
||||
backgroundColor = backgroundColor,
|
||||
editState = editState,
|
||||
accountViewModel = accountViewModel,
|
||||
|
@ -163,6 +163,7 @@ fun VideoDisplay(
|
||||
TranslatableRichTextViewer(
|
||||
content = it,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
quotesLeft = 1,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
|
@ -31,6 +31,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material3.pullrefresh.pullRefresh
|
||||
import androidx.compose.material3.pullrefresh.rememberPullRefreshState
|
||||
@ -55,6 +56,7 @@ import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapTheDevsCard
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapUserSetCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
|
||||
@Composable
|
||||
@ -196,6 +198,9 @@ private fun FeedLoaded(
|
||||
nav,
|
||||
)
|
||||
}
|
||||
HorizontalDivider(
|
||||
thickness = DividerThickness,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -284,7 +289,6 @@ fun NoteCardCompose(
|
||||
isQuotedNote: Boolean = false,
|
||||
unPackReply: Boolean = true,
|
||||
makeItShort: Boolean = false,
|
||||
addMarginTop: Boolean = true,
|
||||
showHidden: Boolean = false,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
@ -300,8 +304,8 @@ fun NoteCardCompose(
|
||||
isQuotedNote = isQuotedNote,
|
||||
unPackReply = unPackReply,
|
||||
makeItShort = makeItShort,
|
||||
addMarginTop = addMarginTop,
|
||||
showHidden = showHidden,
|
||||
quotesLeft = 3,
|
||||
parentBackgroundColor = parentBackgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
|
@ -38,6 +38,7 @@ import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.pullrefresh.PullRefreshIndicator
|
||||
@ -56,6 +57,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
|
||||
@ -227,24 +229,15 @@ private fun FeedLoaded(
|
||||
modifier = Modifier,
|
||||
isBoostedNote = false,
|
||||
showHidden = state.showHidden.value,
|
||||
quotesLeft = 3,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
|
||||
/*var see by
|
||||
remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
if (see) {
|
||||
} else {
|
||||
Row(defaultModifier) {
|
||||
Button(onClick = { see = true }) {
|
||||
Text("Show")
|
||||
}
|
||||
}
|
||||
}*/
|
||||
HorizontalDivider(
|
||||
thickness = DividerThickness,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ class SettingsState() {
|
||||
var automaticallyShowProfilePictures by mutableStateOf(ConnectivityType.ALWAYS)
|
||||
var dontShowPushNotificationSelector by mutableStateOf<Boolean>(false)
|
||||
var dontAskForNotificationPermissions by mutableStateOf<Boolean>(false)
|
||||
var featureSet by mutableStateOf(FeatureSetType.SIMPLIFIED)
|
||||
var featureSet by mutableStateOf(FeatureSetType.COMPLETE)
|
||||
|
||||
var isOnMobileData: State<Boolean> = mutableStateOf(false)
|
||||
|
||||
|
@ -93,6 +93,7 @@ import com.vitorpamplona.amethyst.ui.components.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
|
||||
import com.vitorpamplona.amethyst.ui.note.BlankNote
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayDraft
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayOtsIfInOriginal
|
||||
import com.vitorpamplona.amethyst.ui.note.HiddenNote
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
|
||||
@ -106,7 +107,6 @@ 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
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DisplayOts
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DisplayPoW
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DisplayReward
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DisplayZapSplits
|
||||
@ -264,34 +264,35 @@ fun ThreadFeedView(
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Column {
|
||||
Row {
|
||||
val selectedNoteColor = MaterialTheme.colorScheme.selectedNote
|
||||
val background =
|
||||
remember {
|
||||
if (item.idHex == noteId) mutableStateOf(selectedNoteColor) else null
|
||||
}
|
||||
|
||||
NoteCompose(
|
||||
item,
|
||||
modifier =
|
||||
Modifier.drawReplyLevel(
|
||||
item.replyLevel(),
|
||||
MaterialTheme.colorScheme.placeholderText,
|
||||
if (item.idHex == noteId) {
|
||||
MaterialTheme.colorScheme.lessImportantLink
|
||||
} else {
|
||||
MaterialTheme.colorScheme.placeholderText
|
||||
},
|
||||
),
|
||||
parentBackgroundColor = background,
|
||||
isBoostedNote = false,
|
||||
unPackReply = false,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
val selectedNoteColor = MaterialTheme.colorScheme.selectedNote
|
||||
val background =
|
||||
remember {
|
||||
if (item.idHex == noteId) mutableStateOf(selectedNoteColor) else null
|
||||
}
|
||||
}
|
||||
|
||||
NoteCompose(
|
||||
item,
|
||||
modifier =
|
||||
Modifier.drawReplyLevel(
|
||||
item.replyLevel(),
|
||||
MaterialTheme.colorScheme.placeholderText,
|
||||
if (item.idHex == noteId) {
|
||||
MaterialTheme.colorScheme.lessImportantLink
|
||||
} else {
|
||||
MaterialTheme.colorScheme.placeholderText
|
||||
},
|
||||
),
|
||||
parentBackgroundColor = background,
|
||||
isBoostedNote = false,
|
||||
unPackReply = false,
|
||||
quotesLeft = 3,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
|
||||
HorizontalDivider(
|
||||
thickness = DividerThickness,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -377,7 +378,6 @@ fun NoteMaster(
|
||||
note.author?.let { account.isHidden(it) } ?: false,
|
||||
accountViewModel,
|
||||
Modifier,
|
||||
false,
|
||||
nav,
|
||||
onClick = { showHiddenNote = true },
|
||||
)
|
||||
@ -473,7 +473,7 @@ fun NoteMaster(
|
||||
DisplayDraft()
|
||||
}
|
||||
|
||||
DisplayOts(note, accountViewModel)
|
||||
DisplayOtsIfInOriginal(note, editState, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -527,8 +527,7 @@ fun NoteMaster(
|
||||
} else if (noteEvent is CommunityPostApprovalEvent) {
|
||||
RenderPostApproval(
|
||||
baseNote,
|
||||
false,
|
||||
true,
|
||||
quotesLeft = 3,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
@ -559,9 +558,9 @@ fun NoteMaster(
|
||||
} else if (noteEvent is GitRepositoryEvent) {
|
||||
RenderGitRepositoryEvent(baseNote, accountViewModel, nav)
|
||||
} else if (noteEvent is GitPatchEvent) {
|
||||
RenderGitPatchEvent(baseNote, false, true, backgroundColor, accountViewModel, nav)
|
||||
RenderGitPatchEvent(baseNote, false, true, quotesLeft = 3, backgroundColor, accountViewModel, nav)
|
||||
} else if (noteEvent is GitIssueEvent) {
|
||||
RenderGitIssueEvent(baseNote, false, true, backgroundColor, accountViewModel, nav)
|
||||
RenderGitIssueEvent(baseNote, false, true, quotesLeft = 3, backgroundColor, accountViewModel, nav)
|
||||
} else if (noteEvent is AppDefinitionEvent) {
|
||||
RenderAppDefinition(baseNote, accountViewModel, nav)
|
||||
} else if (noteEvent is HighlightEvent) {
|
||||
@ -572,17 +571,19 @@ fun NoteMaster(
|
||||
noteEvent.inPost(),
|
||||
false,
|
||||
true,
|
||||
quotesLeft = 3,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
} else if (noteEvent is RepostEvent || noteEvent is GenericRepostEvent) {
|
||||
RenderRepost(baseNote, backgroundColor, accountViewModel, nav)
|
||||
RenderRepost(baseNote, quotesLeft = 3, backgroundColor, accountViewModel, nav)
|
||||
} else if (noteEvent is TextNoteModificationEvent) {
|
||||
RenderTextModificationEvent(
|
||||
note = baseNote,
|
||||
makeItShort = false,
|
||||
canPreview = true,
|
||||
quotesLeft = 3,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
@ -597,6 +598,7 @@ fun NoteMaster(
|
||||
baseNote,
|
||||
false,
|
||||
canPreview,
|
||||
quotesLeft = 3,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
@ -611,6 +613,7 @@ fun NoteMaster(
|
||||
baseNote,
|
||||
false,
|
||||
canPreview,
|
||||
quotesLeft = 3,
|
||||
backgroundColor,
|
||||
editState,
|
||||
accountViewModel,
|
||||
@ -890,6 +893,7 @@ private fun RenderWikiHeaderForThreadPreview() {
|
||||
baseNote!!,
|
||||
false,
|
||||
true,
|
||||
quotesLeft = 3,
|
||||
backgroundColor,
|
||||
editState,
|
||||
accountViewModel,
|
||||
|
@ -37,6 +37,7 @@ import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ServiceManager
|
||||
import com.vitorpamplona.amethyst.commons.compose.GenericBaseCache
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AccountState
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
@ -220,21 +221,21 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
viewModelScope.launch(Dispatchers.IO) { account.delete(account.boostsTo(note)) }
|
||||
}
|
||||
|
||||
fun calculateIfNoteWasZappedByAccount(
|
||||
suspend fun calculateIfNoteWasZappedByAccount(
|
||||
zappedNote: Note,
|
||||
onWasZapped: (Boolean) -> Unit,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
withContext(Dispatchers.IO) {
|
||||
account.calculateIfNoteWasZappedByAccount(zappedNote) { onWasZapped(true) }
|
||||
}
|
||||
}
|
||||
|
||||
fun calculateZapAmount(
|
||||
suspend fun calculateZapAmount(
|
||||
zappedNote: Note,
|
||||
onZapAmount: (String) -> Unit,
|
||||
) {
|
||||
if (zappedNote.zapPayments.isNotEmpty()) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
withContext(Dispatchers.IO) {
|
||||
account.calculateZappedAmount(zappedNote) { onZapAmount(showAmount(it)) }
|
||||
}
|
||||
} else {
|
||||
@ -242,28 +243,45 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
}
|
||||
}
|
||||
|
||||
fun calculateZapraiser(
|
||||
suspend fun calculateZapraiser(
|
||||
zappedNote: Note,
|
||||
onZapraiserStatus: (ZapraiserStatus) -> Unit,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val zapraiserAmount = zappedNote.event?.zapraiserAmount() ?: 0
|
||||
account.calculateZappedAmount(zappedNote) { newZapAmount ->
|
||||
var percentage = newZapAmount.div(zapraiserAmount.toBigDecimal()).toFloat()
|
||||
val zapraiserAmount = zappedNote.event?.zapraiserAmount() ?: 0
|
||||
if (zappedNote.zapPayments.isNotEmpty()) {
|
||||
withContext(Dispatchers.IO) {
|
||||
account.calculateZappedAmount(zappedNote) { newZapAmount ->
|
||||
var percentage = newZapAmount.div(zapraiserAmount.toBigDecimal()).toFloat()
|
||||
|
||||
if (percentage > 1) {
|
||||
percentage = 1f
|
||||
}
|
||||
|
||||
val newZapraiserProgress = percentage
|
||||
val newZapraiserLeft =
|
||||
if (percentage > 0.99) {
|
||||
"0"
|
||||
} else {
|
||||
showAmount((zapraiserAmount * (1 - percentage)).toBigDecimal())
|
||||
if (percentage > 1) {
|
||||
percentage = 1f
|
||||
}
|
||||
onZapraiserStatus(ZapraiserStatus(newZapraiserProgress, newZapraiserLeft))
|
||||
|
||||
val newZapraiserProgress = percentage
|
||||
val newZapraiserLeft =
|
||||
if (percentage > 0.99) {
|
||||
"0"
|
||||
} else {
|
||||
showAmount((zapraiserAmount * (1 - percentage)).toBigDecimal())
|
||||
}
|
||||
onZapraiserStatus(ZapraiserStatus(newZapraiserProgress, newZapraiserLeft))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var percentage = zappedNote.zapsAmount.div(zapraiserAmount.toBigDecimal()).toFloat()
|
||||
|
||||
if (percentage > 1) {
|
||||
percentage = 1f
|
||||
}
|
||||
|
||||
val newZapraiserProgress = percentage
|
||||
val newZapraiserLeft =
|
||||
if (percentage > 0.99) {
|
||||
"0"
|
||||
} else {
|
||||
showAmount((zapraiserAmount * (1 - percentage)).toBigDecimal())
|
||||
}
|
||||
onZapraiserStatus(ZapraiserStatus(newZapraiserProgress, newZapraiserLeft))
|
||||
}
|
||||
}
|
||||
|
||||
@ -681,41 +699,42 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
val relevantReports: ImmutableSet<Note> = persistentSetOf(),
|
||||
)
|
||||
|
||||
fun isNoteAcceptable(
|
||||
suspend fun isNoteAcceptable(
|
||||
note: Note,
|
||||
onReady: (NoteComposeReportState) -> Unit,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val isFromLoggedIn = note.author?.pubkeyHex == userProfile().pubkeyHex
|
||||
val isFromLoggedInFollow = note.author?.let { userProfile().isFollowingCached(it) } ?: true
|
||||
val newState =
|
||||
withContext(Dispatchers.IO) {
|
||||
val isFromLoggedIn = note.author?.pubkeyHex == userProfile().pubkeyHex
|
||||
val isFromLoggedInFollow = note.author?.let { userProfile().isFollowingCached(it) } ?: true
|
||||
|
||||
if (isFromLoggedIn || isFromLoggedInFollow) {
|
||||
// No need to process if from trusted people
|
||||
onReady(NoteComposeReportState(true, true, false, persistentSetOf()))
|
||||
} else if (note.author?.let { account.isHidden(it) } == true) {
|
||||
onReady(NoteComposeReportState(false, false, true, persistentSetOf()))
|
||||
} else {
|
||||
val newCanPreview = !note.hasAnyReports()
|
||||
|
||||
val newIsAcceptable = account.isAcceptable(note)
|
||||
|
||||
if (newCanPreview && newIsAcceptable) {
|
||||
// No need to process reports if nothing is wrong
|
||||
onReady(NoteComposeReportState(true, true, false, persistentSetOf()))
|
||||
if (isFromLoggedIn || isFromLoggedInFollow) {
|
||||
// No need to process if from trusted people
|
||||
NoteComposeReportState(true, true, false, persistentSetOf())
|
||||
} else if (note.author?.let { account.isHidden(it) } == true) {
|
||||
NoteComposeReportState(false, false, true, persistentSetOf())
|
||||
} else {
|
||||
val newRelevantReports = account.getRelevantReports(note)
|
||||
val newCanPreview = !note.hasAnyReports()
|
||||
|
||||
val newIsAcceptable = account.isAcceptable(note)
|
||||
|
||||
if (newCanPreview && newIsAcceptable) {
|
||||
// No need to process reports if nothing is wrong
|
||||
NoteComposeReportState(true, true, false, persistentSetOf())
|
||||
} else {
|
||||
val newRelevantReports = account.getRelevantReports(note)
|
||||
|
||||
onReady(
|
||||
NoteComposeReportState(
|
||||
newIsAcceptable,
|
||||
newCanPreview,
|
||||
false,
|
||||
newRelevantReports.toImmutableSet(),
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onReady(newState)
|
||||
}
|
||||
|
||||
fun unwrap(
|
||||
@ -774,15 +793,10 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
viewModelScope.launch(Dispatchers.IO) { UrlCachedPreviewer.previewInfo(url, onResult) }
|
||||
}
|
||||
|
||||
fun loadReactionTo(
|
||||
note: Note?,
|
||||
onNewReactionType: (String?) -> Unit,
|
||||
) {
|
||||
if (note == null) return
|
||||
suspend fun loadReactionTo(note: Note?): String? {
|
||||
if (note == null) return null
|
||||
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
onNewReactionType(note.getReactionBy(userProfile()))
|
||||
}
|
||||
return note.getReactionBy(userProfile())
|
||||
}
|
||||
|
||||
fun verifyNip05(
|
||||
@ -1021,37 +1035,6 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun parseNIP19(
|
||||
str: String,
|
||||
onNote: (LoadedBechLink) -> Unit,
|
||||
) {
|
||||
withContext(Dispatchers.IO) {
|
||||
Nip19Bech32.uriToRoute(str)?.let {
|
||||
var returningNote: Note? = null
|
||||
|
||||
when (val parsed = it.entity) {
|
||||
is Nip19Bech32.NSec -> {}
|
||||
is Nip19Bech32.NPub -> {}
|
||||
is Nip19Bech32.NProfile -> {}
|
||||
is Nip19Bech32.Note -> LocalCache.checkGetOrCreateNote(parsed.hex)?.let { note -> returningNote = note }
|
||||
is Nip19Bech32.NEvent -> LocalCache.checkGetOrCreateNote(parsed.hex)?.let { note -> returningNote = note }
|
||||
is Nip19Bech32.NEmbed -> {
|
||||
loadNEmbedIfNeeded(parsed.event)
|
||||
|
||||
LocalCache.checkGetOrCreateNote(parsed.event.id)?.let { note ->
|
||||
returningNote = note
|
||||
}
|
||||
}
|
||||
is Nip19Bech32.NRelay -> {}
|
||||
is Nip19Bech32.NAddress -> LocalCache.checkGetOrCreateNote(parsed.atag)?.let { note -> returningNote = note }
|
||||
else -> {}
|
||||
}
|
||||
|
||||
onNote(LoadedBechLink(returningNote, it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun checkIsOnline(
|
||||
media: String?,
|
||||
onDone: (Boolean) -> Unit,
|
||||
@ -1067,20 +1050,22 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
fun loadAndMarkAsRead(
|
||||
routeForLastRead: String,
|
||||
createdAt: Long?,
|
||||
onIsNew: (Boolean) -> Unit,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val lastTime = account.loadLastRead(routeForLastRead)
|
||||
): Boolean {
|
||||
if (createdAt == null) return false
|
||||
|
||||
if (createdAt != null) {
|
||||
val lastTime = account.loadLastRead(routeForLastRead)
|
||||
|
||||
val onIsNew = createdAt > lastTime
|
||||
|
||||
if (onIsNew) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
if (account.markAsRead(routeForLastRead, createdAt)) {
|
||||
refreshMarkAsReadObservers()
|
||||
}
|
||||
onIsNew(createdAt > lastTime)
|
||||
} else {
|
||||
onIsNew(false)
|
||||
}
|
||||
}
|
||||
|
||||
return onIsNew
|
||||
}
|
||||
|
||||
fun markAllAsRead(
|
||||
@ -1319,9 +1304,41 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun deleteDraft(draftTag: String) {
|
||||
val notes = LocalCache.draftNotes(draftTag)
|
||||
account.delete(notes)
|
||||
}
|
||||
|
||||
val bechLinkCache = CachedLoadedBechLink(this)
|
||||
|
||||
class CachedLoadedBechLink(val accountViewModel: AccountViewModel) : GenericBaseCache<String, LoadedBechLink>(20) {
|
||||
override suspend fun compute(key: String): LoadedBechLink? {
|
||||
return Nip19Bech32.uriToRoute(key)?.let {
|
||||
var returningNote: Note? = null
|
||||
|
||||
when (val parsed = it.entity) {
|
||||
is Nip19Bech32.NSec -> {}
|
||||
is Nip19Bech32.NPub -> {}
|
||||
is Nip19Bech32.NProfile -> {}
|
||||
is Nip19Bech32.Note -> withContext(Dispatchers.IO) { LocalCache.checkGetOrCreateNote(parsed.hex)?.let { note -> returningNote = note } }
|
||||
is Nip19Bech32.NEvent -> withContext(Dispatchers.IO) { LocalCache.checkGetOrCreateNote(parsed.hex)?.let { note -> returningNote = note } }
|
||||
is Nip19Bech32.NEmbed ->
|
||||
withContext(Dispatchers.IO) {
|
||||
accountViewModel.loadNEmbedIfNeeded(parsed.event)
|
||||
|
||||
LocalCache.checkGetOrCreateNote(parsed.event.id)?.let { note ->
|
||||
returningNote = note
|
||||
}
|
||||
}
|
||||
is Nip19Bech32.NRelay -> {}
|
||||
is Nip19Bech32.NAddress -> withContext(Dispatchers.IO) { LocalCache.checkGetOrCreateNote(parsed.atag)?.let { note -> returningNote = note } }
|
||||
else -> {}
|
||||
}
|
||||
|
||||
LoadedBechLink(returningNote, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -834,6 +834,7 @@ fun LongChannelHeader(
|
||||
TranslatableRichTextViewer(
|
||||
content = summary ?: stringResource(id = R.string.groups_no_descriptor),
|
||||
canPreview = false,
|
||||
quotesLeft = 1,
|
||||
tags = tags,
|
||||
backgroundColor = background,
|
||||
id = baseChannel.idHex,
|
||||
|
@ -1103,6 +1103,7 @@ private fun DrawAdditionalInfo(
|
||||
TranslatableRichTextViewer(
|
||||
content = it,
|
||||
canPreview = false,
|
||||
quotesLeft = 1,
|
||||
tags = EmptyTagList,
|
||||
backgroundColor = background,
|
||||
id = it,
|
||||
|
@ -36,7 +36,6 @@ import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@ -290,7 +289,6 @@ private fun SearchBar(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun SearchTextField(
|
||||
searchBarViewModel: SearchBarViewModel,
|
||||
@ -411,9 +409,14 @@ private fun DisplaySearchResults(
|
||||
) { _, item ->
|
||||
NoteCompose(
|
||||
item,
|
||||
quotesLeft = 1,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
|
||||
HorizontalDivider(
|
||||
thickness = DividerThickness,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -303,7 +303,6 @@ fun RenderReportState(
|
||||
state.isHiddenAuthor,
|
||||
accountViewModel,
|
||||
Modifier.fillMaxWidth(),
|
||||
false,
|
||||
nav,
|
||||
onClick = { showReportedNote = true },
|
||||
)
|
||||
|
@ -100,6 +100,7 @@ val HalfTopPadding = Modifier.padding(top = 5.dp)
|
||||
|
||||
val HalfPadding = Modifier.padding(5.dp)
|
||||
val StdPadding = Modifier.padding(10.dp)
|
||||
val BigPadding = Modifier.padding(15.dp)
|
||||
|
||||
val HalfHorzPadding = Modifier.padding(horizontal = 5.dp)
|
||||
val HalfVertPadding = Modifier.padding(vertical = 5.dp)
|
||||
@ -200,14 +201,6 @@ val imageHeaderBannerSize = Modifier.fillMaxWidth().height(150.dp)
|
||||
|
||||
val authorNotePictureForImageHeader = Modifier.size(75.dp).padding(10.dp)
|
||||
|
||||
val normalNoteModifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.padding(
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
top = 0.dp,
|
||||
)
|
||||
|
||||
val normalWithTopMarginNoteModifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.padding(
|
||||
|
@ -142,6 +142,7 @@
|
||||
<string name="clear">মুছে ফেলুন</string>
|
||||
<string name="app_logo">অ্যাপের লোগো</string>
|
||||
<string name="nsec_npub_hex_private_key">Nsec.. অথবা Npub..</string>
|
||||
<string name="ncryptsec_password">কী খুলতে পাসওয়ার্ড</string>
|
||||
<string name="show_password">পাসওয়ার্ড দেখান</string>
|
||||
<string name="hide_password">পাসওয়ার্ড লুকান</string>
|
||||
<string name="invalid_key">অকেজো চাবি</string>
|
||||
|
@ -15,4 +15,7 @@
|
||||
<string name="group_picture">Group Picture</string>
|
||||
<string name="explicit_content">Explicit Content</string>
|
||||
<string name="spam">Spam</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="posts">Posts</string>
|
||||
<string name="bytes"></string>
|
||||
</resources>
|
||||
|
@ -441,6 +441,8 @@
|
||||
<string name="connectivity_type_always">Siempre</string>
|
||||
<string name="connectivity_type_wifi_only">Solo Wi-Fi</string>
|
||||
<string name="connectivity_type_never">Nunca</string>
|
||||
<string name="ui_feature_set_type_complete">Completo</string>
|
||||
<string name="ui_feature_set_type_simplified">Simplificado</string>
|
||||
<string name="system">Sistema</string>
|
||||
<string name="light">Claro</string>
|
||||
<string name="dark">Oscuro</string>
|
||||
@ -452,6 +454,8 @@
|
||||
<string name="automatically_show_url_preview">Vista previa de URL</string>
|
||||
<string name="automatically_hide_nav_bars">Desplazamiento inmersivo</string>
|
||||
<string name="automatically_hide_nav_bars_description">Ocultar barras de navegación al desplazarse</string>
|
||||
<string name="ui_style">Modo de interfaz</string>
|
||||
<string name="ui_style_description">Elegir el estilo de publicación</string>
|
||||
<string name="load_image">Cargar imagen</string>
|
||||
<string name="spamming_users">Spammers</string>
|
||||
<string name="muted_button">Silenciado. Hacer clic para reactivar el sonido.</string>
|
||||
|
@ -441,6 +441,8 @@
|
||||
<string name="connectivity_type_always">Siempre</string>
|
||||
<string name="connectivity_type_wifi_only">Solo Wi-Fi</string>
|
||||
<string name="connectivity_type_never">Nunca</string>
|
||||
<string name="ui_feature_set_type_complete">Completo</string>
|
||||
<string name="ui_feature_set_type_simplified">Simplificado</string>
|
||||
<string name="system">Sistema</string>
|
||||
<string name="light">Claro</string>
|
||||
<string name="dark">Oscuro</string>
|
||||
@ -452,6 +454,8 @@
|
||||
<string name="automatically_show_url_preview">Vista previa de URL</string>
|
||||
<string name="automatically_hide_nav_bars">Desplazamiento inmersivo</string>
|
||||
<string name="automatically_hide_nav_bars_description">Ocultar barras de navegación al desplazarse</string>
|
||||
<string name="ui_style">Modo de interfaz</string>
|
||||
<string name="ui_style_description">Elegir el estilo de publicación</string>
|
||||
<string name="load_image">Cargar imagen</string>
|
||||
<string name="spamming_users">Spammers</string>
|
||||
<string name="muted_button">Silenciado. Hacer clic para reactivar el sonido.</string>
|
||||
|
@ -6,6 +6,7 @@
|
||||
<string name="your_profile_image">头像图片</string>
|
||||
<string name="scan_qr">扫描二维码</string>
|
||||
<string name="show_anyway">仍然显示</string>
|
||||
<string name="post_was_hidden">此帖被隐藏,因为它提到了你隐藏的用户或单词</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">帖文被标记为不当</string>
|
||||
<string name="post_not_found">事件正在加载或无法在你的中继列表中找到</string>
|
||||
<string name="channel_image">频道图片</string>
|
||||
@ -16,6 +17,7 @@
|
||||
<string name="spam">垃圾信息</string>
|
||||
<string name="impersonation">冒充</string>
|
||||
<string name="illegal_behavior">非法行为</string>
|
||||
<string name="other">其它</string>
|
||||
<string name="unknown">未知</string>
|
||||
<string name="relay_icon">中继器图标</string>
|
||||
<string name="unknown_author">未知作者</string>
|
||||
@ -23,6 +25,9 @@
|
||||
<string name="copy_user_pubkey">复制作者@npub</string>
|
||||
<string name="copy_note_id">复制笔记ID</string>
|
||||
<string name="broadcast">广播</string>
|
||||
<string name="timestamp_it">时间戳</string>
|
||||
<string name="timestamp_pending">时间戳:待确认</string>
|
||||
<string name="timestamp_pending_short">OTS:待定</string>
|
||||
<string name="request_deletion">请求删除</string>
|
||||
<string name="block_report">屏蔽/举报</string>
|
||||
<string name="block_hide_user"><![CDATA[阻止并隐藏用户]]></string>
|
||||
@ -33,7 +38,7 @@
|
||||
<string name="login_with_a_private_key_to_be_able_to_reply">你正在使用公钥,公钥是只读的。使用私钥登录以便回复</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_boost_posts">你正在使用公钥,公钥是只读的。使用私钥登录以便提升帖子</string>
|
||||
<string name="login_with_a_private_key_to_like_posts">使用私钥登录以便点赞帖子</string>
|
||||
<string name="no_zap_amount_setup_long_press_to_change">没有设置Zap金额。长按以更改</string>
|
||||
<string name="no_zap_amount_setup_long_press_to_change">没有设置打闪金额。长按以更改</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_send_zaps">你正在使用公钥,公钥是只读的。使用私钥登录以便发送打闪</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_follow">你正在使用公钥,公钥是只读的。使用私钥登录以便能够关注</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_unfollow">你正在使用公钥,公钥是只读的。使用私钥登录以便能够取消关注</string>
|
||||
@ -43,12 +48,16 @@
|
||||
<string name="view_count">浏览次数</string>
|
||||
<string name="boost">提升</string>
|
||||
<string name="boosted">已提升</string>
|
||||
<string name="edited">已编辑</string>
|
||||
<string name="edited_number">编辑 #%1$s</string>
|
||||
<string name="original">原版</string>
|
||||
<string name="quote">引用</string>
|
||||
<string name="propose_an_edit">提议编辑</string>
|
||||
<string name="new_amount_in_sats">新的聪金额</string>
|
||||
<string name="add">添加</string>
|
||||
<string name="replying_to">"回复 "</string>
|
||||
<string name="and">" 和 "</string>
|
||||
<string name="in_channel">" 在频道 "</string>
|
||||
<string name="in_channel">"在频道 "</string>
|
||||
<string name="profile_banner">个人档案横幅</string>
|
||||
<string name="payment_successful">付款成功</string>
|
||||
<string name="error_parsing_error_message">解析错误信息时出错</string>
|
||||
@ -63,7 +72,7 @@
|
||||
<string name="lightning_tips">闪电小费</string>
|
||||
<string name="note_to_receiver">给收款方的留言</string>
|
||||
<string name="thank_you_so_much">非常感谢!</string>
|
||||
<string name="amount_in_sats">聪数量</string>
|
||||
<string name="amount_in_sats">聪金额</string>
|
||||
<string name="send_sats">发送聪</string>
|
||||
<string name="error_parsing_preview_for">"解析 %1$s 预览出错:%2$s"</string>
|
||||
<string name="preview_card_image_for">"预览 %1$s 卡片图片"</string>
|
||||
@ -139,12 +148,15 @@
|
||||
<string name="clear">清除</string>
|
||||
<string name="app_logo">应用图标</string>
|
||||
<string name="nsec_npub_hex_private_key">nsec/ npub/ hex 私钥</string>
|
||||
<string name="ncryptsec_password">打开密钥的密码</string>
|
||||
<string name="show_password">显示密码</string>
|
||||
<string name="hide_password">隐藏密码</string>
|
||||
<string name="invalid_key">无效密钥</string>
|
||||
<string name="invalid_key_with_message">无效的密钥:%1$s</string>
|
||||
<string name="i_accept_the">"我接受"</string>
|
||||
<string name="terms_of_use">使用条款</string>
|
||||
<string name="acceptance_of_terms_is_required">需要接受条款</string>
|
||||
<string name="password_is_required">密码必填</string>
|
||||
<string name="key_is_required">需要密钥</string>
|
||||
<string name="name_is_required">名称必填</string>
|
||||
<string name="login">登录</string>
|
||||
@ -195,8 +207,19 @@
|
||||
<string name="mark_all_new_as_read">将所有新内容标记为已读</string>
|
||||
<string name="mark_all_as_read">将所有内容标记为已读</string>
|
||||
<string name="backup_keys">备份密匙</string>
|
||||
<string name="account_backup_tips2_md" tools:ignore="Typos">## 备份与安全提示
|
||||
\n\n你的帐户由一个私钥保护。私钥是以**nsec1**开头的随机字符串。任何拥有你的私钥的人都可以使用你的身份发布内容。
|
||||
\n\n- **不要**将你的私钥添加到任何你不信任的网站或软件,亦不要在网上公开。
|
||||
\n- Amethyst 开发人员**永远不会**要求你提供私钥。
|
||||
\n- **请**保留你的私钥的安全备份,以备帐户恢复。我们建议使用密码管理器。
|
||||
</string>
|
||||
<string name="account_backup_tips3_md" tools:ignore="Typos"> 为了额外的安全性,你可以用密码加密你的密钥。 此密钥以 **ncryptsec1** 开头,没有密码就不能使用。
|
||||
\n\n如果你丢失密码, 你将无法恢复你的密钥。
|
||||
</string>
|
||||
<string name="failed_to_encrypt_key">私钥加密失败</string>
|
||||
<string name="secret_key_copied_to_clipboard">私人密钥(nsec)已复制到剪贴板</string>
|
||||
<string name="copy_my_secret_key">复制我的私人密钥</string>
|
||||
<string name="encrypt_and_copy_my_secret_key">加密并复制我的密钥。</string>
|
||||
<string name="biometric_authentication_failed">身份验证失败</string>
|
||||
<string name="biometric_authentication_failed_explainer">生物计未能验证此手机的所有者</string>
|
||||
<string name="biometric_authentication_failed_explainer_with_error">生物计未能验证此手机的所有者。错误:%1$s</string>
|
||||
@ -417,6 +440,8 @@
|
||||
<string name="connectivity_type_always">始终</string>
|
||||
<string name="connectivity_type_wifi_only">仅限 WiFi</string>
|
||||
<string name="connectivity_type_never">从不</string>
|
||||
<string name="ui_feature_set_type_complete">完整版</string>
|
||||
<string name="ui_feature_set_type_simplified">简化版</string>
|
||||
<string name="system">系统</string>
|
||||
<string name="light">浅色</string>
|
||||
<string name="dark">深色</string>
|
||||
@ -428,6 +453,8 @@
|
||||
<string name="automatically_show_url_preview">URL 预览</string>
|
||||
<string name="automatically_hide_nav_bars">沉浸式滚动</string>
|
||||
<string name="automatically_hide_nav_bars_description">滚动时隐藏导航栏</string>
|
||||
<string name="ui_style">界面模式</string>
|
||||
<string name="ui_style_description">选择帖子样式</string>
|
||||
<string name="load_image">加载图像</string>
|
||||
<string name="spamming_users">垃圾邮件</string>
|
||||
<string name="muted_button">静音。点击取消静音</string>
|
||||
@ -634,4 +661,29 @@
|
||||
<string name="show_npub_as_a_qr_code">将 npub 显示为二维码</string>
|
||||
<string name="invalid_nip19_uri">地址无效</string>
|
||||
<string name="invalid_nip19_uri_description">Amethyst 收到了要打开的 URI,但该 uri 无效:%1$s</string>
|
||||
<string name="zap_the_devs_title">打闪开发人员!</string>
|
||||
<string name="zap_the_devs_description">你的捐赠帮助我们做出不同的贡献。每个聪都很重要!</string>
|
||||
<string name="donate_now">立即捐款</string>
|
||||
<string name="version_name">版本 %1$s</string>
|
||||
<string name="thank_you">谢谢!</string>
|
||||
<string name="max_limit">最大上限</string>
|
||||
<string name="forked_from"></string>
|
||||
<string name="git_web_address">网址</string>
|
||||
<string name="existed_since">OTS:%1$s</string>
|
||||
<string name="ots_info_title">时间戳证明</string>
|
||||
<string name="ots_info_description">%1$s之前的某个时候签署了此帖子的证明。此证明是在那个日期和时间在比特币区块链中盖章的。</string>
|
||||
<string name="edit_post">编辑帖子</string>
|
||||
<string name="proposal_to_edit">提议改进帖子</string>
|
||||
<string name="message_to_author">变动摘要</string>
|
||||
<string name="message_to_author_placeholder">快速修正…</string>
|
||||
<string name="accept_the_suggestion">接受建议</string>
|
||||
<string name="accessibility_download_for_offline">下载</string>
|
||||
<string name="accessibility_lyrics_on">开启歌词</string>
|
||||
<string name="accessibility_lyrics_off">关闭歌词</string>
|
||||
<string name="accessibility_turn_on_sealed_message">密封消息关闭。点击打开密封消息</string>
|
||||
<string name="accessibility_turn_off_sealed_message">密封消息开启。点击关闭密封消息</string>
|
||||
<string name="accessibility_send">发送</string>
|
||||
<string name="accessibility_play_username">作为音频播放用户名</string>
|
||||
<string name="accessibility_scan_qr_code">扫描二维码</string>
|
||||
<string name="accessibility_navigate_to_alby">导航到第三方钱包提供商 Alby</string>
|
||||
</resources>
|
||||
|
@ -6,6 +6,7 @@
|
||||
<string name="your_profile_image">頭像圖片</string>
|
||||
<string name="scan_qr">掃描 QR</string>
|
||||
<string name="show_anyway">一直顯示</string>
|
||||
<string name="post_was_hidden">此帖被隱藏,因爲它提到了你隱藏的用戶或單詞</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">貼文被標記為不當</string>
|
||||
<string name="post_not_found">事件正在加載或無法在你的中繼器列表中找到</string>
|
||||
<string name="channel_image">頻道圖片</string>
|
||||
@ -16,6 +17,7 @@
|
||||
<string name="spam">垃圾郵件</string>
|
||||
<string name="impersonation">冒充</string>
|
||||
<string name="illegal_behavior">非法行為</string>
|
||||
<string name="other">其它</string>
|
||||
<string name="unknown">未知</string>
|
||||
<string name="relay_icon">中繼器圖標</string>
|
||||
<string name="unknown_author">未知作者</string>
|
||||
@ -23,6 +25,9 @@
|
||||
<string name="copy_user_pubkey">複製作者@npub</string>
|
||||
<string name="copy_note_id">複製筆記ID</string>
|
||||
<string name="broadcast">廣播</string>
|
||||
<string name="timestamp_it">時間戳</string>
|
||||
<string name="timestamp_pending">時間戳:待確認</string>
|
||||
<string name="timestamp_pending_short">OTS:待定</string>
|
||||
<string name="request_deletion">請求刪除</string>
|
||||
<string name="block_report">屏蔽/舉報</string>
|
||||
<string name="block_hide_user"><![CDATA[阻止並隱藏用户]]></string>
|
||||
@ -33,7 +38,7 @@
|
||||
<string name="login_with_a_private_key_to_be_able_to_reply">你正在使用公鑰,公鑰是只讀的。使用私鑰登錄以便回覆</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_boost_posts">你正在使用公鑰,公鑰是只讀的。使用私鑰登錄以便提升貼文</string>
|
||||
<string name="login_with_a_private_key_to_like_posts">使用私鑰登錄以便點贊貼文</string>
|
||||
<string name="no_zap_amount_setup_long_press_to_change">沒有設置Zap金額。長按以更改</string>
|
||||
<string name="no_zap_amount_setup_long_press_to_change">沒有設置打閃金額。長按以更改</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_send_zaps">你正在使用公鑰,公鑰是只讀的。使用私鑰登錄以便發送打閃</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_follow">你正在使用公鑰,公鑰是只讀的。使用私鑰登錄以便關注</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_unfollow">你正在使用公鑰,公鑰是只讀的。使用私鑰登錄以便取關</string>
|
||||
@ -43,12 +48,16 @@
|
||||
<string name="view_count">瀏覽次數</string>
|
||||
<string name="boost">提升</string>
|
||||
<string name="boosted">已提升</string>
|
||||
<string name="edited">已編輯</string>
|
||||
<string name="edited_number">編輯 #%1$s</string>
|
||||
<string name="original">原版</string>
|
||||
<string name="quote">引用</string>
|
||||
<string name="new_amount_in_sats">新的金額 (sats)</string>
|
||||
<string name="propose_an_edit">提議編輯</string>
|
||||
<string name="new_amount_in_sats">新的聰金額</string>
|
||||
<string name="add">添加</string>
|
||||
<string name="replying_to">"回覆 "</string>
|
||||
<string name="and">" 和 "</string>
|
||||
<string name="in_channel">" 在頻道 "</string>
|
||||
<string name="in_channel">"在頻道 "</string>
|
||||
<string name="profile_banner">個人檔案橫幅</string>
|
||||
<string name="payment_successful">付款成功</string>
|
||||
<string name="error_parsing_error_message">錯誤解析錯誤消息</string>
|
||||
@ -58,13 +67,13 @@
|
||||
<string name="security_filters">安全過濾器</string>
|
||||
<string name="log_out">登出</string>
|
||||
<string name="show_more">顯示更多</string>
|
||||
<string name="lightning_invoice">閃電網絡發票</string>
|
||||
<string name="lightning_invoice">閃電發票</string>
|
||||
<string name="pay">付款</string>
|
||||
<string name="lightning_tips">閃電小費</string>
|
||||
<string name="note_to_receiver">寫給收款方的留言</string>
|
||||
<string name="thank_you_so_much">非常感謝!</string>
|
||||
<string name="amount_in_sats">數量 (sats)</string>
|
||||
<string name="send_sats">發送 sats</string>
|
||||
<string name="amount_in_sats">聰金額</string>
|
||||
<string name="send_sats">發送聰</string>
|
||||
<string name="error_parsing_preview_for">"解析 %1$s 預覽出錯:%2$s"</string>
|
||||
<string name="preview_card_image_for">"預覽 %1$s 卡片圖片"</string>
|
||||
<string name="new_channel">新頻道</string>
|
||||
@ -105,7 +114,7 @@
|
||||
<string name="failed_to_save_the_image">保存圖片失敗</string>
|
||||
<string name="upload_image">上傳圖片</string>
|
||||
<string name="uploading">上傳中…</string>
|
||||
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">用户尚未設置閃電地址以接收 sats</string>
|
||||
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">用户尚未設置閃電地址以接收聰</string>
|
||||
<string name="reply_here">"回覆這裡.."</string>
|
||||
<string name="copies_the_note_id_to_the_clipboard_for_sharing">複製筆記ID到剪貼板,供分享</string>
|
||||
<string name="copy_channel_id_note_to_the_clipboard">複製頻道ID(筆記)到剪貼板</string>
|
||||
@ -139,12 +148,15 @@
|
||||
<string name="clear">清除</string>
|
||||
<string name="app_logo">應用標誌</string>
|
||||
<string name="nsec_npub_hex_private_key">nsec/ npub/ hex 私鑰</string>
|
||||
<string name="ncryptsec_password">打開密鑰的密碼</string>
|
||||
<string name="show_password">顯示密碼</string>
|
||||
<string name="hide_password">隱藏密碼</string>
|
||||
<string name="invalid_key">無效密鑰</string>
|
||||
<string name="invalid_key_with_message">無效的密鑰:%1$s</string>
|
||||
<string name="i_accept_the">"我接受"</string>
|
||||
<string name="terms_of_use">使用條款</string>
|
||||
<string name="acceptance_of_terms_is_required">需要接受條款</string>
|
||||
<string name="password_is_required">密碼必填</string>
|
||||
<string name="key_is_required">需要密鑰</string>
|
||||
<string name="name_is_required">名稱為必填</string>
|
||||
<string name="login">登錄</string>
|
||||
@ -195,8 +207,19 @@
|
||||
<string name="mark_all_new_as_read">將所有新內容標記為已讀</string>
|
||||
<string name="mark_all_as_read">將所有內容標記為已讀</string>
|
||||
<string name="backup_keys">備份密鑰</string>
|
||||
<string name="account_backup_tips2_md" tools:ignore="Typos">## 備份與安全提示
|
||||
\n\n你的帳戶由一個私鑰保護。私鑰是以 **nsec1** 開頭的隨機字符串。任何擁有你的私鑰的人都可以使用你的身份發佈內容。
|
||||
\n\n- **不要**將你的私鑰添加到任何你不信任的網站或軟件,亦不要在網上公開。
|
||||
\n- Amethyst 開發人員**永遠不會**要求你提供私鑰。
|
||||
\n- **請**保留你的私鑰的安全備份,以備帳戶恢復。我們建議使用密碼管理器。
|
||||
</string>
|
||||
<string name="account_backup_tips3_md" tools:ignore="Typos"> 爲了額外的安全性,你可以用密碼加密你的密鑰。此密鑰以 **ncryptsec1** 開頭,沒有密碼就不能使用。
|
||||
\n\n如果你丟失密碼,你將無法恢復你的密鑰。
|
||||
</string>
|
||||
<string name="failed_to_encrypt_key">私鑰加密失敗</string>
|
||||
<string name="secret_key_copied_to_clipboard">私人密鑰(nsec)已複製到剪貼板</string>
|
||||
<string name="copy_my_secret_key">複製我的私人密鑰</string>
|
||||
<string name="encrypt_and_copy_my_secret_key">加密並複製我的密鑰</string>
|
||||
<string name="biometric_authentication_failed">身份驗證失敗</string>
|
||||
<string name="biometric_authentication_failed_explainer">生物計未能驗證此手機的所有者</string>
|
||||
<string name="biometric_authentication_failed_explainer_with_error">生物計未能驗證此手機的所有者。錯誤:%1$s</string>
|
||||
@ -417,6 +440,8 @@
|
||||
<string name="connectivity_type_always">始終</string>
|
||||
<string name="connectivity_type_wifi_only">僅限 WiFi</string>
|
||||
<string name="connectivity_type_never">永不</string>
|
||||
<string name="ui_feature_set_type_complete">完整版</string>
|
||||
<string name="ui_feature_set_type_simplified">簡化版</string>
|
||||
<string name="system">系統</string>
|
||||
<string name="light">淺色</string>
|
||||
<string name="dark">深色</string>
|
||||
@ -428,6 +453,8 @@
|
||||
<string name="automatically_show_url_preview">鏈接預覽</string>
|
||||
<string name="automatically_hide_nav_bars">沉浸式捲動</string>
|
||||
<string name="automatically_hide_nav_bars_description">捲動時隱藏導航欄</string>
|
||||
<string name="ui_style">界面模式</string>
|
||||
<string name="ui_style_description">選擇帖子樣式</string>
|
||||
<string name="load_image">加載圖像</string>
|
||||
<string name="spamming_users">垃圾郵件</string>
|
||||
<string name="muted_button">靜音。點擊取消靜音</string>
|
||||
@ -634,4 +661,28 @@
|
||||
<string name="show_npub_as_a_qr_code">將 npub 顯示爲二維碼</string>
|
||||
<string name="invalid_nip19_uri">地址無效</string>
|
||||
<string name="invalid_nip19_uri_description">Amethyst 收到了要打開的 URI,但該 URI 無效:%1$s</string>
|
||||
<string name="zap_the_devs_title">打閃開發人員</string>
|
||||
<string name="zap_the_devs_description">你的捐贈幫助我們做出不同的貢獻。 每個聰都很重要!</string>
|
||||
<string name="donate_now">立即捐款</string>
|
||||
<string name="version_name">版本 %1$s</string>
|
||||
<string name="thank_you">謝謝!</string>
|
||||
<string name="max_limit">最大上限</string>
|
||||
<string name="git_web_address">網址</string>
|
||||
<string name="existed_since">OTS:%1$s</string>
|
||||
<string name="ots_info_title">時間戳證明</string>
|
||||
<string name="ots_info_description">有在 %1$s 之前的某個時間簽署了此帖子的證明。此證明是在那個日期和時間在比特幣區塊鏈中記錄的時間戳。</string>
|
||||
<string name="edit_post">編輯帖子</string>
|
||||
<string name="proposal_to_edit">提議改進帖子</string>
|
||||
<string name="message_to_author">變動摘要</string>
|
||||
<string name="message_to_author_placeholder">快速修正…</string>
|
||||
<string name="accept_the_suggestion">接受提議</string>
|
||||
<string name="accessibility_download_for_offline">下載</string>
|
||||
<string name="accessibility_lyrics_on">開啓歌詞</string>
|
||||
<string name="accessibility_lyrics_off">關閉歌詞</string>
|
||||
<string name="accessibility_turn_on_sealed_message">密封消息關閉。點擊打開密封消息</string>
|
||||
<string name="accessibility_turn_off_sealed_message">密封消息關閉。點擊打開密封消息</string>
|
||||
<string name="accessibility_send">發送</string>
|
||||
<string name="accessibility_play_username">作爲音頻播放用戶名</string>
|
||||
<string name="accessibility_scan_qr_code">掃描二維碼</string>
|
||||
<string name="accessibility_navigate_to_alby">導航到第三方錢包提供商 Alby</string>
|
||||
</resources>
|
||||
|
@ -65,12 +65,14 @@ import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun TranslatableRichTextViewer(
|
||||
content: String,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
tags: ImmutableListOfLists<String>,
|
||||
backgroundColor: MutableState<Color>,
|
||||
@ -93,15 +95,16 @@ fun TranslatableRichTextViewer(
|
||||
|
||||
Crossfade(targetState = translatedTextState) {
|
||||
RenderText(
|
||||
it,
|
||||
content,
|
||||
canPreview,
|
||||
modifier,
|
||||
tags,
|
||||
backgroundColor,
|
||||
id,
|
||||
accountViewModel,
|
||||
nav,
|
||||
translatedTextState = it,
|
||||
content = content,
|
||||
canPreview = canPreview,
|
||||
quotesLeft = quotesLeft,
|
||||
modifier = modifier,
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
id = id,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -111,6 +114,7 @@ private fun RenderText(
|
||||
translatedTextState: TranslationConfig,
|
||||
content: String,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
modifier: Modifier,
|
||||
tags: ImmutableListOfLists<String>,
|
||||
backgroundColor: MutableState<Color>,
|
||||
@ -130,6 +134,7 @@ private fun RenderText(
|
||||
ExpandableRichTextViewer(
|
||||
toBeViewed,
|
||||
canPreview,
|
||||
quotesLeft,
|
||||
modifier,
|
||||
tags,
|
||||
backgroundColor,
|
||||
@ -363,7 +368,7 @@ fun TranslateAndWatchLanguageChanges(
|
||||
LaunchedEffect(accountState) {
|
||||
// This takes some time. Launches as a Composition scope to make sure this gets cancel if this
|
||||
// item gets out of view.
|
||||
launch(Dispatchers.IO) {
|
||||
withContext(Dispatchers.IO) {
|
||||
LanguageTranslatorService.autoTranslate(
|
||||
content,
|
||||
accountViewModel.account.dontTranslateFrom,
|
||||
|
@ -9,7 +9,11 @@ android {
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk 26
|
||||
targetSdk 34
|
||||
|
||||
// Enable measuring on an emulator, or devices with low battery
|
||||
testInstrumentationRunner 'androidx.benchmark.junit4.AndroidBenchmarkRunner'
|
||||
testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = "EMULATOR,LOW-BATTERY"
|
||||
}
|
||||
|
||||
@ -21,12 +25,7 @@ android {
|
||||
jvmTarget = '17'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk 26
|
||||
targetSdk 34
|
||||
|
||||
testInstrumentationRunner 'androidx.benchmark.junit4.AndroidBenchmarkRunner'
|
||||
}
|
||||
sourceSets.androidTest.assets.srcDirs += ["../quartz/src/androidTest/assets"]
|
||||
|
||||
testBuildType = "benchmark"
|
||||
buildTypes {
|
||||
@ -49,6 +48,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
androidTestImplementation platform(libs.androidx.compose.bom)
|
||||
androidTestImplementation libs.androidx.runner
|
||||
androidTestImplementation libs.androidx.junit
|
||||
androidTestImplementation libs.junit
|
||||
|
@ -25,57 +25,57 @@ import androidx.benchmark.junit4.measureRepeated
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.vitorpamplona.amethyst.commons.parts.accessory0Seven
|
||||
import com.vitorpamplona.amethyst.commons.parts.accessory1Nose
|
||||
import com.vitorpamplona.amethyst.commons.parts.accessory2HornRed
|
||||
import com.vitorpamplona.amethyst.commons.parts.accessory3Button
|
||||
import com.vitorpamplona.amethyst.commons.parts.accessory4Satellite
|
||||
import com.vitorpamplona.amethyst.commons.parts.accessory5Mustache
|
||||
import com.vitorpamplona.amethyst.commons.parts.accessory6Hat
|
||||
import com.vitorpamplona.amethyst.commons.parts.accessory7Antenna
|
||||
import com.vitorpamplona.amethyst.commons.parts.accessory8Brush
|
||||
import com.vitorpamplona.amethyst.commons.parts.accessory9Horn
|
||||
import com.vitorpamplona.amethyst.commons.parts.body0Trooper
|
||||
import com.vitorpamplona.amethyst.commons.parts.body1Thin
|
||||
import com.vitorpamplona.amethyst.commons.parts.body2Thinnest
|
||||
import com.vitorpamplona.amethyst.commons.parts.body3Front
|
||||
import com.vitorpamplona.amethyst.commons.parts.body4Round
|
||||
import com.vitorpamplona.amethyst.commons.parts.body5Neck
|
||||
import com.vitorpamplona.amethyst.commons.parts.body6IronMan
|
||||
import com.vitorpamplona.amethyst.commons.parts.body7NeckThinner
|
||||
import com.vitorpamplona.amethyst.commons.parts.body8Big
|
||||
import com.vitorpamplona.amethyst.commons.parts.body9Huge
|
||||
import com.vitorpamplona.amethyst.commons.parts.eyes0Squint
|
||||
import com.vitorpamplona.amethyst.commons.parts.eyes1Round
|
||||
import com.vitorpamplona.amethyst.commons.parts.eyes2Single
|
||||
import com.vitorpamplona.amethyst.commons.parts.eyes3Scott
|
||||
import com.vitorpamplona.amethyst.commons.parts.eyes4RoundSingle
|
||||
import com.vitorpamplona.amethyst.commons.parts.eyes5RoundSmall
|
||||
import com.vitorpamplona.amethyst.commons.parts.eyes6WallE
|
||||
import com.vitorpamplona.amethyst.commons.parts.eyes7Bar
|
||||
import com.vitorpamplona.amethyst.commons.parts.eyes8SmallBar
|
||||
import com.vitorpamplona.amethyst.commons.parts.eyes9Shield
|
||||
import com.vitorpamplona.amethyst.commons.parts.face0C3po
|
||||
import com.vitorpamplona.amethyst.commons.parts.face1Rock
|
||||
import com.vitorpamplona.amethyst.commons.parts.face2Long
|
||||
import com.vitorpamplona.amethyst.commons.parts.face3Oval
|
||||
import com.vitorpamplona.amethyst.commons.parts.face4Cylinder
|
||||
import com.vitorpamplona.amethyst.commons.parts.face5Baloon
|
||||
import com.vitorpamplona.amethyst.commons.parts.face6Triangle
|
||||
import com.vitorpamplona.amethyst.commons.parts.face7Bent
|
||||
import com.vitorpamplona.amethyst.commons.parts.face8TriangleInv
|
||||
import com.vitorpamplona.amethyst.commons.parts.face9Square
|
||||
import com.vitorpamplona.amethyst.commons.parts.mouth0Horz
|
||||
import com.vitorpamplona.amethyst.commons.parts.mouth1Cylinder
|
||||
import com.vitorpamplona.amethyst.commons.parts.mouth2Teeth
|
||||
import com.vitorpamplona.amethyst.commons.parts.mouth3Grid
|
||||
import com.vitorpamplona.amethyst.commons.parts.mouth4Vert
|
||||
import com.vitorpamplona.amethyst.commons.parts.mouth5MidOpen
|
||||
import com.vitorpamplona.amethyst.commons.parts.mouth6Cell
|
||||
import com.vitorpamplona.amethyst.commons.parts.mouth7Happy
|
||||
import com.vitorpamplona.amethyst.commons.parts.mouth8Buttons
|
||||
import com.vitorpamplona.amethyst.commons.parts.mouth9Closed
|
||||
import com.vitorpamplona.amethyst.commons.robohash.Black
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.accessory0Seven
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.accessory1Nose
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.accessory2HornRed
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.accessory3Button
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.accessory4Satellite
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.accessory5Mustache
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.accessory6Hat
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.accessory7Antenna
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.accessory8Brush
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.accessory9Horn
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.body0Trooper
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.body1Thin
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.body2Thinnest
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.body3Front
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.body4Round
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.body5Neck
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.body6IronMan
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.body7NeckThinner
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.body8Big
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.body9Huge
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.eyes0Squint
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.eyes1Round
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.eyes2Single
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.eyes3Scott
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.eyes4RoundSingle
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.eyes5RoundSmall
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.eyes6WallE
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.eyes7Bar
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.eyes8SmallBar
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.eyes9Shield
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.face0C3po
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.face1Rock
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.face2Long
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.face3Oval
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.face4Cylinder
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.face5Baloon
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.face6Triangle
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.face7Bent
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.face8TriangleInv
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.face9Square
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.mouth0Horz
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.mouth1Cylinder
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.mouth2Teeth
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.mouth3Grid
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.mouth4Vert
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.mouth5MidOpen
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.mouth6Cell
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.mouth7Happy
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.mouth8Buttons
|
||||
import com.vitorpamplona.amethyst.commons.robohash.parts.mouth9Closed
|
||||
import com.vitorpamplona.amethyst.commons.robohash.roboBuilder
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
@ -0,0 +1,397 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.benchmark
|
||||
|
||||
import androidx.benchmark.junit4.BenchmarkRule
|
||||
import androidx.benchmark.junit4.measureRepeated
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.amethyst.commons.data.LargeCache
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ConcurrentSkipListMap
|
||||
import java.util.function.BiConsumer
|
||||
import java.util.function.Consumer
|
||||
import java.util.zip.GZIPInputStream
|
||||
|
||||
open class BaseCacheBenchmark {
|
||||
fun getEventDB(): List<Event> {
|
||||
// This file includes duplicates
|
||||
val fullDBInputStream = getInstrumentation().context.assets.open("nostr_vitor_startup_data.json")
|
||||
|
||||
return Event.mapper.readValue<ArrayList<Event>>(
|
||||
GZIPInputStream(fullDBInputStream),
|
||||
)
|
||||
}
|
||||
|
||||
fun getConcurrentSkipList(db: List<Event>): ConcurrentSkipListMap<HexKey, Event> {
|
||||
val cache = ConcurrentSkipListMap<HexKey, Event>()
|
||||
|
||||
db.forEach {
|
||||
cache.put(it.id, it)
|
||||
}
|
||||
|
||||
return cache
|
||||
}
|
||||
|
||||
fun getConcurrentHashMap(db: List<Event>): ConcurrentHashMap<HexKey, Event> {
|
||||
val cache = ConcurrentHashMap<HexKey, Event>()
|
||||
|
||||
db.forEach {
|
||||
cache.put(it.id, it)
|
||||
}
|
||||
|
||||
return cache
|
||||
}
|
||||
|
||||
fun getRegularHashMap(db: List<Event>): HashMap<HexKey, Event> {
|
||||
val cache = HashMap<HexKey, Event>()
|
||||
|
||||
db.forEach {
|
||||
cache.put(it.id, it)
|
||||
}
|
||||
|
||||
return cache
|
||||
}
|
||||
|
||||
fun getLargeCache(db: List<Event>): LargeCache<HexKey, Event> {
|
||||
val cache = LargeCache<HexKey, Event>()
|
||||
|
||||
db.forEach { event ->
|
||||
cache.getOrCreate(event.id) {
|
||||
event
|
||||
}
|
||||
}
|
||||
|
||||
return cache
|
||||
}
|
||||
|
||||
fun hasId(event: Event) {
|
||||
assertTrue(event.id.isNotEmpty())
|
||||
}
|
||||
|
||||
val consumer =
|
||||
Consumer<Event> {
|
||||
hasId(it)
|
||||
}
|
||||
|
||||
val biconsumer =
|
||||
BiConsumer<HexKey, Event> { hex, event ->
|
||||
hasId(event)
|
||||
}
|
||||
}
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class CacheLoadingBenchmark : BaseCacheBenchmark() {
|
||||
@get:Rule val benchmarkRule = BenchmarkRule()
|
||||
|
||||
@Test
|
||||
fun loadConcurrentSkipList() {
|
||||
val db = getEventDB()
|
||||
benchmarkRule.measureRepeated { getConcurrentSkipList(db) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loadCountDuplicates() {
|
||||
val db = getEventDB().distinctBy { it.id }.toList()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loadConcurrentHashMap() {
|
||||
val db = getEventDB()
|
||||
benchmarkRule.measureRepeated { getConcurrentHashMap(db) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loadRegularHashMap() {
|
||||
val db = getEventDB()
|
||||
benchmarkRule.measureRepeated { getRegularHashMap(db) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loadLargeCache() {
|
||||
val db = getEventDB()
|
||||
benchmarkRule.measureRepeated { getLargeCache(db) }
|
||||
}
|
||||
}
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class CommonForEachBenchmark : BaseCacheBenchmark() {
|
||||
@get:Rule
|
||||
val benchmarkRule = BenchmarkRule()
|
||||
|
||||
@Test
|
||||
fun benchForEachRegularList() {
|
||||
val db = getEventDB().distinctBy { it.id }.toList()
|
||||
benchmarkRule.measureRepeated { db.forEach { hasId(it) } }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun benchForEachConsumerList() {
|
||||
val db = getEventDB().distinctBy { it.id }.toList()
|
||||
benchmarkRule.measureRepeated { db.forEach(consumer) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun forEachConcurrentSkipList() {
|
||||
val cache = getConcurrentSkipList(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.forEach { hasId(it.value) } }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun forEachConcurrentHashMap() {
|
||||
val cache = getConcurrentHashMap(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.forEach { hasId(it.value) } }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun forEachRegularHashMap() {
|
||||
val cache = getRegularHashMap(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.forEach { hasId(it.value) } }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun consumerForEachConcurrentSkipList() {
|
||||
val cache = getConcurrentSkipList(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.forEach(biconsumer) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun consumerForEachConcurrentHashMap() {
|
||||
val cache = getConcurrentHashMap(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.forEach(biconsumer) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun consumerForEachRegularHashMap() {
|
||||
val cache = getRegularHashMap(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.forEach(biconsumer) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun consumerForEachLargeCache() {
|
||||
val cache = getLargeCache(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.forEach(biconsumer) }
|
||||
}
|
||||
}
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class CommonMapBenchmark : BaseCacheBenchmark() {
|
||||
@get:Rule
|
||||
val benchmarkRule = BenchmarkRule()
|
||||
|
||||
@Test
|
||||
fun benchMapRegularList() {
|
||||
val db = getEventDB().distinctBy { it.id }.toList()
|
||||
benchmarkRule.measureRepeated { db.map { it.id } }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun consumerMapConcurrentSkipList() {
|
||||
val cache = getConcurrentSkipList(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.map { it.value.id } }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun consumerMapConcurrentHashMap() {
|
||||
val cache = getConcurrentHashMap(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.map { it.value.id } }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun consumerMapLargeCache() {
|
||||
val cache = getLargeCache(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.map { key, item -> item.id } }
|
||||
}
|
||||
}
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class BiggerForEachBenchmark : BaseCacheBenchmark() {
|
||||
@get:Rule val benchmarkRule = BenchmarkRule()
|
||||
|
||||
@Test
|
||||
fun benchForEachRegularList() {
|
||||
val db = getEventDB().distinctBy { it.id }.toList()
|
||||
benchmarkRule.measureRepeated { db.forEach { hasId(it) } }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun benchForEachConsumerList() {
|
||||
val db = getEventDB().distinctBy { it.id }.toList()
|
||||
benchmarkRule.measureRepeated { db.forEach(consumer) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun forEachConcurrentSkipList() {
|
||||
val cache = getConcurrentSkipList(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.forEach { hasId(it.value) } }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun forEachConcurrentHashMap() {
|
||||
val cache = getConcurrentHashMap(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.forEach { hasId(it.value) } }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun forEachRegularHashMap() {
|
||||
val cache = getRegularHashMap(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.forEach { hasId(it.value) } }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun consumerForEachConcurrentSkipList() {
|
||||
val cache = getConcurrentSkipList(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.forEach(biconsumer) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun consumerForEachConcurrentHashMap() {
|
||||
val cache = getConcurrentHashMap(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.forEach(biconsumer) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun consumerForEachRegularHashMap() {
|
||||
val cache = getRegularHashMap(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.forEach(biconsumer) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun consumerForEachLargeCache() {
|
||||
val cache = getLargeCache(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.forEach(biconsumer) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun valuesConcurrentSkipList() {
|
||||
val cache = getConcurrentSkipList(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.values.forEach { hasId(it) } }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun valuesConcurrentHashMap() {
|
||||
val cache = getConcurrentHashMap(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.values.forEach { hasId(it) } }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun valuesRegularHashMap() {
|
||||
val cache = getRegularHashMap(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.values.forEach { hasId(it) } }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun consumerValuesConcurrentSkipList() {
|
||||
val cache = getConcurrentSkipList(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.values.forEach(consumer) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun consumerValuesConcurrentHashMap() {
|
||||
val cache = getConcurrentHashMap(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.values.forEach(consumer) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun consumerValuesRegularHashMap() {
|
||||
val cache = getRegularHashMap(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.values.forEach(consumer) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun iterableConcurrentSkipList() {
|
||||
val cache = getConcurrentSkipList(getEventDB())
|
||||
benchmarkRule.measureRepeated {
|
||||
with(cache.iterator()) {
|
||||
while (hasNext()) {
|
||||
hasId(next().value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun iterableConcurrentHashMap() {
|
||||
val cache = getConcurrentHashMap(getEventDB())
|
||||
benchmarkRule.measureRepeated {
|
||||
with(cache.iterator()) {
|
||||
while (hasNext()) {
|
||||
hasId(next().value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun iterableRegularHashMap() {
|
||||
val cache = getRegularHashMap(getEventDB())
|
||||
benchmarkRule.measureRepeated {
|
||||
with(cache.iterator()) {
|
||||
while (hasNext()) {
|
||||
hasId(next().value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun iterableValuesConcurrentSkipList() {
|
||||
val cache = getConcurrentSkipList(getEventDB())
|
||||
benchmarkRule.measureRepeated {
|
||||
with(cache.values.iterator()) {
|
||||
while (hasNext()) {
|
||||
hasId(next())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun iterableValuesConcurrentHashMap() {
|
||||
val cache = getConcurrentHashMap(getEventDB())
|
||||
benchmarkRule.measureRepeated {
|
||||
with(cache.values.iterator()) {
|
||||
while (hasNext()) {
|
||||
hasId(next())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun iterableValuesRegularHashMap() {
|
||||
val cache = getRegularHashMap(getEventDB())
|
||||
benchmarkRule.measureRepeated {
|
||||
with(cache.values.iterator()) {
|
||||
while (hasNext()) {
|
||||
hasId(next())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,205 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.benchmark
|
||||
|
||||
import androidx.benchmark.junit4.BenchmarkRule
|
||||
import androidx.benchmark.junit4.measureRepeated
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.amethyst.commons.data.LargeCache
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.util.Arrays
|
||||
import java.util.function.Consumer
|
||||
import java.util.zip.GZIPInputStream
|
||||
|
||||
open class BaseLargeCacheBenchmark {
|
||||
fun getEventDB(): List<Event> {
|
||||
// This file includes duplicates
|
||||
val fullDBInputStream = getInstrumentation().context.assets.open("nostr_vitor_startup_data.json")
|
||||
|
||||
return Event.mapper.readValue<ArrayList<Event>>(
|
||||
GZIPInputStream(fullDBInputStream),
|
||||
)
|
||||
}
|
||||
|
||||
fun getLargeCache(db: List<Event>): LargeCache<HexKey, Event> {
|
||||
val cache = LargeCache<HexKey, Event>()
|
||||
|
||||
db.forEach {
|
||||
cache.getOrCreate(it.id) { key ->
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
return cache
|
||||
}
|
||||
|
||||
fun hasId(event: Event) {
|
||||
assertTrue(event.id.isNotEmpty())
|
||||
}
|
||||
|
||||
val consumer =
|
||||
Consumer<Event> {
|
||||
hasId(it)
|
||||
}
|
||||
}
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class LargeCacheForEachBenchmark : BaseLargeCacheBenchmark() {
|
||||
@get:Rule val benchmarkRule = BenchmarkRule()
|
||||
|
||||
// 191,353 ns 0 allocs Trace EMULATOR_LargeCacheForEachBenchmark.benchForEachConsumerList
|
||||
@Test
|
||||
fun benchForEachConsumerList() {
|
||||
val db = getEventDB().distinctBy { it.id }.toList()
|
||||
benchmarkRule.measureRepeated { db.forEach(consumer) }
|
||||
}
|
||||
|
||||
// 245,319 ns 1 allocs Trace EMULATOR_LargeCacheForEachBenchmark.benchForEachRegularList
|
||||
@Test
|
||||
fun benchForEachRegularList() {
|
||||
val db = getEventDB().distinctBy { it.id }.toList()
|
||||
benchmarkRule.measureRepeated { db.forEach { hasId(it) } }
|
||||
}
|
||||
|
||||
// 435,097 ns 1 allocs Trace EMULATOR_LargeCacheForEachBenchmark.forEach
|
||||
@Test
|
||||
fun forEach() {
|
||||
val cache = getLargeCache(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.forEach { key, it -> hasId(it) } }
|
||||
}
|
||||
|
||||
// 525,329 ns 18 allocs Trace EMULATOR_LargeCacheForEachBenchmark.filterKind1List
|
||||
@Test
|
||||
fun filterKind1List() {
|
||||
val cache = getLargeCache(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.filter { key, it -> it.kind == 1 } }
|
||||
}
|
||||
|
||||
// 690,323 ns 3581 allocs Trace EMULATOR_LargeCacheForEachBenchmark.filterKind1Set
|
||||
@Test
|
||||
fun filterKind1Set() {
|
||||
val cache = getLargeCache(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.filterIntoSet { key, it -> it.kind == 1 } }
|
||||
}
|
||||
|
||||
// 641,179 ns 23 allocs Trace EMULATOR_LargeCacheForEachBenchmark.mapToSigs
|
||||
@Test
|
||||
fun mapToSigs() {
|
||||
val cache = getLargeCache(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.map { key, it -> it.sig } }
|
||||
}
|
||||
|
||||
// 590,930 ns 23 allocs Trace EMULATOR_LargeCacheForEachBenchmark.mapNotNullTagList
|
||||
@Test
|
||||
fun mapNotNullTagList() {
|
||||
val cache = getLargeCache(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.mapNotNull { key, it -> it.tags.firstOrNull() } }
|
||||
}
|
||||
|
||||
// HashSet: 1,817,833 ns 30632 allocs Trace EMULATOR_LargeCacheForEachBenchmark.mapNotNullTagSet
|
||||
// LinkedHashSet: 2,057,674 ns 30633 allocs Trace EMULATOR_LargeCacheForEachBenchmark.mapNotNullTagSet
|
||||
@Test
|
||||
fun mapNotNullTagSet() {
|
||||
val cache = getLargeCache(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.mapNotNullIntoSet { key, it -> it.tags.firstOrNull() } }
|
||||
}
|
||||
|
||||
// 2,619,604 ns 93505 allocs Trace EMULATOR_LargeCacheForEachBenchmark.mapFlattenTagList
|
||||
@Test
|
||||
fun mapFlattenTagList() {
|
||||
val cache = getLargeCache(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.mapFlatten { key, it -> it.tags.asList() } }
|
||||
}
|
||||
|
||||
// ----
|
||||
|
||||
// 4,802,623 ns 114928 allocs Trace EMULATOR_LargeCacheForEachBenchmark.mapFlattenTagSetAsList
|
||||
@Test
|
||||
fun mapFlattenTagSetAsList() {
|
||||
val cache = getLargeCache(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.mapFlattenIntoSet { key, it -> it.tags.asList() } }
|
||||
}
|
||||
|
||||
// 5,695,432 ns 146089 allocs Trace EMULATOR_LargeCacheForEachBenchmark.mapFlattenTagSetArraysAsList
|
||||
@Test
|
||||
fun mapFlattenTagSetArraysAsList() {
|
||||
val cache = getLargeCache(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.mapFlattenIntoSet { key, it -> Arrays.asList(*it.tags) } }
|
||||
}
|
||||
|
||||
// 7,008,496 ns 176161 allocs Trace EMULATOR_LargeCacheForEachBenchmark.mapFlattenTagSetListOf
|
||||
@Test
|
||||
fun mapFlattenTagSetListOf() {
|
||||
val cache = getLargeCache(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.mapFlattenIntoSet { key, it -> listOf(*it.tags) } }
|
||||
}
|
||||
|
||||
// 7,032,714 ns 193834 allocs Trace EMULATOR_LargeCacheForEachBenchmark.mapFlattenTagSetToList
|
||||
@Test
|
||||
fun mapFlattenTagSetToList() {
|
||||
val cache = getLargeCache(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.mapFlattenIntoSet { key, it -> it.tags.toList() } }
|
||||
}
|
||||
|
||||
// ----
|
||||
|
||||
// 467,227 ns 1 allocs Trace EMULATOR_LargeCacheForEachBenchmark.sumOfKinds
|
||||
@Test
|
||||
fun sumOfKinds() {
|
||||
val cache = getLargeCache(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.sumOf { key, it -> it.kind } }
|
||||
}
|
||||
|
||||
// 458,998 ns 1 allocs Trace EMULATOR_LargeCacheForEachBenchmark.sumOfKindLong
|
||||
@Test
|
||||
fun sumOfKindLong() {
|
||||
val cache = getLargeCache(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.sumOfLong { key, it -> it.createdAt } }
|
||||
}
|
||||
|
||||
// 1,021,368 ns 11683 allocs Trace EMULATOR_LargeCacheForEachBenchmark.groupByKind
|
||||
@Test
|
||||
fun groupByKind() {
|
||||
val cache = getLargeCache(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.groupBy { key, it -> it.kind } }
|
||||
}
|
||||
|
||||
// 1,133,156 ns 39899 allocs Trace EMULATOR_LargeCacheForEachBenchmark.countByGroupKind
|
||||
@Test
|
||||
fun countByGroupKind() {
|
||||
val cache = getLargeCache(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.countByGroup { key, it -> it.kind } }
|
||||
}
|
||||
|
||||
// 428,641 ns 1 allocs Trace EMULATOR_LargeCacheForEachBenchmark.countNotEmptyTags
|
||||
@Test
|
||||
fun countNotEmptyTags() {
|
||||
val cache = getLargeCache(getEventDB())
|
||||
benchmarkRule.measureRepeated { cache.count { key, it -> it.tags.isNotEmpty() } }
|
||||
}
|
||||
}
|
@ -57,6 +57,7 @@ dependencies {
|
||||
api libs.kotlinx.collections.immutable
|
||||
|
||||
testImplementation libs.junit
|
||||
androidTestImplementation platform(libs.androidx.compose.bom)
|
||||
androidTestImplementation libs.androidx.junit
|
||||
androidTestImplementation libs.androidx.espresso.core
|
||||
}
|
@ -4036,7 +4036,7 @@ class RichTextParserTest {
|
||||
@Test
|
||||
fun testShortTextToParse() {
|
||||
val state =
|
||||
com.vitorpamplona.amethyst.commons.RichTextParser()
|
||||
RichTextParser()
|
||||
.parseText("Hi, how are you doing? ", EmptyTagList)
|
||||
org.junit.Assert.assertTrue(state.urlSet.isEmpty())
|
||||
org.junit.Assert.assertTrue(state.imagesForPager.isEmpty())
|
||||
@ -4051,8 +4051,7 @@ class RichTextParserTest {
|
||||
@Test
|
||||
fun testShortNewLinesTextToParse() {
|
||||
val state =
|
||||
com.vitorpamplona.amethyst.commons.RichTextParser()
|
||||
.parseText("\nHi, \nhow\n\n\n are you doing? \n", EmptyTagList)
|
||||
RichTextParser().parseText("\nHi, \nhow\n\n\n are you doing? \n", EmptyTagList)
|
||||
org.junit.Assert.assertTrue(state.urlSet.isEmpty())
|
||||
org.junit.Assert.assertTrue(state.imagesForPager.isEmpty())
|
||||
org.junit.Assert.assertTrue(state.imageList.isEmpty())
|
||||
@ -4076,7 +4075,7 @@ class RichTextParserTest {
|
||||
.trimIndent()
|
||||
|
||||
val state =
|
||||
com.vitorpamplona.amethyst.commons.RichTextParser()
|
||||
RichTextParser()
|
||||
.parseText(text, EmptyTagList)
|
||||
org.junit.Assert.assertEquals(
|
||||
"https://lnshort.it/live-stream-embeds/",
|
||||
@ -4153,7 +4152,7 @@ class RichTextParserTest {
|
||||
"That’s it ! That’s the #note https://cdn.nostr.build/i/1dc0726b6cb0f94a92bd66765ffb90f6c67e90c17bb957fc3d5d4782cbd73de7.jpg "
|
||||
|
||||
val state =
|
||||
com.vitorpamplona.amethyst.commons.RichTextParser()
|
||||
RichTextParser()
|
||||
.parseText(text, EmptyTagList)
|
||||
|
||||
printStateForDebug(state)
|
||||
@ -4186,7 +4185,7 @@ class RichTextParserTest {
|
||||
"That’s it! https://cdn.nostr.build/i/1dc0726b6cb0f94a92bd66765ffb90f6c67e90c17bb957fc3d5d4782cbd73de7.jpg That’s the #note"
|
||||
|
||||
val state =
|
||||
com.vitorpamplona.amethyst.commons.RichTextParser()
|
||||
RichTextParser()
|
||||
.parseText(text, EmptyTagList)
|
||||
|
||||
printStateForDebug(state)
|
||||
@ -4217,7 +4216,7 @@ class RichTextParserTest {
|
||||
val text = "That’s it! http://vitorpamplona.com/. That’s the note"
|
||||
|
||||
val state =
|
||||
com.vitorpamplona.amethyst.commons.RichTextParser()
|
||||
RichTextParser()
|
||||
.parseText(text, EmptyTagList)
|
||||
|
||||
printStateForDebug(state)
|
||||
@ -4281,7 +4280,7 @@ class RichTextParserTest {
|
||||
}
|
||||
}
|
||||
|
||||
private fun printStateForDebug(state: com.vitorpamplona.amethyst.commons.RichTextViewerState) {
|
||||
private fun printStateForDebug(state: RichTextViewerState) {
|
||||
state.paragraphs.forEach { paragraph ->
|
||||
paragraph.words.forEach { seg ->
|
||||
println(
|
||||
|
@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.compose
|
||||
|
||||
import android.util.LruCache
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.produceState
|
||||
|
||||
@Composable
|
||||
fun <K, V> produceCachedState(
|
||||
cache: CachedState<K, V>,
|
||||
key: K,
|
||||
): State<V?> {
|
||||
return produceState(initialValue = cache.cached(key), key1 = key) {
|
||||
value = cache.update(key)
|
||||
}
|
||||
}
|
||||
|
||||
interface CachedState<K, V> {
|
||||
fun cached(k: K): V?
|
||||
|
||||
suspend fun update(k: K): V?
|
||||
}
|
||||
|
||||
abstract class GenericBaseCache<K, V>(capacity: Int) : CachedState<K, V> {
|
||||
private val cache = LruCache<K, V>(capacity)
|
||||
|
||||
override fun cached(k: K): V? {
|
||||
return cache[k]
|
||||
}
|
||||
|
||||
override suspend fun update(k: K): V? {
|
||||
cache[k]?.let { return it }
|
||||
|
||||
val v = compute(k)
|
||||
|
||||
cache.put(k, v)
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
abstract suspend fun compute(key: K): V?
|
||||
}
|
@ -62,6 +62,12 @@ class LargeCache<K, V> {
|
||||
return runner.results
|
||||
}
|
||||
|
||||
fun <R> map(consumer: BiNotNullMapper<K, V, R>): List<R> {
|
||||
val runner = BiNotNullMapCollector(consumer)
|
||||
innerForEach(runner)
|
||||
return runner.results
|
||||
}
|
||||
|
||||
fun <R> mapNotNull(consumer: BiMapper<K, V, R?>): List<R> {
|
||||
val runner = BiMapCollector(consumer)
|
||||
innerForEach(runner)
|
||||
@ -86,12 +92,6 @@ class LargeCache<K, V> {
|
||||
return runner.results
|
||||
}
|
||||
|
||||
fun <R> map(consumer: BiNotNullMapper<K, V, R>): List<R> {
|
||||
val runner = BiNotNullMapCollector(consumer)
|
||||
innerForEach(runner)
|
||||
return runner.results
|
||||
}
|
||||
|
||||
fun sumOf(consumer: BiSumOf<K, V>): Int {
|
||||
val runner = BiSumOfCollector(consumer)
|
||||
innerForEach(runner)
|
||||
|
@ -1,7 +1,7 @@
|
||||
[versions]
|
||||
accompanistAdaptive = "0.34.0"
|
||||
activityCompose = "1.8.2"
|
||||
agp = "8.3.0"
|
||||
agp = "8.3.1"
|
||||
androidKotlinGeohash = "1.0"
|
||||
androidLifecycle = "2.7.0"
|
||||
androidxJunit = "1.2.0-alpha03"
|
||||
|
@ -57,7 +57,9 @@ dependencies {
|
||||
implementation 'net.java.dev.jna:jna:5.14.0@aar'
|
||||
|
||||
// Performant Parser of JSONs into Events
|
||||
api libs.jackson.module.kotlin
|
||||
api(libs.jackson.module.kotlin) {
|
||||
exclude(module: 'byte-buddy') // Workaround https://github.com/FasterXML/jackson-databind/issues/4428 until Jackson 2.17.1
|
||||
}
|
||||
|
||||
// immutable collections to avoid recomposition
|
||||
api libs.kotlinx.collections.immutable
|
||||
@ -66,6 +68,7 @@ dependencies {
|
||||
api libs.url.detector
|
||||
|
||||
testImplementation libs.junit
|
||||
androidTestImplementation platform(libs.androidx.compose.bom)
|
||||
androidTestImplementation libs.androidx.junit
|
||||
androidTestImplementation libs.androidx.espresso.core
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
# 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
|
||||
|
||||
# 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 ** { *; }
|
||||
|
||||
# preserve access to native classses
|
||||
-keep class fr.acinq.secp256k1.** { *; }
|
||||
|
||||
# JNA For Libsodium
|
||||
-keep class com.goterl.lazysodium.** { *; }
|
||||
|
||||
# libscrypt
|
||||
-keep class com.lambdaworks.codec.** { *; }
|
||||
-keep class com.lambdaworks.crypto.** { *; }
|
||||
-keep class com.lambdaworks.jni.** { *; }
|
||||
|
||||
# JNA also requires AWT, which Android does not have. So the classes are broken down to filter AWT out
|
||||
-keep class com.sun.jna.ToNativeConverter { *; }
|
||||
-keep class com.sun.jna.NativeMapped { *; }
|
||||
-keep class com.sun.jna.CallbackReference { *; }
|
||||
-keep class com.sun.jna.ptr.IntByReference { *; }
|
||||
-keep class com.sun.jna.NativeLong { *; }
|
||||
-keep class com.sun.jna.Structure { *; }
|
||||
-keep class com.sun.jna.Structure$* { *; }
|
||||
-keep class com.sun.jna.Native$ffi_callback { *; }
|
||||
-keep class * implements com.sun.jna.Structure$* { *; }
|
||||
-keep class * implements com.sun.jna.Native$* { *; }
|
||||
-keep class com.sun.jna.Native {
|
||||
private static com.sun.jna.NativeMapped fromNative(java.lang.Class, java.lang.Object);
|
||||
private static com.sun.jna.NativeMapped fromNative(java.lang.reflect.Method, java.lang.Object);
|
||||
private static java.lang.Class nativeType(java.lang.Class);
|
||||
private static java.lang.Object toNative(com.sun.jna.ToNativeConverter, java.lang.Object);
|
||||
private static java.lang.Object fromNative(com.sun.jna.FromNativeConverter, java.lang.Object, java.lang.reflect.Method);
|
||||
}
|
||||
|
||||
# JSON parsing
|
||||
-keep class com.vitorpamplona.quartz.crypto.** { *; }
|
||||
-keep class com.vitorpamplona.quartz.encoders.** { *; }
|
||||
-keep class com.vitorpamplona.quartz.events.** { *; }
|
||||
-keep class com.vitorpamplona.quartz.signers.** { *; }
|
||||
-keep class com.vitorpamplona.quartz.utils.** { *; }
|
||||
|
||||
-keep class com.vitorpamplona.amethyst.model.** { *; }
|
||||
-keep class com.vitorpamplona.amethyst.service.** { *; }
|
BIN
quartz/src/androidTest/assets/nostr_vitor_startup_data.json
Normal file
BIN
quartz/src/androidTest/assets/nostr_vitor_startup_data.json
Normal file
Binary file not shown.
@ -30,6 +30,7 @@ import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.io.InputStreamReader
|
||||
import java.util.zip.GZIPInputStream
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class LargeDBSignatureCheck {
|
||||
@ -49,6 +50,28 @@ class LargeDBSignatureCheck {
|
||||
counter++
|
||||
}
|
||||
|
||||
assertEquals(eventArray.size, counter)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun insertStartupDatabase() =
|
||||
runBlocking {
|
||||
// This file includes duplicates
|
||||
val fullDBInputStream = getInstrumentation().context.assets.open("nostr_vitor_startup_data.json")
|
||||
|
||||
val eventArray =
|
||||
Event.mapper.readValue<ArrayList<Event>>(
|
||||
GZIPInputStream(fullDBInputStream),
|
||||
) as List<Event>
|
||||
|
||||
var counter = 0
|
||||
eventArray.forEach {
|
||||
if (it.sig != "") {
|
||||
assertTrue(it.hasValidSignature())
|
||||
}
|
||||
counter++
|
||||
}
|
||||
|
||||
assertEquals(eventArray.size, counter)
|
||||
}
|
||||
}
|
||||
|
@ -54,6 +54,8 @@ open class Event(
|
||||
val content: String,
|
||||
val sig: HexKey,
|
||||
) : EventInterface {
|
||||
override fun isContentEncoded() = false
|
||||
|
||||
override fun countMemory(): Long {
|
||||
return 12L +
|
||||
id.bytesUsedInMemory() +
|
||||
|
@ -27,6 +27,8 @@ import java.math.BigDecimal
|
||||
|
||||
@Immutable
|
||||
interface EventInterface {
|
||||
fun isContentEncoded(): Boolean
|
||||
|
||||
fun countMemory(): Long
|
||||
|
||||
fun id(): HexKey
|
||||
|
@ -36,6 +36,8 @@ class FileStorageEvent(
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
fun type() = tags.firstOrNull { it.size > 1 && it[0] == TYPE }?.get(1)
|
||||
|
||||
fun decryptKey() = tags.firstOrNull { it.size > 2 && it[0] == DECRYPT }?.let { AESGCM(it[1], it[2]) }
|
||||
|
@ -42,6 +42,8 @@ abstract class GeneralListEvent(
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
@Transient private var privateTagsCache: Array<Array<String>>? = null
|
||||
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
fun category() = dTag()
|
||||
|
||||
fun bookmarkedPosts() = taggedEvents()
|
||||
|
@ -38,6 +38,8 @@ class GiftWrapEvent(
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
@Transient private var cachedInnerEvent: Map<HexKey, Event?> = mapOf()
|
||||
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
fun preCachedGift(signer: NostrSigner): Event? {
|
||||
return cachedInnerEvent[signer.pubKey]
|
||||
}
|
||||
|
@ -34,13 +34,13 @@ class HighlightEvent(
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseTextNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
fun inUrl() = taggedUrls().firstOrNull()
|
||||
fun inUrl() = firstTaggedUrl()
|
||||
|
||||
fun author() = taggedUsers().firstOrNull()
|
||||
fun author() = firstTaggedUser()
|
||||
|
||||
fun quote() = content
|
||||
|
||||
fun inPost() = taggedAddresses().firstOrNull()
|
||||
fun inPost() = firstTaggedAddress()
|
||||
|
||||
companion object {
|
||||
const val KIND = 9802
|
||||
|
@ -48,6 +48,8 @@ class OtsEvent(
|
||||
@Transient
|
||||
var verifiedTime: Long? = null
|
||||
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
fun digestEvent() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1)
|
||||
|
||||
fun digest() = digestEvent()?.hexToByteArray()
|
||||
|
@ -40,6 +40,8 @@ class PrivateDmEvent(
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig), ChatroomKeyable {
|
||||
@Transient private var decryptedContent: Map<HexKey, String> = mapOf()
|
||||
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
/**
|
||||
* This may or may not be the actual recipient's pub key. The event is intended to look like a
|
||||
* nip-04 EncryptedDmEvent but may omit the recipient, too. This value can be queried and used for
|
||||
|
@ -39,6 +39,8 @@ class SealedGossipEvent(
|
||||
) : WrappedEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
@Transient private var cachedInnerEvent: Map<HexKey, Event?> = mapOf()
|
||||
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
fun preCachedGossip(signer: NostrSigner): Event? {
|
||||
return cachedInnerEvent[signer.pubKey]
|
||||
}
|
||||
|
@ -37,12 +37,12 @@ class Nip30Test {
|
||||
|
||||
assertEquals(
|
||||
"Alex Gleason ",
|
||||
(result!![0] as Nip30CustomEmoji.TextType).text,
|
||||
(result[0] as Nip30CustomEmoji.TextType).text,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
"http://soapbox",
|
||||
(result!![1] as Nip30CustomEmoji.ImageUrlType).url,
|
||||
(result[1] as Nip30CustomEmoji.ImageUrlType).url,
|
||||
)
|
||||
}
|
||||
|
||||
@ -57,12 +57,12 @@ class Nip30Test {
|
||||
|
||||
assertEquals(
|
||||
"http://soapbox",
|
||||
(result!![0] as Nip30CustomEmoji.ImageUrlType).url,
|
||||
(result[0] as Nip30CustomEmoji.ImageUrlType).url,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
"Alex Gleason",
|
||||
(result!![1] as Nip30CustomEmoji.TextType).text,
|
||||
(result[1] as Nip30CustomEmoji.TextType).text,
|
||||
)
|
||||
}
|
||||
|
||||
@ -80,19 +80,19 @@ class Nip30Test {
|
||||
|
||||
assertEquals(7, result!!.size)
|
||||
|
||||
assertEquals("Hello ", (result!![0] as Nip30CustomEmoji.TextType).text)
|
||||
assertEquals("Hello ", (result[0] as Nip30CustomEmoji.TextType).text)
|
||||
|
||||
assertEquals("http://gleasonator", (result!![1] as Nip30CustomEmoji.ImageUrlType).url)
|
||||
assertEquals("http://gleasonator", (result[1] as Nip30CustomEmoji.ImageUrlType).url)
|
||||
|
||||
assertEquals(" 😂 ", (result!![2] as Nip30CustomEmoji.TextType).text)
|
||||
assertEquals(" 😂 ", (result[2] as Nip30CustomEmoji.TextType).text)
|
||||
|
||||
assertEquals("http://ablobcatrainbow", (result!![3] as Nip30CustomEmoji.ImageUrlType).url)
|
||||
assertEquals("http://ablobcatrainbow", (result[3] as Nip30CustomEmoji.ImageUrlType).url)
|
||||
|
||||
assertEquals(" ", (result!![4] as Nip30CustomEmoji.TextType).text)
|
||||
assertEquals(" ", (result[4] as Nip30CustomEmoji.TextType).text)
|
||||
|
||||
assertEquals("http://disputed", (result!![5] as Nip30CustomEmoji.ImageUrlType).url)
|
||||
assertEquals("http://disputed", (result[5] as Nip30CustomEmoji.ImageUrlType).url)
|
||||
|
||||
assertEquals(" yolo", (result!![6] as Nip30CustomEmoji.TextType).text)
|
||||
assertEquals(" yolo", (result[6] as Nip30CustomEmoji.TextType).text)
|
||||
}
|
||||
|
||||
@Test()
|
||||
@ -114,11 +114,11 @@ class Nip30Test {
|
||||
|
||||
assertEquals(3, result!!.size)
|
||||
|
||||
assertEquals("hello ", (result!![0] as Nip30CustomEmoji.TextType).text)
|
||||
assertEquals("hello ", (result[0] as Nip30CustomEmoji.TextType).text)
|
||||
|
||||
assertEquals("http://vitor", (result!![1] as Nip30CustomEmoji.ImageUrlType).url)
|
||||
assertEquals("http://vitor", (result[1] as Nip30CustomEmoji.ImageUrlType).url)
|
||||
|
||||
assertEquals(" how :can I help:", (result!![2] as Nip30CustomEmoji.TextType).text)
|
||||
assertEquals(" how :can I help:", (result[2] as Nip30CustomEmoji.TextType).text)
|
||||
}
|
||||
|
||||
@Test()
|
||||
@ -130,9 +130,9 @@ class Nip30Test {
|
||||
|
||||
assertEquals(3, result!!.size)
|
||||
|
||||
assertEquals("\uD883\uDEDE\uD883\uDEDE麺の", (result!![0] as Nip30CustomEmoji.TextType).text)
|
||||
assertEquals("http://x30EDE", (result!![1] as Nip30CustomEmoji.ImageUrlType).url)
|
||||
assertEquals("。:\uD883\uDEDE:(Violation of NIP-30)", (result!![2] as Nip30CustomEmoji.TextType).text)
|
||||
assertEquals("\uD883\uDEDE\uD883\uDEDE麺の", (result[0] as Nip30CustomEmoji.TextType).text)
|
||||
assertEquals("http://x30EDE", (result[1] as Nip30CustomEmoji.ImageUrlType).url)
|
||||
assertEquals("。:\uD883\uDEDE:(Violation of NIP-30)", (result[2] as Nip30CustomEmoji.TextType).text)
|
||||
}
|
||||
|
||||
@Test()
|
||||
@ -155,14 +155,14 @@ class Nip30Test {
|
||||
assertEquals(9, result!!.size)
|
||||
|
||||
var i = 0
|
||||
assertEquals("\u200B", (result!![i++] as Nip30CustomEmoji.TextType).text)
|
||||
assertEquals("https://media.misskeyusercontent.com/emoji/_ri.png", (result!![i++] as Nip30CustomEmoji.ImageUrlType).url)
|
||||
assertEquals("\u200B\u200B", (result!![i++] as Nip30CustomEmoji.TextType).text)
|
||||
assertEquals("https://media.misskeyusercontent.com/emoji/_ri.png", (result!![i++] as Nip30CustomEmoji.ImageUrlType).url)
|
||||
assertEquals("\u200Bはベイクドモチョチョ\u200B", (result!![i++] as Nip30CustomEmoji.TextType).text)
|
||||
assertEquals("https://media.misskeyusercontent.com/emoji/petthex_japanesecake.gif", (result!![i++] as Nip30CustomEmoji.ImageUrlType).url)
|
||||
assertEquals("\u200Bを食べました\u200B", (result!![i++] as Nip30CustomEmoji.TextType).text)
|
||||
assertEquals("https://media.misskeyusercontent.com/misskey/f6294900-f678-43cc-bc36-3ee5deeca4c2.gif", (result!![i++] as Nip30CustomEmoji.ImageUrlType).url)
|
||||
assertEquals("\u200B\n#ioメシヨソイゲーム\nhttps://misskey.io/play/9g3qza4jow", (result!![i++] as Nip30CustomEmoji.TextType).text)
|
||||
assertEquals("\u200B", (result[i++] as Nip30CustomEmoji.TextType).text)
|
||||
assertEquals("https://media.misskeyusercontent.com/emoji/_ri.png", (result[i++] as Nip30CustomEmoji.ImageUrlType).url)
|
||||
assertEquals("\u200B\u200B", (result[i++] as Nip30CustomEmoji.TextType).text)
|
||||
assertEquals("https://media.misskeyusercontent.com/emoji/_ri.png", (result[i++] as Nip30CustomEmoji.ImageUrlType).url)
|
||||
assertEquals("\u200Bはベイクドモチョチョ\u200B", (result[i++] as Nip30CustomEmoji.TextType).text)
|
||||
assertEquals("https://media.misskeyusercontent.com/emoji/petthex_japanesecake.gif", (result[i++] as Nip30CustomEmoji.ImageUrlType).url)
|
||||
assertEquals("\u200Bを食べました\u200B", (result[i++] as Nip30CustomEmoji.TextType).text)
|
||||
assertEquals("https://media.misskeyusercontent.com/misskey/f6294900-f678-43cc-bc36-3ee5deeca4c2.gif", (result[i++] as Nip30CustomEmoji.ImageUrlType).url)
|
||||
assertEquals("\u200B\n#ioメシヨソイゲーム\nhttps://misskey.io/play/9g3qza4jow", (result[i] as Nip30CustomEmoji.TextType).text)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user