Merge branch 'main' into main

This commit is contained in:
greenart7c3 2024-03-20 07:06:24 -03:00 committed by GitHub
commit 499939ed68
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
82 changed files with 1785 additions and 1042 deletions

View File

@ -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
}

View File

@ -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"))
}
}

View File

@ -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,

View File

@ -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()

View File

@ -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
}
}

View File

@ -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? {

View File

@ -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
}
}
}

View File

@ -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"),
)

View File

@ -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)

View File

@ -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)

View File

@ -65,6 +65,7 @@ fun NotifyRequestDialog(
TranslatableRichTextViewer(
textContent,
canPreview = true,
quotesLeft = 1,
Modifier.fillMaxWidth(),
EmptyTagList,
background,

View File

@ -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}") },

View File

@ -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,

View File

@ -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) }

View File

@ -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) }) }

View File

@ -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,
)
}
}
}

View File

@ -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,
) {

View File

@ -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,

View File

@ -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))
}
}
}

View File

@ -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,

View File

@ -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,
)
}
}

View File

@ -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,

View File

@ -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)
}
}

View File

@ -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,
)
}
}

View File

@ -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

View File

@ -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))
// }
}
}

View File

@ -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,
)
}
}

View File

@ -232,6 +232,7 @@ fun RenderAppDefinition(
TranslatableRichTextViewer(
content = it,
canPreview = false,
quotesLeft = 1,
tags = tags,
backgroundColor = backgroundColor,
id = note.idHex,

View File

@ -216,6 +216,7 @@ fun AudioHeader(
TranslatableRichTextViewer(
content = it,
canPreview = true,
quotesLeft = 1,
tags = tags,
backgroundColor = background,
id = note.idHex,

View File

@ -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)
}
}

View File

@ -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,

View File

@ -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 =

View File

@ -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),
)

View File

@ -105,6 +105,7 @@ fun RenderPinListEvent(
TranslatableRichTextViewer(
content = pin,
canPreview = true,
quotesLeft = 1,
tags = EmptyTagList,
backgroundColor = backgroundColor,
id = baseNote.idHex,

View File

@ -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,

View File

@ -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,
)
}
}

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -163,6 +163,7 @@ fun VideoDisplay(
TranslatableRichTextViewer(
content = it,
canPreview = canPreview && !makeItShort,
quotesLeft = 1,
modifier = Modifier.fillMaxWidth(),
tags = tags,
backgroundColor = backgroundColor,

View File

@ -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,

View File

@ -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,
)
}
}
}

View File

@ -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)

View File

@ -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,

View File

@ -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)
}
}
}
}

View File

@ -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,

View File

@ -1103,6 +1103,7 @@ private fun DrawAdditionalInfo(
TranslatableRichTextViewer(
content = it,
canPreview = false,
quotesLeft = 1,
tags = EmptyTagList,
backgroundColor = background,
id = it,

View File

@ -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,
)
}
}
}

View File

@ -303,7 +303,6 @@ fun RenderReportState(
state.isHiddenAuthor,
accountViewModel,
Modifier.fillMaxWidth(),
false,
nav,
onClick = { showReportedNote = true },
)

View File

@ -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(

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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())
}
}
}
}
}

View File

@ -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() } }
}
}

View File

@ -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
}

View File

@ -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 {
"Thats it ! Thats 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 {
"Thats it! https://cdn.nostr.build/i/1dc0726b6cb0f94a92bd66765ffb90f6c67e90c17bb957fc3d5d4782cbd73de7.jpg Thats the #note"
val state =
com.vitorpamplona.amethyst.commons.RichTextParser()
RichTextParser()
.parseText(text, EmptyTagList)
printStateForDebug(state)
@ -4217,7 +4216,7 @@ class RichTextParserTest {
val text = "Thats it! http://vitorpamplona.com/. Thats 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(

View File

@ -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?
}

View File

@ -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)

View File

@ -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"

View File

@ -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
}

View File

@ -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.** { *; }

View File

@ -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)
}
}

View File

@ -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() +

View File

@ -27,6 +27,8 @@ import java.math.BigDecimal
@Immutable
interface EventInterface {
fun isContentEncoded(): Boolean
fun countMemory(): Long
fun id(): HexKey

View File

@ -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]) }

View File

@ -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()

View File

@ -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]
}

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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]
}

View File

@ -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)
}
}