From c5133e98f85a0e952ddf1f4847ddcefacc79798c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 29 Dec 2025 18:57:56 -0500 Subject: [PATCH 001/137] - Fixes Preview Tag - Fixes Email Pattern --- commons/build.gradle.kts | 1 + .../vitorpamplona/amethyst/commons/preview/BlurhashTest.kt | 6 +++--- .../amethyst/commons/richtext/RichTextParserTest.kt | 3 ++- .../com/vitorpamplona/amethyst/commons/hashtags/Amethyst.kt | 2 ++ .../com/vitorpamplona/amethyst/commons/hashtags/Btc.kt | 2 ++ .../com/vitorpamplona/amethyst/commons/hashtags/Cashu.kt | 2 ++ .../com/vitorpamplona/amethyst/commons/hashtags/Coffee.kt | 2 ++ .../vitorpamplona/amethyst/commons/hashtags/Flowerstr.kt | 2 ++ .../com/vitorpamplona/amethyst/commons/hashtags/Footstr.kt | 2 ++ .../com/vitorpamplona/amethyst/commons/hashtags/Gamestr.kt | 2 ++ .../vitorpamplona/amethyst/commons/hashtags/Grownostr.kt | 2 ++ .../vitorpamplona/amethyst/commons/hashtags/Lightning.kt | 2 ++ .../com/vitorpamplona/amethyst/commons/hashtags/Mate.kt | 2 ++ .../com/vitorpamplona/amethyst/commons/hashtags/Nostr.kt | 2 ++ .../com/vitorpamplona/amethyst/commons/hashtags/Plebs.kt | 2 ++ .../com/vitorpamplona/amethyst/commons/hashtags/Skull.kt | 2 ++ .../com/vitorpamplona/amethyst/commons/hashtags/Tunestr.kt | 2 ++ .../com/vitorpamplona/amethyst/commons/hashtags/Weed.kt | 2 ++ .../com/vitorpamplona/amethyst/commons/hashtags/Zap.kt | 2 ++ .../com/vitorpamplona/amethyst/commons/icons/Following.kt | 2 ++ .../kotlin/com/vitorpamplona/amethyst/commons/icons/Like.kt | 2 ++ .../com/vitorpamplona/amethyst/commons/icons/Liked.kt | 2 ++ .../com/vitorpamplona/amethyst/commons/icons/Reply.kt | 2 ++ .../com/vitorpamplona/amethyst/commons/icons/Repost.kt | 2 ++ .../com/vitorpamplona/amethyst/commons/icons/Reposted.kt | 2 ++ .../com/vitorpamplona/amethyst/commons/icons/Search.kt | 2 ++ .../com/vitorpamplona/amethyst/commons/icons/Share.kt | 2 ++ .../kotlin/com/vitorpamplona/amethyst/commons/icons/Zap.kt | 2 ++ .../com/vitorpamplona/amethyst/commons/icons/ZapSplit.kt | 2 ++ .../amethyst/commons/robohash/RobohashAssembler.kt | 2 ++ .../amethyst/commons/robohash/parts/Accessory0Seven.kt | 6 ++++++ .../amethyst/commons/robohash/parts/Accessory1Nose.kt | 2 ++ .../amethyst/commons/robohash/parts/Accessory2HornRed.kt | 2 ++ .../amethyst/commons/robohash/parts/Accessory3Button.kt | 2 ++ .../amethyst/commons/robohash/parts/Accessory4Satellite.kt | 2 ++ .../amethyst/commons/robohash/parts/Accessory5Mustache.kt | 2 ++ .../amethyst/commons/robohash/parts/Body0Trooper.kt | 2 ++ .../amethyst/commons/robohash/parts/Body1Thin.kt | 2 ++ .../amethyst/commons/robohash/parts/Body2Thinnest.kt | 2 ++ .../amethyst/commons/robohash/parts/Body3Front.kt | 2 ++ .../amethyst/commons/robohash/parts/Body4Round.kt | 2 ++ .../amethyst/commons/robohash/parts/Body5Neck.kt | 2 ++ .../amethyst/commons/robohash/parts/Body6Ironman.kt | 2 ++ .../amethyst/commons/robohash/parts/Body7Neckthinner.kt | 2 ++ .../amethyst/commons/robohash/parts/Body8Big.kt | 2 ++ .../amethyst/commons/robohash/parts/Body9Huge.kt | 2 ++ .../com/vitorpamplona/amethyst/commons/richtext/Patterns.kt | 4 ++-- 47 files changed, 98 insertions(+), 6 deletions(-) diff --git a/commons/build.gradle.kts b/commons/build.gradle.kts index e66de925e..095072fe3 100644 --- a/commons/build.gradle.kts +++ b/commons/build.gradle.kts @@ -64,6 +64,7 @@ kotlin { implementation(compose.runtime) implementation(compose.material3) implementation(compose.materialIconsExtended) + implementation(compose.components.uiToolingPreview) // LruCache (KMP-ready) implementation(libs.androidx.collection) diff --git a/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/preview/BlurhashTest.kt b/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/preview/BlurhashTest.kt index d0f9614e7..0c8e5cc73 100644 --- a/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/preview/BlurhashTest.kt +++ b/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/preview/BlurhashTest.kt @@ -50,7 +50,7 @@ class BlurhashTest { val bmp1 = BlurHashDecoderOld.decode(warmHex, 100, (100 * (1 / aspectRatio)).roundToInt()) val bmp2 = BlurHashDecoder.decodeKeepAspectRatio(warmHex, 100) - assertTrue(bmp1!!.sameAs(bmp2!!)) + assertTrue(bmp1!!.sameAs(bmp2!!.bitmap)) } @Test @@ -60,7 +60,7 @@ class BlurhashTest { val bmp1 = BlurHashDecoderOld.decode(warmHex, 25, (25 * (1 / aspectRatio)).roundToInt()) val bmp2 = BlurHashDecoder.decodeKeepAspectRatio(warmHex, 25) - assertTrue(bmp1!!.sameAs(bmp2!!)) + assertTrue(bmp1!!.sameAs(bmp2!!.bitmap)) } @Test @@ -75,7 +75,7 @@ class BlurhashTest { val bmp1 = BlurHashDecoderOld.decode(testHex, 100, (100 * (1 / aspectRatio)).roundToInt()) val bmp2 = BlurHashDecoder.decodeKeepAspectRatio(testHex, 100) - assertTrue(bmp1!!.sameAs(bmp2!!)) + assertTrue(bmp1!!.sameAs(bmp2!!.bitmap)) } @Test diff --git a/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParserTest.kt b/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParserTest.kt index af10eface..2bf2ea3f6 100644 --- a/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParserTest.kt +++ b/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParserTest.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.commons.richtext import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists +import junit.framework.TestCase.assertEquals import org.junit.Test import org.junit.runner.RunWith @@ -688,7 +689,7 @@ class RichTextParserTest { val state = RichTextParser() .parseText(textToParse, EmptyTagList, null) - org.junit.Assert.assertEquals( + assertEquals( "relay.shitforce.one, relayable.org, universe.nostrich.land, nos.lol, universe.nostrich.land?lang=zh, universe.nostrich.land?lang=en, relay.damus.io, relay.nostr.wirednet.jp, offchain.pub, nostr.rocks, relay.wellorder.net, nostr.oxtr.dev, universe.nostrich.land?lang=ja, relay.mostr.pub, nostr.bitcoiner.social, Nostr-Check.com, MR.Rabbit, Ancap.su, ⚡\uFE0Fsatscoinsv@getalby.com, miceliomad@miceliomad.github.io/nostr/, zapper.lol, smies.me, baller.hodl", state.urlSet.joinToString(", "), ) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Amethyst.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Amethyst.kt index c47beb329..e5a58bb75 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Amethyst.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Amethyst.kt @@ -33,7 +33,9 @@ import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun CustomHashTagIconsAmethystPreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Btc.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Btc.kt index 1bb9aef79..42171fb11 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Btc.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Btc.kt @@ -34,7 +34,9 @@ import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun CustomHashTagIconsBtcPreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Cashu.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Cashu.kt index 96be5b124..0cdf87955 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Cashu.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Cashu.kt @@ -32,7 +32,9 @@ import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun CustomHashTagIconsCashuPreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Coffee.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Coffee.kt index 6c2543cd2..4ae21b2b3 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Coffee.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Coffee.kt @@ -32,7 +32,9 @@ import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun CustomHashTagIconsCoffeePreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Flowerstr.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Flowerstr.kt index d8a9ba332..a435b0450 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Flowerstr.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Flowerstr.kt @@ -32,7 +32,9 @@ import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun CustomHashTagIconsFlowerstrPreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Footstr.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Footstr.kt index 70111c982..2d5c0cda6 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Footstr.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Footstr.kt @@ -32,7 +32,9 @@ import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun CustomHashTagIconsFootstrPreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Gamestr.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Gamestr.kt index 9a07e47ae..1a41080b1 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Gamestr.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Gamestr.kt @@ -29,7 +29,9 @@ import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun CustomHashTagIconsGamestrPreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Grownostr.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Grownostr.kt index afec134fe..be21273e8 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Grownostr.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Grownostr.kt @@ -32,7 +32,9 @@ import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun CustomHashTagIconsGrownostrPreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Lightning.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Lightning.kt index 3b884cc17..40060fd8d 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Lightning.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Lightning.kt @@ -32,7 +32,9 @@ import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun CustomHashTagIconsLightningPreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Mate.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Mate.kt index 037b8a7ec..8aa010b16 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Mate.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Mate.kt @@ -32,7 +32,9 @@ import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun CustomHashTagIconsMatePreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Nostr.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Nostr.kt index 24160dde7..6e20b3a34 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Nostr.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Nostr.kt @@ -32,7 +32,9 @@ import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun CustomHashTagIconsNostrPreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Plebs.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Plebs.kt index d1a17353f..cd3231214 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Plebs.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Plebs.kt @@ -32,7 +32,9 @@ import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun CustomHashTagIconsPlebsPreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Skull.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Skull.kt index 8e0a23216..1ae1a8764 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Skull.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Skull.kt @@ -32,7 +32,9 @@ import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun CustomHashTagIconsSkullPreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Tunestr.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Tunestr.kt index 2d307df19..03b2a0098 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Tunestr.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Tunestr.kt @@ -32,7 +32,9 @@ import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun CustomHashTagIconsTunestrPreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Weed.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Weed.kt index 668f43b67..21a30eaa8 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Weed.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Weed.kt @@ -32,7 +32,9 @@ import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun CustomHashTagIconsWeedPreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Zap.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Zap.kt index eec30fc12..d76add7ea 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Zap.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Zap.kt @@ -32,7 +32,9 @@ import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun CustomHashTagIconsZapPreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Following.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Following.kt index fe0b9cfc8..659d6fa39 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Following.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Following.kt @@ -30,7 +30,9 @@ import androidx.compose.ui.graphics.StrokeJoin import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable private fun VectorPreview() { Image(Following, null) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Like.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Like.kt index 994b6db33..98f7a144a 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Like.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Like.kt @@ -31,7 +31,9 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable private fun VectorPreview() { Image(Like, null) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Liked.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Liked.kt index 8e89cc135..d5c8b8a8b 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Liked.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Liked.kt @@ -31,7 +31,9 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable private fun VectorPreview() { Image(Liked, null) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Reply.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Reply.kt index 31349a88b..c4d06b2f2 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Reply.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Reply.kt @@ -31,7 +31,9 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable private fun VectorPreview() { Image(Reply, null) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Repost.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Repost.kt index ffbb79b22..23c452b03 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Repost.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Repost.kt @@ -31,7 +31,9 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable private fun VectorPreview() { Image(Repost, null) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Reposted.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Reposted.kt index 2cad31d4a..6ce0a9672 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Reposted.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Reposted.kt @@ -31,7 +31,9 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable private fun VectorPreview() { Image(Reposted, null) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Search.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Search.kt index e967053d2..87cdcd700 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Search.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Search.kt @@ -31,7 +31,9 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable private fun VectorPreview() { Image(Search, null) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Share.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Share.kt index 49923f688..5056c88d7 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Share.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Share.kt @@ -31,7 +31,9 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable private fun VectorPreview() { Image(Share, null) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Zap.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Zap.kt index d5169185f..62924cf26 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Zap.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Zap.kt @@ -31,7 +31,9 @@ import androidx.compose.ui.graphics.vector.DefaultFillType import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.PathBuilder import androidx.compose.ui.graphics.vector.path +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable private fun VectorPreview() { Image(Zap, null) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/ZapSplit.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/ZapSplit.kt index 91a111e19..a88a8ae36 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/ZapSplit.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/ZapSplit.kt @@ -32,7 +32,9 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.PathBuilder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable private fun VectorPreview() { Image(ZapSplit, null) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/RobohashAssembler.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/RobohashAssembler.kt index 982196e72..ba5a13bf0 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/RobohashAssembler.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/RobohashAssembler.kt @@ -86,6 +86,7 @@ import com.vitorpamplona.amethyst.commons.robohash.parts.mouth9Closed import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.sha256.sha256 +import org.jetbrains.compose.ui.tooling.preview.Preview val Black = SolidColor(Color.Black) val Gray = SolidColor(Color(0xFF6d6e70)) @@ -106,6 +107,7 @@ val MediumGray = SolidColor(Color(0xFFd0d2d3)) val DefaultSize = 55.dp const val VIEWPORT_SIZE = 300f +@Preview @Composable fun RobohashPreview() { val assembler = RobohashAssembler() diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory0Seven.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory0Seven.kt index e58577de5..37e1faa37 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory0Seven.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory0Seven.kt @@ -21,18 +21,24 @@ package com.vitorpamplona.amethyst.commons.robohash.parts import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.PathData import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.roboBuilder +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun Accessory0SevenPreview() { Image( + modifier = Modifier.size(10000.dp), painter = rememberVectorPainter( roboBuilder { diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory1Nose.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory1Nose.kt index b467f1c61..b650de0bc 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory1Nose.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory1Nose.kt @@ -29,7 +29,9 @@ import androidx.compose.ui.graphics.vector.PathData import androidx.compose.ui.graphics.vector.rememberVectorPainter import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.roboBuilder +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun Accessory1NosePreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory2HornRed.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory2HornRed.kt index 75ae3d3e6..bc644203e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory2HornRed.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory2HornRed.kt @@ -34,7 +34,9 @@ import com.vitorpamplona.amethyst.commons.robohash.LightGray import com.vitorpamplona.amethyst.commons.robohash.LightRed import com.vitorpamplona.amethyst.commons.robohash.MediumGray import com.vitorpamplona.amethyst.commons.robohash.roboBuilder +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun Accessory2HornRedPreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory3Button.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory3Button.kt index d52a26aaa..021040dab 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory3Button.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory3Button.kt @@ -30,7 +30,9 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.LightRed import com.vitorpamplona.amethyst.commons.robohash.roboBuilder +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun Accessory3ButtonPreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory4Satellite.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory4Satellite.kt index d414a5302..13546dc08 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory4Satellite.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory4Satellite.kt @@ -30,7 +30,9 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.LightRed import com.vitorpamplona.amethyst.commons.robohash.roboBuilder +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun Accessory4SatellitePreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory5Mustache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory5Mustache.kt index ec6cad933..1a9f43667 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory5Mustache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory5Mustache.kt @@ -29,7 +29,9 @@ import androidx.compose.ui.graphics.vector.PathData import androidx.compose.ui.graphics.vector.rememberVectorPainter import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.roboBuilder +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun Accessory5MustachePreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body0Trooper.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body0Trooper.kt index f11ab8e24..4fe0eb588 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body0Trooper.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body0Trooper.kt @@ -30,7 +30,9 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.Gray import com.vitorpamplona.amethyst.commons.robohash.roboBuilder +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun Body0TropperPreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body1Thin.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body1Thin.kt index b20f22d08..d0c41903a 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body1Thin.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body1Thin.kt @@ -30,7 +30,9 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.Gray import com.vitorpamplona.amethyst.commons.robohash.roboBuilder +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun Body1ThinPreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body2Thinnest.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body2Thinnest.kt index 520bc1566..f0fc68638 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body2Thinnest.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body2Thinnest.kt @@ -30,7 +30,9 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.Gray import com.vitorpamplona.amethyst.commons.robohash.roboBuilder +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun Body2ThinnestPreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body3Front.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body3Front.kt index 646f5e332..01acfb160 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body3Front.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body3Front.kt @@ -30,7 +30,9 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.Gray import com.vitorpamplona.amethyst.commons.robohash.roboBuilder +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun Body3FrontPreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body4Round.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body4Round.kt index 9a5115f7f..b2a6e7340 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body4Round.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body4Round.kt @@ -30,7 +30,9 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.Gray import com.vitorpamplona.amethyst.commons.robohash.roboBuilder +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun Body4RoundPreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body5Neck.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body5Neck.kt index 11bb4cd4d..f389f36e7 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body5Neck.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body5Neck.kt @@ -30,7 +30,9 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.Gray import com.vitorpamplona.amethyst.commons.robohash.roboBuilder +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun Body5NeckPreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body6Ironman.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body6Ironman.kt index 373a62121..c84eab7c3 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body6Ironman.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body6Ironman.kt @@ -31,7 +31,9 @@ import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.Gray import com.vitorpamplona.amethyst.commons.robohash.Yellow import com.vitorpamplona.amethyst.commons.robohash.roboBuilder +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun Body6IronManPreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body7Neckthinner.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body7Neckthinner.kt index f6cf8f407..5f592e53d 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body7Neckthinner.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body7Neckthinner.kt @@ -30,7 +30,9 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.Gray import com.vitorpamplona.amethyst.commons.robohash.roboBuilder +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun Body7NeckThinnerPreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body8Big.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body8Big.kt index ab93ec93b..7d9f9c578 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body8Big.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body8Big.kt @@ -30,7 +30,9 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.Gray import com.vitorpamplona.amethyst.commons.robohash.roboBuilder +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun Body8BigPreview() { Image( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body9Huge.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body9Huge.kt index 98810d09d..6f2c4ecd2 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body9Huge.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body9Huge.kt @@ -30,7 +30,9 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.Gray import com.vitorpamplona.amethyst.commons.robohash.roboBuilder +import org.jetbrains.compose.ui.tooling.preview.Preview +@Preview @Composable fun Body9HugePreview() { Image( diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/Patterns.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/Patterns.kt index d0ef9308b..5e9ddefbb 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/Patterns.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/Patterns.kt @@ -28,11 +28,11 @@ import java.util.regex.Pattern */ object Patterns { /** - * Email address pattern from RFC 5322. + * Email address pattern from RFC 5322... From android.util.Patterns. */ val EMAIL_ADDRESS: Pattern = Pattern.compile( - "[a-zA-Z0-9+._%-]+@[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}", + "[a-zA-Z0-9+._%-]{1,256}@[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}(\\.[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25})+", ) /** From 54ce0a5a92b898a022d37797513317ad3432cabc Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 29 Dec 2025 19:37:45 -0500 Subject: [PATCH 002/137] Moves mutable time to Amethyst --- .../reqCommand/event/watchers/EventWatcherSubAssembler.kt | 2 +- .../reqCommand/user/watchers/UserCardsSubAssembler.kt | 2 +- .../reqCommand/user/watchers/UserReportsSubAssembler.kt | 2 +- .../main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt | 2 +- .../com/vitorpamplona/amethyst/service/relays}/MutableTime.kt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename {ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters => amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays}/MutableTime.kt (97%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt index edf756ce4..ff3f946a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt @@ -25,8 +25,8 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast +import com.vitorpamplona.amethyst.service.relays.MutableTime import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.ammolite.relays.filters.MutableTime import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt index f8fbd4542..21431db86 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt @@ -25,8 +25,8 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.toHexSet import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState +import com.vitorpamplona.amethyst.service.relays.MutableTime import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.ammolite.relays.filters.MutableTime import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt index f4342fdaf..5b2fc6337 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt @@ -25,8 +25,8 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.toHexSet import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState +import com.vitorpamplona.amethyst.service.relays.MutableTime import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.ammolite.relays.filters.MutableTime import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt index 0d1d6bbe8..0473c743b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.relays import androidx.collection.LruCache import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.ammolite.relays.filters.MutableTime +import com.vitorpamplona.amethyst.service.relays.MutableTime import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl typealias SincePerRelayMap = MutableMap diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/MutableTime.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/MutableTime.kt similarity index 97% rename from ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/MutableTime.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/MutableTime.kt index 67dfd8f2f..7a6701355 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/MutableTime.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/MutableTime.kt @@ -18,7 +18,7 @@ * 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.ammolite.relays.filters +package com.vitorpamplona.amethyst.service.relays /* * Wrapper class to allow changing in EOSE without modifying the list it is included within From 258c4e0111ee58b6da78034db4c12cba9d48cc0a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 29 Dec 2025 19:42:47 -0500 Subject: [PATCH 003/137] Moves BundledUpdate (which should cease to exist at some point) to Amethyst's module --- .../amethyst/model/LocalCache.kt | 2 +- .../amethyst/service}/BundledUpdate.kt | 5 +-- .../eoseManagers/BaseEoseManager.kt | 2 +- .../AccountFollowsLoaderSubAssembler.kt | 2 +- .../ui/feeds/ChannelFeedContentState.kt | 4 +-- .../amethyst/ui/feeds/FeedContentState.kt | 4 +-- .../amethyst/ui/screen/UserFeedViewModel.kt | 2 +- .../notifications/CardFeedContentState.kt | 4 +-- .../notifications/NotificationSummaryState.kt | 2 +- .../profile/zaps/LnZapFeedViewModel.kt | 2 +- .../loggedIn/settings/StringFeedViewModel.kt | 2 +- .../ammolite/service/MainThreadChecker.kt | 36 ------------------- 12 files changed, 14 insertions(+), 53 deletions(-) rename {ammolite/src/main/java/com/vitorpamplona/ammolite/relays => amethyst/src/main/java/com/vitorpamplona/amethyst/service}/BundledUpdate.kt (97%) delete mode 100644 ammolite/src/main/java/com/vitorpamplona/ammolite/service/MainThreadChecker.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 5042c45e2..28b05c3f2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -31,9 +31,9 @@ import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChanne import com.vitorpamplona.amethyst.model.observables.LatestByKindAndAuthor import com.vitorpamplona.amethyst.model.observables.LatestByKindWithETag import com.vitorpamplona.amethyst.model.privateChats.ChatroomList +import com.vitorpamplona.amethyst.service.BundledInsert import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.note.dateFormatter -import com.vitorpamplona.ammolite.relays.BundledInsert import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/BundledUpdate.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/BundledUpdate.kt similarity index 97% rename from ammolite/src/main/java/com/vitorpamplona/ammolite/relays/BundledUpdate.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/service/BundledUpdate.kt index 037a603db..2c5db2a8d 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/BundledUpdate.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/BundledUpdate.kt @@ -18,9 +18,8 @@ * 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.ammolite.relays +package com.vitorpamplona.amethyst.service -import com.vitorpamplona.ammolite.service.checkNotInMainThread import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineExceptionHandler @@ -135,8 +134,6 @@ class BasicBundledInsert( newObject: T, onUpdate: suspend (Set) -> Unit, ) { - checkNotInMainThread() - queue.put(newObject) if (onlyOneInBlock.getAndSet(true)) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt index a5ba24b91..970e79779 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers import com.vitorpamplona.amethyst.isDebug -import com.vitorpamplona.ammolite.relays.BundledUpdate +import com.vitorpamplona.amethyst.service.BundledUpdate import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt index 9c7db9910..4693c9e74 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt @@ -24,10 +24,10 @@ import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.BundledUpdate import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.IEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast -import com.vitorpamplona.ammolite.relays.BundledUpdate import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt index 8f7cedc71..cf62c2cf5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt @@ -25,11 +25,11 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.BundledInsert +import com.vitorpamplona.amethyst.service.BundledUpdate import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.dal.AdditiveComplexFeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists -import com.vitorpamplona.ammolite.relays.BundledInsert -import com.vitorpamplona.ammolite.relays.BundledUpdate import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.flattenToSet import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt index b1a12702a..283735f7a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt @@ -25,12 +25,12 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.BasicBundledInsert +import com.vitorpamplona.amethyst.service.BasicBundledUpdate import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.IFeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists -import com.vitorpamplona.ammolite.relays.BasicBundledInsert -import com.vitorpamplona.ammolite.relays.BasicBundledUpdate import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent import com.vitorpamplona.quartz.utils.flattenToSet import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt index 0ee61023e..b31b33843 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt @@ -27,11 +27,11 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.BundledUpdate import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists -import com.vitorpamplona.ammolite.relays.BundledUpdate import com.vitorpamplona.quartz.utils.Log import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt index e2ed1d22f..8dc2fd2f4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt @@ -29,6 +29,8 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.BundledInsert +import com.vitorpamplona.amethyst.service.BundledUpdate import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrderCard @@ -36,8 +38,6 @@ import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent import com.vitorpamplona.amethyst.ui.feeds.LoadedFeedState import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.dal.NotificationFeedFilter -import com.vitorpamplona.ammolite.relays.BundledInsert -import com.vitorpamplona.ammolite.relays.BundledUpdate import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt index 16447e5f3..daa23cac1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt @@ -29,10 +29,10 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.BundledInsert import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.note.showAmountInteger import com.vitorpamplona.amethyst.ui.note.showCount -import com.vitorpamplona.ammolite.relays.BundledInsert import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedViewModel.kt index 1619707db..7fa6baad1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedViewModel.kt @@ -24,10 +24,10 @@ import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.service.BundledUpdate import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists -import com.vitorpamplona.ammolite.relays.BundledUpdate import com.vitorpamplona.quartz.utils.Log import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedViewModel.kt index 5d49b9a50..6741a207d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedViewModel.kt @@ -26,11 +26,11 @@ import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.service.BundledUpdate import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists -import com.vitorpamplona.ammolite.relays.BundledUpdate import com.vitorpamplona.quartz.utils.Log import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/service/MainThreadChecker.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/service/MainThreadChecker.kt deleted file mode 100644 index afd0bf22a..000000000 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/service/MainThreadChecker.kt +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.ammolite.service - -import android.os.Looper -import com.vitorpamplona.ammolite.BuildConfig - -fun checkNotInMainThread() { - if (BuildConfig.DEBUG && isMainThread()) { - throw OnMainThreadException("It should not be in the MainThread") - } -} - -fun isMainThread() = Looper.myLooper() == Looper.getMainLooper() - -class OnMainThreadException( - str: String, -) : RuntimeException(str) From f654af9d8afed8c85afdf0a30ae28f094a9449ca Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Tue, 30 Dec 2025 15:18:48 +0200 Subject: [PATCH 004/137] initial skills --- .claude/core-skills-plan.md | 288 +++++ .claude/skills/android-expert/SKILL.md | 1066 +++++++++++++++++ .../references/android-navigation.md | 615 ++++++++++ .../references/android-permissions.md | 659 ++++++++++ .../references/proguard-rules.md | 468 ++++++++ .../scripts/analyze-apk-size.sh | 230 ++++ .claude/skills/desktop-expert/SKILL.md | 748 ++++++++++++ .../references/desktop-compose-apis.md | 597 +++++++++ .../references/desktop-navigation.md | 464 +++++++ .../references/keyboard-shortcuts.md | 400 +++++++ .../desktop-expert/references/os-detection.md | 579 +++++++++ .claude/skills/kotlin-expert/SKILL.md | 811 +++++++++++++ .../references/dsl-builder-examples.md | 602 ++++++++++ .../kotlin-expert/references/flow-patterns.md | 405 +++++++ .../references/immutability-patterns.md | 641 ++++++++++ .../references/sealed-class-catalog.md | 482 ++++++++ .claude/skills/kotlin-multiplatform/SKILL.md | 400 +++++++ .../references/abstraction-examples.md | 311 +++++ .../references/expect-actual-catalog.md | 163 +++ .../references/source-set-hierarchy.md | 332 +++++ .../references/target-compatibility.md | 345 ++++++ .../scripts/suggest-kmp-dependency.sh | 166 +++ .../scripts/validate-kmp-structure.sh | 126 ++ 23 files changed, 10898 insertions(+) create mode 100644 .claude/core-skills-plan.md create mode 100644 .claude/skills/android-expert/SKILL.md create mode 100644 .claude/skills/android-expert/references/android-navigation.md create mode 100644 .claude/skills/android-expert/references/android-permissions.md create mode 100644 .claude/skills/android-expert/references/proguard-rules.md create mode 100755 .claude/skills/android-expert/scripts/analyze-apk-size.sh create mode 100644 .claude/skills/desktop-expert/SKILL.md create mode 100644 .claude/skills/desktop-expert/references/desktop-compose-apis.md create mode 100644 .claude/skills/desktop-expert/references/desktop-navigation.md create mode 100644 .claude/skills/desktop-expert/references/keyboard-shortcuts.md create mode 100644 .claude/skills/desktop-expert/references/os-detection.md create mode 100644 .claude/skills/kotlin-expert/SKILL.md create mode 100644 .claude/skills/kotlin-expert/references/dsl-builder-examples.md create mode 100644 .claude/skills/kotlin-expert/references/flow-patterns.md create mode 100644 .claude/skills/kotlin-expert/references/immutability-patterns.md create mode 100644 .claude/skills/kotlin-expert/references/sealed-class-catalog.md create mode 100644 .claude/skills/kotlin-multiplatform/SKILL.md create mode 100644 .claude/skills/kotlin-multiplatform/references/abstraction-examples.md create mode 100644 .claude/skills/kotlin-multiplatform/references/expect-actual-catalog.md create mode 100644 .claude/skills/kotlin-multiplatform/references/source-set-hierarchy.md create mode 100644 .claude/skills/kotlin-multiplatform/references/target-compatibility.md create mode 100755 .claude/skills/kotlin-multiplatform/scripts/suggest-kmp-dependency.sh create mode 100755 .claude/skills/kotlin-multiplatform/scripts/validate-kmp-structure.sh diff --git a/.claude/core-skills-plan.md b/.claude/core-skills-plan.md new file mode 100644 index 000000000..ab4dcad6e --- /dev/null +++ b/.claude/core-skills-plan.md @@ -0,0 +1,288 @@ +# AmethystMultiplatform Skills Creation Plan + +## Overview +Create 8 hybrid domain skills combining general expertise with AmethystMultiplatform-specific patterns. + +**Approach:** Each skill provides domain knowledge + project-specific implementation patterns from codebase. + +## Skills to Implement + +### 1. kotlin-multiplatform ✅ COMPLETED +**Focus:** KMP architecture, jvmAndroid source set pattern, expect/actual + +**SKILL.md sections:** +- Mental model: KMP hierarchy as dependency graph +- Source set architecture: commonMain → jvmAndroid → {androidMain, jvmMain} +- The jvmAndroid pattern (unique to this project, verified in quartz/build.gradle.kts:132-149) +- expect/actual mechanics with 24+ examples from codebase +- iOS framework setup for Quartz distribution + +**Bundled resources:** +- `references/source-set-hierarchy.md` - Visual diagram + examples +- `references/expect-actual-catalog.md` - All 24 expect/actual pairs with patterns +- `scripts/validate-kmp-structure.sh` - Verify source set dependencies +- `assets/kmp-hierarchy-diagram.png` - Visual graph + +**Differentiation:** Existing kotlin-multiplatform agent = general KMP. This skill = Amethyst's unique jvmAndroid pattern, concrete examples. + +**Status:** ✅ Skill created and packaged at `.claude/skills/kotlin-multiplatform/` + +--- + +### 2. gradle-expert +**Focus:** Build optimization, dependency resolution, multi-module KMP troubleshooting + +**SKILL.md sections:** +- Build architecture: 4 modules, dependency flow +- Version catalog mastery (libs.versions.toml) +- Module dependency patterns (api vs implementation) +- Android-specific: compileSdk, proguard +- Desktop packaging: TargetFormat, distributions +- Build performance: daemon, parallel, caching +- Common errors: compose version conflicts, secp256k1 JNI variants + +**Bundled resources:** +- `references/build-commands.md` - Common gradle tasks +- `references/dependency-graph.md` - Module visualization +- `scripts/analyze-build-time.sh` - Performance report +- `scripts/fix-dependency-conflicts.sh` - Conflict patterns + +**Differentiation:** Focus on 4-module structure, KMP + Android + Desktop combo, specific issues (compose conflicts). + +--- + +### 3. kotlin-expert ✅ DRAFT COMPLETE +**Focus:** Flow state management, sealed hierarchies, immutability, DSL builders, inline/reified + +**SKILL.md sections:** +- Flow state management: StateFlow/SharedFlow patterns (AccountManager, RelayConnectionManager) +- Sealed hierarchies: sealed class vs sealed interface decision trees (AccountState, SignerResult) +- Immutability: @Immutable for Compose performance (173+ event classes) +- DSL builders: Type-safe fluent APIs (TagArrayBuilder, TlvBuilder) +- Inline functions: reified generics, performance optimization (OptimizedJsonMapper) +- Value classes: Zero-cost wrappers (optimization opportunity) + +**Bundled resources:** +- `references/flow-patterns.md` - StateFlow/SharedFlow with AccountManager, RelayManager patterns +- `references/sealed-class-catalog.md` - All 8 sealed types in quartz with usage patterns +- `references/dsl-builder-examples.md` - TagArrayBuilder, PrivateTagArrayBuilder, TlvBuilder, custom DSL patterns +- `references/immutability-patterns.md` - @Immutable annotation, data classes, ImmutableList/Map/Set + +**Differentiation:** Complements kotlin-coroutines agent (deep async). This skill = Amethyst Kotlin idioms (StateFlow state management, sealed for type safety, @Immutable for Compose, DSL builders). + +**Status:** ✅ SKILL.md (455 lines) + 4 references created at `.claude/skills/kotlin-expert/` + +**10-Step Progress:** +1. ✅ UNDERSTAND - Defined scope (Flow/sealed/DSL/immutability/inline) +2. ✅ EXPLORE - Found 173 @Immutable events, StateFlow in AccountManager/RelayManager, SignerResult generics, TagArrayBuilder +3. ✅ RESEARCH - StateFlow vs SharedFlow, sealed class vs interface best practices 2025 +4. ✅ SYNTHESIZE - Extracted Amethyst patterns (hot flows for state, sealed for results, @Immutable for perf) +5. ✅ DRAFT - Created SKILL.md + 4 reference files (flow, sealed, dsl, immutability) +6. ✅ SELF-CRITIQUE - Reviewed against 4 Core Truths (all PASS) +7. ✅ ITERATE - Draft complete (skipping deep iteration for now) +8. ⏸️ TEST - Deferred to later (requires real usage scenarios) +9. ⏸️ FINALIZE - Deferred to later +10. ✅ DOCUMENT - Updated plan + +--- + +### 4. compose-expert +**Focus:** Shared composables, state management, animations, Material3 + +**SKILL.md sections:** +- Shared composables philosophy (100+ already shared in commons/commonMain) +- State management: remember, derivedStateOf, produceState +- Recomposition optimization: @Stable/@Immutable +- Material3 conventions: theming +- Custom icons: ImageVector builders (38+ icons) +- Platform differences: Desktop vs Android UI +- Performance: lazy lists, image loading + +**Bundled resources:** +- `references/shared-composables-catalog.md` - 100+ composables +- `references/state-patterns.md` - State hoisting examples +- `references/icon-assets.md` - Custom icon catalog +- `scripts/find-composables.sh` - Grep @Composable + +**Differentiation:** Multiplatform Compose patterns, shared vs platform UI philosophy, Amethyst conventions (robohash, custom icons). + +--- + +### 5. ios-expert +**Focus:** iosMain patterns, Swift/KMP interop, XCFramework generation + +**SKILL.md sections:** +- iOS source sets: iosMain, iosX64Main, iosArm64Main +- Swift interop: type mapping, nullability +- expect/actual iOS: 10+ examples from quartz/iosMain +- XCFramework setup: baseName = "quartz-kmpKit" +- Platform APIs: platform.posix, CFNetwork, Security +- CocoaPods integration +- XCode project setup + +**Bundled resources:** +- `references/ios-actual-implementations.md` - 10 iosMain actuals +- `references/swift-interop-guide.md` - Type mapping +- `references/xcode-integration.md` - XCode setup +- `scripts/generate-xcframework.sh` - Build all iOS targets + +**Differentiation:** iOS platform specialization with Amethyst iosMain patterns, Quartz framework setup. + +--- + +### 6. desktop-expert ✅ DRAFT COMPLETE +**Focus:** Desktop UX, window management, Compose Desktop APIs, OS-specific conventions + +**SKILL.md sections:** +- Desktop entry point: application {} DSL +- Window management: WindowState, positioning, multi-window +- Menu system: MenuBar, keyboard shortcuts (OS-aware) +- System tray: minimize to tray +- Desktop navigation: NavigationRail pattern (vs Android bottom nav) +- File system: Desktop.getDesktop(), file pickers, drag-drop +- Desktop UX principles: keyboard-first, native feel, tooltips +- OS-specific behavior: macOS vs Windows vs Linux +- Platform detection: PlatformDetector utility +- Packaging: DMG, MSI, DEB distribution + +**Bundled resources:** +- `references/desktop-compose-apis.md` - Complete Desktop API catalog (Window, Tray, MenuBar, Dialog, etc.) +- `references/desktop-navigation.md` - NavigationRail vs BottomNav patterns +- `references/keyboard-shortcuts.md` - Standard shortcuts by OS with DesktopShortcuts helper +- `references/os-detection.md` - Platform detection, file paths, system integration + +**Differentiation:** Desktop-only APIs, OS conventions (Cmd vs Ctrl), NavigationRail, delegates build to gradle-expert and shared code to kotlin-multiplatform/compose-expert. + +**Status:** ✅ SKILL.md + 4 references created at `.claude/skills/desktop-expert/` + +**10-Step Progress:** +1. ✅ UNDERSTAND - Defined desktop usage scenarios +2. ✅ EXPLORE - Analyzed desktopApp/ module patterns (Main.kt, FeedScreen.kt, LoginScreen.kt) +3. ✅ RESEARCH - Compose Desktop APIs, OS-specific UX conventions (JetBrains docs, HIG) +4. ✅ SYNTHESIZE - Extracted desktop principles from codebase +5. ✅ DRAFT - Created SKILL.md + 4 reference files +6. ✅ SELF-CRITIQUE - Reviewed against 4 Core Truths (all PASS) +7. ✅ ITERATE - Draft complete (skipping deep iteration for now) +8. ⏸️ TEST - Deferred to later (requires real desktop scenarios) +9. ⏸️ FINALIZE - Deferred to later +10. ✅ DOCUMENT - Updated plan + +--- + +### 7. android-expert ✅ DRAFT COMPLETE +**Focus:** Android platform APIs, navigation, permissions, Material Design + +**SKILL.md sections:** +- Android module structure: amethyst/ layout +- Navigation: Navigation Compose, bottom nav +- Permissions: runtime (camera, biometric) +- Platform APIs: Intent, Context, ContentResolver +- Lifecycle: Lifecycle-aware, ViewModel +- Material Design: Android Material 3 +- Build config: Proguard, R8 +- Android UX: mobile-first patterns + +**Bundled resources:** +- `references/android-navigation.md` - Navigation Compose +- `references/android-permissions.md` - Permission handling +- `references/proguard-rules.md` - Proguard explanation +- `scripts/analyze-apk-size.sh` - APK optimization + +**Differentiation:** amethyst module structure, Android vs desktop patterns, Amethyst conventions. + +**Status:** ✅ SKILL.md + 3 references + 1 script created at `.claude/skills/android-expert/` + +**10-Step Progress:** +1. ✅ UNDERSTAND - Defined Android usage scenarios +2. ✅ EXPLORE - Analyzed amethyst/ module patterns +3. ✅ RESEARCH - Android best practices + KMP Android patterns +4. ✅ SYNTHESIZE - Extracted Android principles from codebase +5. ✅ DRAFT - Initialized skill, created resources +6. ✅ SELF-CRITIQUE - Reviewed against 4 Core Truths (all PASS) +7. ✅ ITERATE - Draft complete (skipping deep iteration for now) +8. ⏸️ TEST - Deferred to later +9. ⏸️ FINALIZE - Deferred to later +10. ✅ DOCUMENT - Updated plan + +--- + +### 8. nostr-expert +**Focus:** Nostr protocol, NIPs, Quartz architecture, event patterns + +**SKILL.md sections:** +- Quartz architecture: package structure by NIP (57 NIPs implemented) +- Event anatomy: IEvent, Event, kinds, tags (verified in TextNoteEvent.kt) +- NIP implementation patterns: Event class, tags, builders +- Common event types: TextNoteEvent, MetadataEvent, ReactionEvent +- Tag patterns: aTag, eTag, pTag builders +- Relay client: WebSocket, subscriptions +- Cryptography: secp256k1, NIP-44 encryption +- Event builders: TagArrayBuilder DSL + +**Bundled resources:** +- `references/nip-catalog.md` - All 57 NIPs with code links +- `references/event-hierarchy.md` - Event class diagram +- `references/tag-patterns.md` - Tag builders from nip01Core/tags/ +- `scripts/nip-lookup.sh` - Find NIP implementation files + +**Differentiation:** nostr-protocol agent = NIP specs. This skill = Quartz implementation (57 NIPs), code patterns. + +--- + +## Implementation Workflow + +Using skill-creator 10-step methodology per skill: + +**Overall Plan:** +1. **UNDERSTAND** ✅ - 8 skills defined, user clarifications obtained +2. **EXPLORE** ✅ - Codebase analyzed via Explore agent +3. **RESEARCH** ✅ - Domain patterns identified via Plan agent +4. **SYNTHESIZE** ✅ - Skills designed above + +**Per-Skill Implementation:** +- kotlin-multiplatform: ✅ COMPLETED +- android-expert: ✅ DRAFT COMPLETE +- desktop-expert: ✅ DRAFT COMPLETE +- kotlin-expert: ✅ DRAFT COMPLETE +- Remaining 4 skills: Pending (gradle-expert, compose-expert, ios-expert, nostr-expert) + +## Critical Files Referenced + +**Build patterns:** +- `/quartz/build.gradle.kts:132-149` - jvmAndroid source set +- `/commons/build.gradle.kts` - Shared UI setup + +**Code patterns:** +- `/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt` - Event structure +- `/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt` - StateFlow pattern +- `/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Platform.kt` - expect/actual + +**Documentation:** +- `/docs/shared-ui-analysis.md` - UI migration strategy + +## Output Location +`.claude/skills//` for each skill + +## Next Steps + +1. ✅ Save this plan as `.claude/core-skills-plan.md` for reference +2. ✅ Completed kotlin-multiplatform skill +3. ✅ Completed android-expert skill (DRAFT) +4. ✅ Completed desktop-expert skill (DRAFT) +5. ⏳ **Next: Pick one of the remaining 5 skills:** + - gradle-expert - Build optimization, dependency resolution + - kotlin-expert - Coroutines, Flow, sealed classes + - compose-expert - Shared composables, state management + - ios-expert - iOS platform patterns, XCFramework + - nostr-expert - Nostr protocol, NIPs, Quartz + +## Current Focus: 3 skills completed + +**Completed:** +- kotlin-multiplatform ✅ +- android-expert ✅ +- desktop-expert ✅ + +**Remaining:** 5 skills + +Ready to start next skill when requested. diff --git a/.claude/skills/android-expert/SKILL.md b/.claude/skills/android-expert/SKILL.md new file mode 100644 index 000000000..7754d5802 --- /dev/null +++ b/.claude/skills/android-expert/SKILL.md @@ -0,0 +1,1066 @@ +# android-expert + +Android platform expertise for Amethyst Multiplatform project. Covers Compose Navigation, Material3, permissions, lifecycle, and Android-specific patterns in KMP architecture. + +## When to Use + +Auto-invoke when working with: +- Android navigation (Navigation Compose, routes, bottom nav) +- Runtime permissions (camera, notifications, biometric) +- Platform APIs (Intent, Context, Activity) +- Material3 theming and edge-to-edge UI +- Android build configuration (Proguard, APK optimization) +- AndroidManifest.xml configuration +- Android lifecycle (ViewModel, collectAsStateWithLifecycle) + +## Core Mental Model + +**Single Activity Architecture + Compose Navigation** + +``` +MainActivity (Single Entry Point) + ├── enableEdgeToEdge() + ├── AmethystTheme { } + └── NavHost + ├── Route.Home → HomeScreen + ├── Route.Profile(id) → ProfileScreen + └── Route.Settings → SettingsScreen + +Intent Filters (11+) + ├── ACTION_MAIN (launcher) + ├── ACTION_SEND (share) + ├── ACTION_VIEW (deep links: nostr://, https://...) + └── NFC_ACTION_NDEF_DISCOVERED +``` + +**Key Principles:** +1. **Type-Safe Navigation** - @Serializable routes, no strings +2. **Declarative Permissions** - Request contextually with Accompanist +3. **Edge-to-Edge + Insets** - Scaffold handles system bars +4. **ViewModel + Flow → State** - Survive config changes +5. **Platform Isolation** - Android code in `amethyst/` module or `androidMain/` + +## Architecture Overview + +### Module Structure + +``` +amethyst/ # Android app module +├── src/ +│ ├── main/ +│ │ ├── java/com/vitorpamplona/amethyst/ +│ │ │ ├── ui/ +│ │ │ │ ├── MainActivity.kt # Entry point +│ │ │ │ ├── navigation/ +│ │ │ │ │ ├── AppNavigation.kt # NavHost +│ │ │ │ │ ├── routes/Routes.kt # @Serializable routes +│ │ │ │ │ └── bottombars/AppBottomBar.kt +│ │ │ │ ├── screen/ # 80+ screens +│ │ │ │ └── theme/Theme.kt # Material3 theme +│ │ │ └── Amethyst.kt # Application class +│ │ └── AndroidManifest.xml # Permissions, intent filters +│ └── androidMain/ # KMP Android source set +│ └── kotlin/ # Platform-specific code +└── build.gradle # Android config +``` + +## 1. Type-Safe Navigation + +### Pattern: @Serializable Routes + +**Best Practice (Navigation 2.8.0+):** +```kotlin +// Routes.kt - Define all routes with type safety +@Serializable +sealed class Route { + @Serializable object Home : Route() + @Serializable object Search : Route() + @Serializable data class Profile(val pubkey: String) : Route() + @Serializable data class Note(val noteId: String) : Route() + @Serializable data class Thread(val noteId: String) : Route() +} + +// AppNavigation.kt - NavHost setup +@Composable +fun AppNavigation( + navController: NavHostController, + accountViewModel: AccountViewModel +) { + NavHost( + navController = navController, + startDestination = Route.Home, + enterTransition = { fadeIn(animationSpec = tween(200)) }, + exitTransition = { fadeOut(animationSpec = tween(200)) } + ) { + composable { + HomeScreen(accountViewModel, navController) + } + + composable { backStackEntry -> + val profile = backStackEntry.toRoute() + ProfileScreen(profile.pubkey, accountViewModel, navController) + } + + composable { backStackEntry -> + val note = backStackEntry.toRoute() + NoteScreen(note.noteId, accountViewModel, navController) + } + } +} +``` + +### Navigation Manager Pattern + +**Amethyst Pattern (`Nav.kt`):** +```kotlin +class Nav( + val controller: NavHostController, + val drawerState: DrawerState, + val scope: CoroutineScope +) { + fun nav(route: Route) { + scope.launch { + controller.navigate(route) + drawerState.close() + } + } + + fun newStack(route: Route) { + scope.launch { + controller.navigate(route) { + popUpTo(Route.Home) { inclusive = false } + } + drawerState.close() + } + } + + fun popBack() { + controller.popBackStack() + } +} + +// Usage in composables +@Composable +fun HomeScreen(nav: Nav) { + Button(onClick = { nav.nav(Route.Profile("npub1...")) }) { + Text("View Profile") + } +} +``` + +### Bottom Navigation + +**Material3 Pattern:** +```kotlin +@Composable +fun AppBottomBar( + selectedRoute: Route, + nav: Nav +) { + NavigationBar { + BottomBarItem.entries.forEach { item -> + NavigationBarItem( + selected = selectedRoute::class == item.route::class, + onClick = { nav.nav(item.route) }, + icon = { Icon(item.icon, contentDescription = item.label) }, + label = { Text(item.label) } + ) + } + } +} + +enum class BottomBarItem(val route: Route, val icon: ImageVector, val label: String) { + HOME(Route.Home, Icons.Default.Home, "Home"), + MESSAGES(Route.Messages, Icons.Default.Message, "Messages"), + NOTIFICATIONS(Route.Notifications, Icons.Default.Notifications, "Notifications"), + SEARCH(Route.Search, Icons.Default.Search, "Search"), + PROFILE(Route.Profile, Icons.Default.Person, "Profile") +} +``` + +**Reference:** See `references/android-navigation.md` for complete navigation patterns. + +## 2. Runtime Permissions + +### Declarative Permission Handling + +**Accompanist Pattern (Experimental API):** +```kotlin +import com.google.accompanist.permissions.* + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun CameraFeature() { + val cameraPermissionState = rememberPermissionState( + Manifest.permission.CAMERA + ) + + when { + cameraPermissionState.status.isGranted -> { + // Permission granted - show camera UI + CameraPreview() + } + + cameraPermissionState.status.shouldShowRationale -> { + // Show rationale and request again + Column { + Text("Camera permission is needed to scan QR codes") + Button( + onClick = { cameraPermissionState.launchPermissionRequest() } + ) { + Text("Grant Permission") + } + } + } + + else -> { + // First time - request permission + Button( + onClick = { cameraPermissionState.launchPermissionRequest() } + ) { + Text("Enable Camera") + } + } + } +} +``` + +### Multiple Permissions + +```kotlin +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun MediaUploadFeature() { + val permissionsState = rememberMultiplePermissionsState( + permissions = listOf( + Manifest.permission.CAMERA, + Manifest.permission.READ_EXTERNAL_STORAGE + ) + ) + + when { + permissionsState.allPermissionsGranted -> { + MediaUploadUI() + } + + permissionsState.shouldShowRationale -> { + RationaleDialog( + onConfirm = { permissionsState.launchMultiplePermissionRequest() }, + onDismiss = { /* Handle dismissal */ } + ) + } + + else -> { + PermissionRequestButton( + onClick = { permissionsState.launchMultiplePermissionRequest() } + ) + } + } +} +``` + +### Lifecycle-Aware Permission Requests + +**Amethyst Pattern (LoggedInPage.kt):** +```kotlin +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun NotificationRegistration(accountViewModel: AccountViewModel) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val notificationPermissionState = rememberPermissionState( + Manifest.permission.POST_NOTIFICATIONS + ) + + if (notificationPermissionState.status.isGranted) { + LifecycleResumeEffect( + key1 = accountViewModel, + notificationPermissionState.status.isGranted + ) { + val scope = rememberCoroutineScope() + scope.launch { + PushNotificationUtils.checkAndInit( + context = context, + accountViewModel = accountViewModel + ) + } + + onPauseOrDispose { + // Cleanup when paused + } + } + } + } +} +``` + +### AndroidManifest Permission Declarations + +**Key Permissions in Amethyst:** +```xml + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +**Reference:** See `references/android-permissions.md` for complete permission patterns. + +## 3. Material3 + Edge-to-Edge + +### Edge-to-Edge Setup + +**MainActivity Pattern:** +```kotlin +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() // Android 15+ immersive UI + super.onCreate(savedInstanceState) + + setContent { + AmethystTheme { + AccountScreen() + } + } + } +} +``` + +### Theme Configuration + +**Material3 Color Schemes:** +```kotlin +// theme/Theme.kt +private val DarkColorPalette = darkColorScheme( + primary = Purple200, + secondary = Teal200, + tertiary = Pink80, + background = Color.Black, + surface = Color.Black, + onPrimary = Color.White, + onSecondary = Color.Black, + onBackground = Color.White, + onSurface = Color.White +) + +private val LightColorPalette = lightColorScheme( + primary = Purple500, + secondary = Teal700, + tertiary = Pink40, + background = Color.White, + surface = Color.White, + onPrimary = Color.White, + onSecondary = Color.White, + onBackground = Color.Black, + onSurface = Color.Black +) + +@Composable +fun AmethystTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val colorScheme = if (darkTheme) DarkColorPalette else LightColorPalette + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} +``` + +### Scaffold with Insets + +**Handling System Bars:** +```kotlin +@Composable +fun MainScreen(navController: NavHostController) { + val currentRoute by navController.currentBackStackEntryAsState() + + Scaffold( + topBar = { AppTopBar(currentRoute) }, + bottomBar = { AppBottomBar(currentRoute, navController) }, + floatingActionButton = { NewPostFab() } + ) { innerPadding -> + // Scaffold automatically handles system bar insets + NavHost( + navController = navController, + modifier = Modifier.padding(innerPadding) + ) { + // Routes... + } + } +} +``` + +**Custom Inset Handling:** +```kotlin +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.systemBarsPadding + +@Composable +fun CustomEdgeToEdgeScreen() { + Box( + modifier = Modifier + .fillMaxSize() + .systemBarsPadding() // Add padding for system bars + ) { + // Content draws edge-to-edge with safe padding + } +} +``` + +## 4. ViewModel + Lifecycle + +### ViewModel Pattern + +**Standard Structure (80+ ViewModels in Amethyst):** +```kotlin +class FeedViewModel( + private val accountStateViewModel: AccountStateViewModel +) : ViewModel() { + + private val _feedState = MutableStateFlow(FeedState.Loading) + val feedState: StateFlow = _feedState.asStateFlow() + + init { + loadFeed() + } + + fun loadFeed() { + viewModelScope.launch { + _feedState.value = FeedState.Loading + try { + val posts = repository.getFeed() + _feedState.value = FeedState.Success(posts) + } catch (e: Exception) { + _feedState.value = FeedState.Error(e.message) + } + } + } + + fun refresh() { + loadFeed() + } +} + +sealed class FeedState { + object Loading : FeedState() + data class Success(val posts: List) : FeedState() + data class Error(val message: String?) : FeedState() +} +``` + +### Compose Integration + +**collectAsStateWithLifecycle Pattern:** +```kotlin +@Composable +fun FeedScreen( + feedViewModel: FeedViewModel = viewModel() +) { + val feedState by feedViewModel.feedState.collectAsStateWithLifecycle() + + when (feedState) { + is FeedState.Loading -> { + LoadingIndicator() + } + is FeedState.Success -> { + val posts = (feedState as FeedState.Success).posts + LazyColumn { + items(posts) { post -> + PostCard(post) + } + } + } + is FeedState.Error -> { + ErrorScreen( + message = (feedState as FeedState.Error).message, + onRetry = { feedViewModel.refresh() } + ) + } + } +} +``` + +### Lifecycle Effects + +**LifecycleResumeEffect Pattern:** +```kotlin +@Composable +fun ChatScreen(chatViewModel: ChatViewModel) { + LifecycleResumeEffect(key1 = chatViewModel) { + // Called when composable resumes (onResume) + chatViewModel.connectToRelay() + + onPauseOrDispose { + // Called when composable pauses (onPause) or disposes + chatViewModel.disconnectFromRelay() + } + } +} +``` + +**DisposableEffect for Cleanup:** +```kotlin +@Composable +fun VideoPlayer(videoUrl: String) { + val context = LocalContext.current + val exoPlayer = remember { + ExoPlayer.Builder(context).build().apply { + setMediaItem(MediaItem.fromUri(videoUrl)) + prepare() + } + } + + DisposableEffect(videoUrl) { + onDispose { + exoPlayer.release() + } + } + + AndroidView( + factory = { PlayerView(it).apply { player = exoPlayer } } + ) +} +``` + +## 5. Platform APIs + +### Activity & Context Access + +**LocalContext Pattern:** +```kotlin +@Composable +fun ShareButton(text: String) { + val context = LocalContext.current + + Button( + onClick = { + val intent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, text) + } + context.startActivity(Intent.createChooser(intent, "Share via")) + } + ) { + Text("Share") + } +} +``` + +**Activity Reference:** +```kotlin +// WindowUtils.kt pattern +@Composable +fun getActivity(): Activity? = LocalContext.current.getActivity() + +tailrec fun Context.getActivity(): ComponentActivity = + when (this) { + is ComponentActivity -> this + is ContextWrapper -> baseContext.getActivity() + else -> throw IllegalStateException("Context not an Activity") + } + +// Usage +@Composable +fun FullscreenToggle() { + val activity = getActivity() + + Button( + onClick = { + activity?.window?.setFlags( + WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN + ) + } + ) { + Text("Go Fullscreen") + } +} +``` + +### Intent Handling + +**Deep Links (AppNavigation.kt pattern):** +```kotlin +@Composable +fun AppNavigation( + navController: NavHostController, + accountViewModel: AccountViewModel +) { + val activity = LocalContext.current as? Activity + + LaunchedEffect(activity?.intent) { + activity?.intent?.let { intent -> + when (intent.action) { + Intent.ACTION_SEND -> { + val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT) + val sharedImage = intent.getParcelableExtra(Intent.EXTRA_STREAM) + navController.navigate( + Route.NewPost(message = sharedText, attachment = sharedImage.toString()) + ) + } + + Intent.ACTION_VIEW -> { + val uri = intent.data + when (uri?.scheme) { + "nostr" -> handleNostrUri(uri, navController) + "https", "http" -> handleWebUri(uri, navController) + } + } + } + } + } + + NavHost(navController = navController) { + // Routes... + } +} + +fun handleNostrUri(uri: Uri, navController: NavHostController) { + // nostr:npub1... -> Profile + // nostr:note1... -> Note + // nostr:nevent1... -> Event + when { + uri.path?.startsWith("npub") == true -> { + navController.navigate(Route.Profile(uri.path!!)) + } + uri.path?.startsWith("note") == true -> { + navController.navigate(Route.Note(uri.path!!)) + } + } +} +``` + +### File Sharing with FileProvider + +**ShareHelper Pattern:** +```kotlin +fun shareImage(context: Context, imageUri: Uri) { + try { + // Get file from cache + val cachedFile = getCachedFile(context, imageUri) + + // Create content URI via FileProvider + val contentUri = FileProvider.getUriForFile( + context, + "${context.packageName}.provider", + cachedFile + ) + + val shareIntent = Intent(Intent.ACTION_SEND).apply { + type = "image/*" + putExtra(Intent.EXTRA_STREAM, contentUri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + + context.startActivity(Intent.createChooser(shareIntent, "Share Image")) + } catch (e: Exception) { + Toast.makeText(context, "Failed to share: ${e.message}", Toast.LENGTH_SHORT).show() + } +} +``` + +**FileProvider Configuration (AndroidManifest.xml):** +```xml + + + +``` + +### Activity Results + +**External Signer Integration (Amethyst pattern):** +```kotlin +@Composable +fun SignerIntegration(accountViewModel: AccountViewModel) { + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + result.data?.let { data -> + accountViewModel.account.signer.newResponse(data) + } + } + } + + Button( + onClick = { + val signerIntent = Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse("nostrsigner:...") + } + launcher.launch(signerIntent) + } + ) { + Text("Sign with External App") + } +} +``` + +## 6. Build Configuration + +### Android Block + +**build.gradle (Amethyst pattern):** +```gradle +android { + namespace = 'com.vitorpamplona.amethyst' + compileSdk = 36 + + defaultConfig { + applicationId = "com.vitorpamplona.amethyst" + minSdk = 26 // Android 8.0 (Oreo) + targetSdk = 36 // Android 15 + versionCode = 430 + versionName = "1.04.2" + + vectorDrawables { + useSupportLibrary = true + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + + buildFeatures { + compose = true + buildConfig = true // Enable BuildConfig access + } + + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() + } + + packaging { + resources { + excludes += '/META-INF/{AL2.0,LGPL2.1}' + } + } + + // Product flavors for Play Store vs F-Droid + flavorDimensions = ["channel"] + productFlavors { + create("play") { + dimension = "channel" + // Firebase, Google services + } + create("fdroid") { + dimension = "channel" + // UnifiedPush, open-source alternatives + } + } +} + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_21 + } +} +``` + +### Dependencies + +**Key Android Dependencies:** +```gradle +dependencies { + // Compose BOM + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.ui.tooling.preview) + + // Navigation + implementation(libs.androidx.navigation.compose) + + // Lifecycle + implementation(libs.androidx.lifecycle.runtime.compose) + implementation(libs.androidx.lifecycle.viewmodel.compose) + + // Activity + implementation(libs.androidx.activity.compose) + + // Accompanist + implementation(libs.accompanist.permissions) + + // Shared module + implementation(project(":commons")) + implementation(project(":quartz")) +} +``` + +### Proguard Rules + +**Common Rules for Amethyst:** +```proguard +# Keep Kotlin metadata +-keep class kotlin.Metadata { *; } + +# Keep Nostr event classes +-keep class com.vitorpamplona.quartz.events.** { *; } + +# Keep serialization +-keepattributes *Annotation*, InnerClasses +-dontnote kotlinx.serialization.AnnotationsKt + +# OkHttp +-dontwarn okhttp3.** +-keep class okhttp3.** { *; } + +# Compose +-keep class androidx.compose.** { *; } +-dontwarn androidx.compose.** +``` + +**Reference:** See `references/proguard-rules.md` for complete Proguard configuration. + +### APK Optimization + +**Reference:** See `scripts/analyze-apk-size.sh` for APK size analysis. + +## 7. KMP Android Source Sets + +### Android Module Layout + +**Amethyst Structure:** +``` +amethyst/ +├── src/ +│ ├── main/ # Standard Android +│ │ ├── java/com/.../ # Compose UI code +│ │ ├── res/ # Android resources +│ │ └── AndroidManifest.xml +│ └── androidMain/ # KMP Android source set (if needed) +│ └── kotlin/ # Platform-specific utilities +└── build.gradle +``` + +**Platform-Specific Code:** +```kotlin +// commons/src/androidMain/kotlin/Platform.android.kt +actual fun openExternalUrl(url: String, context: Any) { + val ctx = context as Context + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + ctx.startActivity(intent) +} + +actual fun shareText(text: String, context: Any) { + val ctx = context as Context + val intent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, text) + } + ctx.startActivity(Intent.createChooser(intent, "Share")) +} +``` + +### Build Configuration for KMP + +```gradle +kotlin { + androidTarget { + compilerOptions { + jvmTarget = JvmTarget.JVM_21 + } + } +} + +android { + sourceSets { + // Link androidMain source set + getByName("main") { + manifest.srcFile("src/main/AndroidManifest.xml") + java.srcDirs("src/main/java", "src/androidMain/kotlin") + } + } +} +``` + +## Common Patterns + +### 1. Single Activity Architecture + +**All screens in one activity, navigation via Compose:** +```kotlin +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + + setContent { + AmethystTheme { + val accountViewModel: AccountStateViewModel = viewModel() + AccountScreen(accountViewModel) + } + } + } + + override fun onResume() { + super.onResume() + DEFAULT_MUTED_SETTING.value = true + } + + override fun onPause() { + super.onPause() + LanguageTranslatorService.clear() + } +} +``` + +### 2. Configuration Changes + +**ViewModels survive rotation:** +```kotlin +// ViewModel persists across config changes +@Composable +fun ProfileScreen( + profileViewModel: ProfileViewModel = viewModel() +) { + val profile by profileViewModel.profile.collectAsStateWithLifecycle() + + // UI rebuilds on rotation, but ViewModel data persists + ProfileContent(profile) +} +``` + +### 3. Resource Access + +```kotlin +@Composable +fun LocalizedButton() { + val context = LocalContext.current + + Button( + onClick = { + val message = context.getString(R.string.button_clicked) + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + } + ) { + Text(stringResource(R.string.button_label)) + } +} +``` + +## Testing Android Components + +### Navigation Testing + +```kotlin +@Test +fun testNavigationToProfile() { + val navController = TestNavHostController(ApplicationProvider.getApplicationContext()) + + composeTestRule.setContent { + navController.navigatorProvider.addNavigator(ComposeNavigator()) + AppNavigation(navController, accountViewModel) + } + + composeTestRule.onNodeWithText("Profile").performClick() + + assertEquals( + Route.Profile::class, + navController.currentBackStackEntry?.destination?.route::class + ) +} +``` + +### Permission Testing + +```kotlin +@Test +fun testPermissionRequest() { + val scenario = launchActivity() + + scenario.onActivity { activity -> + // Grant permission via UiAutomator + grantPermissionViaUi(Manifest.permission.CAMERA) + } + + composeTestRule.onNodeWithText("Camera Ready").assertExists() +} +``` + +## Anti-Patterns to Avoid + +1. **String-based navigation** - Use type-safe @Serializable routes +2. **Requesting permissions eagerly** - Request contextually before feature use +3. **Ignoring edge-to-edge** - Handle insets properly with Scaffold +4. **Using GlobalScope** - Use viewModelScope or rememberCoroutineScope +5. **Not handling config changes** - Use ViewModel + collectAsStateWithLifecycle +6. **Hardcoded system bar heights** - Use WindowInsets APIs +7. **Blocking main thread** - Use viewModelScope.launch(Dispatchers.IO) + +## Quick Reference + +| Task | Pattern | +|------|---------| +| **Navigate** | `navController.navigate(Route.Profile(id))` | +| **Request Permission** | `rememberPermissionState().launchPermissionRequest()` | +| **Access Context** | `val context = LocalContext.current` | +| **Get Activity** | `val activity = context.getActivity()` | +| **Open URL** | `Intent(ACTION_VIEW, Uri.parse(url))` | +| **Share Text** | `Intent(ACTION_SEND).putExtra(EXTRA_TEXT, text)` | +| **Observe Flow** | `flow.collectAsStateWithLifecycle()` | +| **Lifecycle Effect** | `LifecycleResumeEffect { ... }` | +| **Handle Insets** | `Modifier.systemBarsPadding()` | +| **Theme** | `MaterialTheme(colorScheme = ...) { }` | + +## File Locations + +**Key Android Files:** +- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt` +- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt` +- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt` +- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt` +- `amethyst/src/main/AndroidManifest.xml` +- `amethyst/build.gradle` + +## Additional Resources + +- `references/android-navigation.md` - Complete navigation patterns and examples +- `references/android-permissions.md` - Permission handling patterns +- `references/proguard-rules.md` - Proguard configuration +- `scripts/analyze-apk-size.sh` - APK size optimization script + +## When NOT to Use + +- Desktop-specific features → Use `desktop-expert` skill +- iOS-specific features → Use `ios-expert` skill +- Shared KMP code → Use `kotlin-multiplatform` skill +- Nostr protocol → Use `nostr-expert` skill +- Compose UI components → Use `compose-expert` skill diff --git a/.claude/skills/android-expert/references/android-navigation.md b/.claude/skills/android-expert/references/android-navigation.md new file mode 100644 index 000000000..59d8a9e31 --- /dev/null +++ b/.claude/skills/android-expert/references/android-navigation.md @@ -0,0 +1,615 @@ +# Android Navigation Patterns + +Complete navigation implementation patterns for Amethyst Android app using Navigation Compose with type safety. + +## Type-Safe Routes (Navigation 2.8.0+) + +### Route Definitions + +```kotlin +// Routes.kt - All 40+ routes in Amethyst +@Serializable +sealed class Route { + // Bottom nav routes + @Serializable object Home : Route() + @Serializable object Messages : Route() + @Serializable object Video : Route() + @Serializable object Discover : Route() + @Serializable object Notification : Route() + + // Content routes with parameters + @Serializable data class Profile(val pubkey: String) : Route() + @Serializable data class Note(val id: String) : Route() + @Serializable data class Channel(val id: String) : Route() + @Serializable data class Thread( + val id: String, + val replyTo: String? = null + ) : Route() + + // New content routes + @Serializable data class NewPost( + val message: String? = null, + val attachment: String? = null, + val replyTo: String? = null + ) : Route() + + // Settings + @Serializable object Settings : Route() + @Serializable object Security : Route() + @Serializable object Relays : Route() + + // Search + @Serializable data class Search(val query: String = "") : Route() + + // Media + @Serializable data class Image(val url: String) : Route() + @Serializable data class Video(val url: String) : Route() +} +``` + +## NavHost Configuration + +### Basic Setup + +```kotlin +@Composable +fun AppNavigation( + navController: NavHostController, + accountViewModel: AccountViewModel, + drawerState: DrawerState +) { + val scope = rememberCoroutineScope() + val nav = remember { + Nav(navController, drawerState, scope) + } + + NavHost( + navController = navController, + startDestination = Route.Home, + enterTransition = { fadeIn(animationSpec = tween(200)) }, + exitTransition = { fadeOut(animationSpec = tween(200)) }, + popEnterTransition = { fadeIn(animationSpec = tween(200)) }, + popExitTransition = { fadeOut(animationSpec = tween(200)) } + ) { + // Define routes + composable { + HomeScreen(accountViewModel, nav) + } + + composable { backStackEntry -> + val profile = backStackEntry.toRoute() + ProfileScreen( + pubkey = profile.pubkey, + accountViewModel = accountViewModel, + nav = nav + ) + } + + composable { backStackEntry -> + val note = backStackEntry.toRoute() + NoteScreen( + noteId = note.id, + accountViewModel = accountViewModel, + nav = nav + ) + } + + composable { backStackEntry -> + val newPost = backStackEntry.toRoute() + NewPostScreen( + initialMessage = newPost.message, + initialAttachment = newPost.attachment, + replyTo = newPost.replyTo, + accountViewModel = accountViewModel, + onPost = { nav.popBack() } + ) + } + } +} +``` + +### Custom Transitions + +```kotlin +composable( + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(300) + ) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(300) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(300) + ) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(300) + ) + } +) { backStackEntry -> + val profile = backStackEntry.toRoute() + ProfileScreen(profile.pubkey, accountViewModel, nav) +} +``` + +## Navigation Manager + +### Nav Wrapper Class + +```kotlin +class Nav( + val controller: NavHostController, + val drawerState: DrawerState, + val scope: CoroutineScope +) { + /** + * Navigate to a route, closing drawer if open + */ + fun nav(route: Route) { + scope.launch { + if (!controller.popBackStack(route, inclusive = false)) { + controller.navigate(route) { + launchSingleTop = true + } + } + drawerState.close() + } + } + + /** + * Navigate with new stack (clear back stack to Home) + */ + fun newStack(route: Route) { + scope.launch { + controller.navigate(route) { + popUpTo(Route.Home) { + inclusive = false + } + launchSingleTop = true + } + drawerState.close() + } + } + + /** + * Pop back stack + */ + fun popBack() { + controller.popBackStack() + } + + /** + * Pop up to specific route + */ + inline fun popUpTo(inclusive: Boolean = false) { + controller.popBackStack(inclusive = inclusive) + } + + /** + * Get current route + */ + fun currentRoute(): Route? { + return controller.currentBackStackEntry?.toRoute() + } +} +``` + +## Bottom Navigation + +### Material3 NavigationBar + +```kotlin +@Composable +fun AppBottomBar( + currentRoute: Route?, + nav: Nav +) { + NavigationBar( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface + ) { + BottomBarRoute.entries.forEach { item -> + NavigationBarItem( + selected = currentRoute?.let { it::class == item.route::class } ?: false, + onClick = { nav.nav(item.route) }, + icon = { + Icon( + imageVector = if (currentRoute?.let { it::class == item.route::class } == true) { + item.selectedIcon + } else { + item.unselectedIcon + }, + contentDescription = item.label + ) + }, + label = { Text(item.label) }, + alwaysShowLabel = false + ) + } + } +} + +enum class BottomBarRoute( + val route: Route, + val selectedIcon: ImageVector, + val unselectedIcon: ImageVector, + val label: String +) { + HOME( + route = Route.Home, + selectedIcon = Icons.Filled.Home, + unselectedIcon = Icons.Outlined.Home, + label = "Home" + ), + MESSAGES( + route = Route.Messages, + selectedIcon = Icons.Filled.Message, + unselectedIcon = Icons.Outlined.Message, + label = "Messages" + ), + VIDEOS( + route = Route.Video, + selectedIcon = Icons.Filled.VideoLibrary, + unselectedIcon = Icons.Outlined.VideoLibrary, + label = "Videos" + ), + DISCOVER( + route = Route.Discover, + selectedIcon = Icons.Filled.Explore, + unselectedIcon = Icons.Outlined.Explore, + label = "Discover" + ), + NOTIFICATIONS( + route = Route.Notification, + selectedIcon = Icons.Filled.Notifications, + unselectedIcon = Icons.Outlined.Notifications, + label = "Notifications" + ) +} +``` + +### Observing Current Route + +```kotlin +@Composable +fun MainScreen() { + val navController = rememberNavController() + val currentBackStackEntry by navController.currentBackStackEntryAsState() + val currentRoute = currentBackStackEntry?.toRoute() + + Scaffold( + topBar = { + if (shouldShowTopBar(currentRoute)) { + AppTopBar(currentRoute) + } + }, + bottomBar = { + if (shouldShowBottomBar(currentRoute)) { + AppBottomBar(currentRoute, nav) + } + } + ) { paddingValues -> + AppNavigation( + navController = navController, + modifier = Modifier.padding(paddingValues) + ) + } +} + +fun shouldShowBottomBar(route: Route?): Boolean { + return when (route) { + is Route.Home, + is Route.Messages, + is Route.Video, + is Route.Discover, + is Route.Notification -> true + else -> false + } +} +``` + +## Navigation Drawer + +### Material3 ModalDrawerSheet + +```kotlin +@Composable +fun AppDrawer( + drawerState: DrawerState, + nav: Nav, + accountViewModel: AccountViewModel +) { + val scope = rememberCoroutineScope() + + ModalDrawerSheet { + // User profile header + DrawerHeader(accountViewModel.account) + + HorizontalDivider() + + // Menu items + NavigationDrawerItem( + label = { Text("Home") }, + selected = false, + onClick = { nav.nav(Route.Home) }, + icon = { Icon(Icons.Default.Home, "Home") } + ) + + NavigationDrawerItem( + label = { Text("Profile") }, + selected = false, + onClick = { nav.nav(Route.Profile(accountViewModel.account.pubkey)) }, + icon = { Icon(Icons.Default.Person, "Profile") } + ) + + NavigationDrawerItem( + label = { Text("Settings") }, + selected = false, + onClick = { nav.nav(Route.Settings) }, + icon = { Icon(Icons.Default.Settings, "Settings") } + ) + + HorizontalDivider() + + NavigationDrawerItem( + label = { Text("Logout") }, + selected = false, + onClick = { + scope.launch { + accountViewModel.logout() + drawerState.close() + } + }, + icon = { Icon(Icons.Default.Logout, "Logout") } + ) + } +} +``` + +### Main Scaffold with Drawer + +```kotlin +@Composable +fun MainScreen() { + val navController = rememberNavController() + val drawerState = rememberDrawerState(DrawerValue.Closed) + val scope = rememberCoroutineScope() + val nav = remember { Nav(navController, drawerState, scope) } + + ModalNavigationDrawer( + drawerState = drawerState, + drawerContent = { + AppDrawer(drawerState, nav, accountViewModel) + } + ) { + Scaffold( + topBar = { + TopAppBar( + title = { Text("Amethyst") }, + navigationIcon = { + IconButton( + onClick = { scope.launch { drawerState.open() } } + ) { + Icon(Icons.Default.Menu, "Menu") + } + } + ) + }, + bottomBar = { AppBottomBar(currentRoute, nav) } + ) { paddingValues -> + AppNavigation( + navController = navController, + modifier = Modifier.padding(paddingValues) + ) + } + } +} +``` + +## Deep Link Handling + +### Intent Processing + +```kotlin +@Composable +fun AppNavigation( + navController: NavHostController, + accountViewModel: AccountViewModel +) { + val activity = LocalContext.current as? Activity + + // Handle incoming intents + LaunchedEffect(activity?.intent) { + activity?.intent?.let { intent -> + handleIntent(intent, navController) + } + } + + NavHost(navController = navController) { + // Routes... + } +} + +fun handleIntent(intent: Intent, navController: NavHostController) { + when (intent.action) { + Intent.ACTION_SEND -> { + // Share text/image + val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT) + val sharedUri = intent.getParcelableExtra(Intent.EXTRA_STREAM) + + navController.navigate( + Route.NewPost( + message = sharedText, + attachment = sharedUri?.toString() + ) + ) + } + + Intent.ACTION_VIEW -> { + // Deep link + intent.data?.let { uri -> + when (uri.scheme) { + "nostr" -> handleNostrUri(uri, navController) + "https", "http" -> handleWebUri(uri, navController) + } + } + } + } +} + +fun handleNostrUri(uri: Uri, navController: NavHostController) { + val path = uri.pathSegments.firstOrNull() ?: return + + when { + path.startsWith("npub") -> { + navController.navigate(Route.Profile(path)) + } + path.startsWith("note") -> { + navController.navigate(Route.Note(path)) + } + path.startsWith("nevent") -> { + // Decode and navigate to event + val eventId = decodeNevent(path) + navController.navigate(Route.Note(eventId)) + } + } +} + +fun handleWebUri(uri: Uri, navController: NavHostController) { + // Handle web-based deep links + // https://njump.me/npub1... + // https://primal.net/profile/npub1... + when (uri.host) { + "njump.me" -> { + val id = uri.pathSegments.lastOrNull() + if (id?.startsWith("npub") == true) { + navController.navigate(Route.Profile(id)) + } + } + "primal.net" -> { + // Parse primal.net URLs + } + } +} +``` + +### AndroidManifest Intent Filters + +```xml + + + + + + + + + + + + + + + + + + + + + + + +``` + +## Nested Navigation + +### Tab Navigation Inside Screen + +```kotlin +@Composable +fun ProfileScreen( + pubkey: String, + nav: Nav +) { + val nestedNavController = rememberNavController() + + Column { + ProfileHeader(pubkey) + + // Tab row + TabRow(selectedTabIndex = currentTab) { + Tab(selected = currentTab == 0, onClick = { /* Notes */ }) + Tab(selected = currentTab == 1, onClick = { /* Replies */ }) + Tab(selected = currentTab == 2, onClick = { /* Likes */ }) + } + + // Nested NavHost for tabs + NavHost( + navController = nestedNavController, + startDestination = ProfileTab.Notes + ) { + composable { + NotesTabContent(pubkey) + } + composable { + RepliesTabContent(pubkey) + } + composable { + LikesTabContent(pubkey) + } + } + } +} + +@Serializable +sealed class ProfileTab { + @Serializable object Notes : ProfileTab() + @Serializable object Replies : ProfileTab() + @Serializable object Likes : ProfileTab() +} +``` + +## Testing Navigation + +### Navigation Test Example + +```kotlin +@Test +fun testNavigationToProfile() { + val navController = TestNavHostController( + ApplicationProvider.getApplicationContext() + ) + + composeTestRule.setContent { + navController.navigatorProvider.addNavigator( + ComposeNavigator() + ) + AppNavigation(navController, accountViewModel) + } + + // Navigate to profile + composeTestRule.onNodeWithText("Profile").performClick() + + // Verify navigation + val currentRoute = navController.currentBackStackEntry?.toRoute() + assertTrue(currentRoute is Route.Profile) +} +``` + +## File Locations + +- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt` +- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt` +- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Nav.kt` +- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppBottomBar.kt` +- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt` diff --git a/.claude/skills/android-expert/references/android-permissions.md b/.claude/skills/android-expert/references/android-permissions.md new file mode 100644 index 000000000..89cc790a1 --- /dev/null +++ b/.claude/skills/android-expert/references/android-permissions.md @@ -0,0 +1,659 @@ +# Android Runtime Permissions + +Complete permission handling patterns for Amethyst using Accompanist Permissions library and Android best practices. + +## Permission Categories in Amethyst + +### Network Permissions (Normal - Auto-granted) + +```xml + + + +``` + +### Media Permissions (Dangerous - Runtime request) + +```xml + + + + + + + + + +``` + +### Notification Permissions (Android 13+) + +```xml + +``` + +### Location Permissions + +```xml + +``` + +### NFC Permissions + +```xml + +``` + +### Foreground Service Permissions + +```xml + + + +``` + +## Accompanist Permissions Library + +### Setup + +```gradle +dependencies { + implementation("com.google.accompanist:accompanist-permissions:0.36.0") +} +``` + +### Single Permission Pattern + +```kotlin +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.rememberPermissionState +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.shouldShowRationale + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun CameraFeature() { + val cameraPermissionState = rememberPermissionState( + Manifest.permission.CAMERA + ) + + when { + // Permission granted - show feature + cameraPermissionState.status.isGranted -> { + CameraPreview() + } + + // Should show rationale - explain why permission is needed + cameraPermissionState.status.shouldShowRationale -> { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Camera permission is needed to scan QR codes for login", + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(16.dp)) + Button( + onClick = { cameraPermissionState.launchPermissionRequest() } + ) { + Text("Grant Permission") + } + } + } + + // First time - request permission + else -> { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Button( + onClick = { cameraPermissionState.launchPermissionRequest() } + ) { + Icon(Icons.Default.CameraAlt, contentDescription = null) + Spacer(modifier = Modifier.width(8.dp)) + Text("Enable Camera") + } + } + } + } +} +``` + +### Multiple Permissions Pattern + +```kotlin +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun MediaUploadFeature() { + val permissionsState = rememberMultiplePermissionsState( + permissions = buildList { + add(Manifest.permission.CAMERA) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { + add(Manifest.permission.READ_EXTERNAL_STORAGE) + } + } + ) + + when { + // All permissions granted + permissionsState.allPermissionsGranted -> { + MediaUploadUI() + } + + // Some permissions need rationale + permissionsState.shouldShowRationale -> { + RationaleDialog( + title = "Permissions Required", + message = "Camera and storage access are needed to upload photos", + onConfirm = { + permissionsState.launchMultiplePermissionRequest() + }, + onDismiss = { /* Handle dismissal */ } + ) + } + + // Request all permissions + else -> { + PermissionRequestScreen( + permissions = permissionsState.permissions, + onRequestPermissions = { + permissionsState.launchMultiplePermissionRequest() + } + ) + } + } +} + +@Composable +fun RationaleDialog( + title: String, + message: String, + onConfirm: () -> Unit, + onDismiss: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(title) }, + text = { Text(message) }, + confirmButton = { + TextButton(onClick = onConfirm) { + Text("Continue") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Cancel") + } + } + ) +} +``` + +## Lifecycle-Aware Permission Requests + +### Amethyst Pattern: POST_NOTIFICATIONS + +**File:** `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt` + +```kotlin +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun NotificationRegistration(accountViewModel: AccountViewModel) { + val context = LocalContext.current + + // Only request on Android 13+ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val notificationPermissionState = rememberPermissionState( + Manifest.permission.POST_NOTIFICATIONS + ) + + // Register for push notifications when permission is granted + if (notificationPermissionState.status.isGranted) { + LifecycleResumeEffect( + key1 = accountViewModel, + key2 = notificationPermissionState.status.isGranted + ) { + val scope = rememberCoroutineScope() + scope.launch(Dispatchers.IO) { + PushNotificationUtils.checkAndInit( + context = context, + accountViewModel = accountViewModel + ) + } + + onPauseOrDispose { + // Cleanup when composable pauses or disposes + } + } + } else { + // Show prompt to enable notifications + NotificationPermissionPrompt( + onEnableClick = { + notificationPermissionState.launchPermissionRequest() + } + ) + } + } +} + +@Composable +fun NotificationPermissionPrompt(onEnableClick: () -> Unit) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Icon( + imageVector = Icons.Default.Notifications, + contentDescription = null, + modifier = Modifier.size(48.dp) + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Enable Notifications", + style = MaterialTheme.typography.titleMedium + ) + Text( + text = "Get notified when someone mentions you or replies to your posts", + style = MaterialTheme.typography.bodyMedium + ) + Spacer(modifier = Modifier.height(16.dp)) + Button( + onClick = onEnableClick, + modifier = Modifier.fillMaxWidth() + ) { + Text("Enable Notifications") + } + } + } +} +``` + +## Permission Best Practices + +### 1. Request Contextually + +**Bad:** +```kotlin +// Requesting permission on app launch +@Composable +fun AppContent() { + val permissionState = rememberPermissionState(Manifest.permission.CAMERA) + + LaunchedEffect(Unit) { + // DON'T DO THIS - user doesn't know why + permissionState.launchPermissionRequest() + } +} +``` + +**Good:** +```kotlin +// Request when user explicitly wants to use camera +@Composable +fun QRScannerButton() { + val permissionState = rememberPermissionState(Manifest.permission.CAMERA) + + Button( + onClick = { + if (permissionState.status.isGranted) { + // Open scanner + } else { + // Request permission + permissionState.launchPermissionRequest() + } + } + ) { + Text("Scan QR Code") + } +} +``` + +### 2. Show Rationale + +```kotlin +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun LocationFeature() { + val locationPermissionState = rememberPermissionState( + Manifest.permission.ACCESS_COARSE_LOCATION + ) + + // Always show rationale first for sensitive permissions + if (!locationPermissionState.status.isGranted) { + LocationRationaleCard( + onEnableClick = { + locationPermissionState.launchPermissionRequest() + } + ) + } else { + LocationMap() + } +} + +@Composable +fun LocationRationaleCard(onEnableClick: () -> Unit) { + Card { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = "Why location access?", + style = MaterialTheme.typography.titleMedium + ) + Text( + text = "Location is used for geohashing your posts. " + + "This helps other users discover local content. " + + "Your exact location is never shared.", + style = MaterialTheme.typography.bodyMedium + ) + Spacer(modifier = Modifier.height(16.dp)) + Row { + OutlinedButton(onClick = { /* Skip */ }) { + Text("Skip") + } + Spacer(modifier = Modifier.width(8.dp)) + Button(onClick = onEnableClick) { + Text("Enable") + } + } + } + } +} +``` + +### 3. Handle Permanent Denial + +```kotlin +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun CameraFeatureWithSettings() { + val context = LocalContext.current + val cameraPermissionState = rememberPermissionState( + Manifest.permission.CAMERA + ) + + when { + cameraPermissionState.status.isGranted -> { + CameraPreview() + } + + cameraPermissionState.status.shouldShowRationale -> { + // User denied once, show rationale + RationaleDialog( + onConfirm = { cameraPermissionState.launchPermissionRequest() } + ) + } + + else -> { + // Might be permanently denied - offer settings + PermanentlyDeniedDialog( + onOpenSettings = { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", context.packageName, null) + } + context.startActivity(intent) + } + ) + } + } +} + +@Composable +fun PermanentlyDeniedDialog(onOpenSettings: () -> Unit) { + AlertDialog( + onDismissRequest = { }, + title = { Text("Permission Denied") }, + text = { + Text( + "Camera permission is required for QR scanning. " + + "Please enable it in Settings." + ) + }, + confirmButton = { + TextButton(onClick = onOpenSettings) { + Text("Open Settings") + } + }, + dismissButton = { + TextButton(onClick = { /* Cancel */ }) { + Text("Cancel") + } + } + ) +} +``` + +### 4. Version-Specific Permissions + +```kotlin +@Composable +fun StoragePermissionRequest() { + val permissions = remember { + buildList { + when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> { + add(Manifest.permission.READ_MEDIA_IMAGES) + add(Manifest.permission.READ_MEDIA_VIDEO) + } + Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> { + // Android 10-12: No permission needed for scoped storage + } + else -> { + // Android 9 and below + add(Manifest.permission.READ_EXTERNAL_STORAGE) + add(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + } + } + } + + if (permissions.isNotEmpty()) { + val permissionsState = rememberMultiplePermissionsState(permissions) + + if (!permissionsState.allPermissionsGranted) { + StoragePermissionUI( + onRequest = { permissionsState.launchMultiplePermissionRequest() } + ) + } else { + MediaPickerUI() + } + } else { + // No permission needed + MediaPickerUI() + } +} +``` + +## Permission Groups + +### Camera + Storage (Media Upload) + +```kotlin +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun MediaCaptureFeature() { + val mediaPermissions = buildList { + add(Manifest.permission.CAMERA) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + add(Manifest.permission.READ_MEDIA_IMAGES) + } else if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { + add(Manifest.permission.READ_EXTERNAL_STORAGE) + } + } + + val permissionsState = rememberMultiplePermissionsState(mediaPermissions) + + when { + permissionsState.allPermissionsGranted -> { + MediaCaptureUI() + } + else -> { + MediaPermissionScreen( + permissions = permissionsState.permissions, + onRequest = { permissionsState.launchMultiplePermissionRequest() } + ) + } + } +} +``` + +### Audio + Storage (Voice Recording) + +```kotlin +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun VoiceRecordingFeature() { + val audioPermissions = buildList { + add(Manifest.permission.RECORD_AUDIO) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + add(Manifest.permission.READ_MEDIA_AUDIO) + } else if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { + add(Manifest.permission.READ_EXTERNAL_STORAGE) + } + } + + val permissionsState = rememberMultiplePermissionsState(audioPermissions) + + if (permissionsState.allPermissionsGranted) { + AudioRecorderUI() + } else { + AudioPermissionScreen( + onRequest = { permissionsState.launchMultiplePermissionRequest() } + ) + } +} +``` + +## Testing Permissions + +### Grant Permission in Tests + +```kotlin +@get:Rule +val permissionRule = GrantPermissionRule.grant( + Manifest.permission.CAMERA, + Manifest.permission.READ_EXTERNAL_STORAGE +) + +@Test +fun testCameraFeatureWithPermission() { + composeTestRule.setContent { + CameraFeature() + } + + // Permission is already granted by rule + composeTestRule.onNodeWithText("Take Photo").assertExists() +} +``` + +### Test Permission Request Flow + +```kotlin +@Test +fun testPermissionRequestFlow() { + composeTestRule.setContent { + CameraFeature() + } + + // Initially shows permission request button + composeTestRule.onNodeWithText("Enable Camera").assertExists() + + // Click to request + composeTestRule.onNodeWithText("Enable Camera").performClick() + + // System permission dialog appears (can't test dialog itself) + // Would need UiAutomator to interact with system dialog +} +``` + +## Permission State Checking + +### Check Permission Before Action + +```kotlin +fun checkAndRequestCameraPermission( + context: Context, + permissionState: PermissionState, + onGranted: () -> Unit +) { + when { + permissionState.status.isGranted -> { + onGranted() + } + permissionState.status.shouldShowRationale -> { + // Show rationale dialog + } + else -> { + permissionState.launchPermissionRequest() + } + } +} +``` + +### Manual Permission Check (Non-Compose) + +```kotlin +fun hasCameraPermission(context: Context): Boolean { + return ContextCompat.checkSelfPermission( + context, + Manifest.permission.CAMERA + ) == PackageManager.PERMISSION_GRANTED +} + +fun requestCameraPermission(activity: ComponentActivity) { + ActivityCompat.requestPermissions( + activity, + arrayOf(Manifest.permission.CAMERA), + REQUEST_CAMERA_PERMISSION + ) +} + +// In Activity +override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray +) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + when (requestCode) { + REQUEST_CAMERA_PERMISSION -> { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // Permission granted + } else { + // Permission denied + } + } + } +} + +companion object { + private const val REQUEST_CAMERA_PERMISSION = 100 +} +``` + +## File Locations + +- `amethyst/src/main/AndroidManifest.xml` - Permission declarations +- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt` - Notification permission pattern +- `amethyst/build.gradle` - Accompanist dependency + +## Resources + +- [Accompanist Permissions Documentation](https://google.github.io/accompanist/permissions/) +- [Android Permissions Guide](https://developer.android.com/guide/topics/permissions/overview) +- [Request Runtime Permissions](https://developer.android.com/training/permissions/requesting) diff --git a/.claude/skills/android-expert/references/proguard-rules.md b/.claude/skills/android-expert/references/proguard-rules.md new file mode 100644 index 000000000..b09f3e1b5 --- /dev/null +++ b/.claude/skills/android-expert/references/proguard-rules.md @@ -0,0 +1,468 @@ +# Proguard Rules for Amethyst + +Proguard configuration for optimizing and obfuscating Android APK while preserving necessary code. + +## What is Proguard/R8? + +**R8** is Android's default code shrinker and obfuscator (replaced Proguard in AGP 3.4.0+). It: +- **Shrinks** code by removing unused classes/methods +- **Obfuscates** code by renaming classes/methods to short names +- **Optimizes** code by inlining methods and removing dead code + +## Amethyst Proguard Configuration + +**File:** `amethyst/proguard-rules.pro` + +### Keep Kotlin Metadata + +```proguard +# Kotlin metadata is required for reflection +-keep class kotlin.Metadata { *; } +-keep class kotlin.** { *; } +-dontwarn kotlin.** + +# Kotlin serialization +-keepattributes *Annotation*, InnerClasses +-dontnote kotlinx.serialization.AnnotationsKt +-dontnote kotlinx.serialization.SerializationKt + +-keep,includedescriptorclasses class com.vitorpamplona.**$$serializer { *; } +-keepclassmembers class com.vitorpamplona.** { + *** Companion; +} +-keepclasseswithmembers class com.vitorpamplona.** { + kotlinx.serialization.KSerializer serializer(...); +} +``` + +### Keep Nostr Event Classes + +```proguard +# Nostr events are serialized/deserialized +-keep class com.vitorpamplona.quartz.events.** { *; } +-keep class com.vitorpamplona.quartz.encoders.** { *; } + +# Keep event builders +-keep class com.vitorpamplona.quartz.builders.** { *; } + +# Keep tag classes +-keep class com.vitorpamplona.quartz.nip01Core.tags.** { *; } +``` + +### Keep Data Classes + +```proguard +# Data classes used in ViewModels and serialization +-keep @kotlinx.serialization.Serializable class * { *; } + +# Keep all data classes +-keep class com.vitorpamplona.amethyst.model.** { *; } +-keep class com.vitorpamplona.amethyst.service.model.** { *; } +``` + +### Keep Compose Classes + +```proguard +# Jetpack Compose +-keep class androidx.compose.** { *; } +-dontwarn androidx.compose.** + +# Compose runtime +-keep class androidx.compose.runtime.** { *; } + +# Compose UI +-keep class androidx.compose.ui.** { *; } + +# Material3 +-keep class androidx.compose.material3.** { *; } + +# Navigation Compose - Keep serializable routes +-keep class * implements java.io.Serializable { *; } +-keepclassmembers class * implements java.io.Serializable { + static final long serialVersionUID; + private static final java.io.ObjectStreamField[] serialPersistentFields; + !static !transient ; + private void writeObject(java.io.ObjectOutputStream); + private void readObject(java.io.ObjectInputStream); + java.lang.Object writeReplace(); + java.lang.Object readResolve(); +} +``` + +### Keep OkHttp/Retrofit + +```proguard +# OkHttp +-dontwarn okhttp3.** +-dontwarn okio.** +-keep class okhttp3.** { *; } +-keep class okio.** { *; } + +# OkHttp WebSockets (for Nostr relays) +-keep class okhttp3.internal.ws.** { *; } + +# Retrofit (if used) +-keepattributes Signature +-keepattributes Exceptions +-keep class retrofit2.** { *; } +``` + +### Keep Jackson (JSON) + +```proguard +# Jackson JSON library +-keep class com.fasterxml.jackson.** { *; } +-keep class org.codehaus.** { *; } +-keepclassmembers class * { + @com.fasterxml.jackson.annotation.* ; +} + +# Jackson polymorphic types +-keepattributes RuntimeVisibleAnnotations +-keep @com.fasterxml.jackson.annotation.JsonTypeInfo class * +``` + +### Keep Secp256k1 (Crypto) + +```proguard +# Secp256k1 native library +-keep class fr.acinq.secp256k1.** { *; } + +# Keep native methods +-keepclasseswithmembernames class * { + native ; +} +``` + +### Keep Tor + +```proguard +# Tor library +-keep class com.msopentech.thali.toronionproxy.** { *; } +-dontwarn com.msopentech.thali.toronionproxy.** +``` + +### Keep ExoPlayer (Media) + +```proguard +# ExoPlayer (Media3) +-keep class androidx.media3.** { *; } +-dontwarn androidx.media3.** + +-keep class com.google.android.exoplayer2.** { *; } +-dontwarn com.google.android.exoplayer2.** +``` + +### Keep Coil (Image Loading) + +```proguard +# Coil image loading +-keep class coil.** { *; } +-keep class coil3.** { *; } +-dontwarn coil.** +-dontwarn coil3.** +``` + +### Keep ViewModels + +```proguard +# ViewModel classes +-keep class * extends androidx.lifecycle.ViewModel { + (); +} + +# ViewModel factories +-keep class * extends androidx.lifecycle.ViewModelProvider$Factory { + (...); +} + +# Keep ViewModel constructors for reflection +-keepclassmembers class * extends androidx.lifecycle.ViewModel { + (...); +} +``` + +### Keep Parcelable + +```proguard +# Parcelable +-keep class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} + +-keepclassmembers class * implements android.os.Parcelable { + public ; + private ; +} +``` + +### Keep Enums + +```proguard +# Enums +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} +``` + +### Remove Logging (Production) + +```proguard +# Remove debug logging in release builds +-assumenosideeffects class android.util.Log { + public static *** d(...); + public static *** v(...); + public static *** i(...); +} + +# Keep error/warning logs +-assumenosideeffects class android.util.Log { + public static *** e(...) return false; + public static *** w(...) return false; +} +``` + +### Keep Crashlytics/Firebase + +```proguard +# Firebase Crashlytics +-keepattributes SourceFile,LineNumberTable +-keep public class * extends java.lang.Exception + +# Firebase +-keep class com.google.firebase.** { *; } +-dontwarn com.google.firebase.** +``` + +## Build Configuration + +### Enable R8 in build.gradle + +```gradle +android { + buildTypes { + release { + minifyEnabled = true + shrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + + debug { + minifyEnabled = false + } + } +} +``` + +### Multiple Proguard Files + +```gradle +android { + buildTypes { + release { + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + "proguard-quartz.pro", // Library-specific rules + "proguard-compose.pro" // Compose-specific rules + ) + } + } +} +``` + +## Debugging Proguard Issues + +### Generate Mapping File + +R8 generates `mapping.txt` in `app/build/outputs/mapping/release/`: + +``` +# Original class name -> Obfuscated name +com.vitorpamplona.amethyst.ui.MainActivity -> a.b.c: + void onCreate(Bundle) -> a +``` + +### Deobfuscate Stack Traces + +```bash +# Using retrace (part of Android SDK) +retrace.sh mapping.txt stacktrace.txt +``` + +### Enable Proguard Output + +```gradle +android { + buildTypes { + release { + proguardFiles(...) + + // Generate reports + postprocessing { + proguardFiles = [...] + obfuscate = true + optimizeCode = true + removeUnusedCode = true + } + } + } +} +``` + +**Output files:** +- `build/outputs/mapping/release/configuration.txt` - All Proguard rules applied +- `build/outputs/mapping/release/mapping.txt` - Obfuscation mappings +- `build/outputs/mapping/release/seeds.txt` - Classes kept by `-keep` rules +- `build/outputs/mapping/release/usage.txt` - Code removed by R8 + +### Test Release Build + +```bash +./gradlew assembleRelease + +# Install and test +adb install app/build/outputs/apk/release/app-release.apk +``` + +## Common Issues + +### Issue: NoSuchMethodException at Runtime + +**Cause:** Proguard removed or renamed a method used via reflection. + +**Solution:** +```proguard +-keep class com.example.YourClass { + public ; +} +``` + +### Issue: Serialization Fails + +**Cause:** Data class fields were renamed. + +**Solution:** +```proguard +-keep @kotlinx.serialization.Serializable class * { *; } +-keepclassmembers class * { + @kotlinx.serialization.SerialName ; +} +``` + +### Issue: Compose Navigation Crashes + +**Cause:** @Serializable route classes were obfuscated. + +**Solution:** +```proguard +# Keep all route classes +-keep @kotlinx.serialization.Serializable class com.vitorpamplona.amethyst.ui.navigation.routes.** { *; } +``` + +### Issue: Native Library Crashes + +**Cause:** Native method signatures were changed. + +**Solution:** +```proguard +-keepclasseswithmembernames class * { + native ; +} +``` + +## Optimization Tips + +### 1. Keep Only What's Necessary + +Don't use broad wildcards: +```proguard +# Bad - keeps everything +-keep class com.vitorpamplona.** { *; } + +# Good - keeps only specific packages +-keep class com.vitorpamplona.quartz.events.** { *; } +``` + +### 2. Test Thoroughly + +- Test all app features after enabling Proguard +- Test deep links and navigation +- Test serialization/deserialization +- Test external library integrations + +### 3. Use AGP's Proguard Analysis + +```gradle +android { + buildTypes { + release { + // Generate R8 configuration + android.debug.obsoleteApi = true + } + } +} +``` + +### 4. Analyze APK Size + +```bash +# Build release APK +./gradlew assembleRelease + +# Analyze APK with Android Studio +# Build > Analyze APK > Select app-release.apk +``` + +See `scripts/analyze-apk-size.sh` for automated analysis. + +## Product Flavor Specific Rules + +### Play Flavor (Firebase) + +```proguard +# proguard-play.pro +-keep class com.google.firebase.** { *; } +-keep class com.google.android.gms.** { *; } +``` + +### F-Droid Flavor (No Google Services) + +```proguard +# proguard-fdroid.pro +# UnifiedPush +-keep class org.unifiedpush.** { *; } +``` + +**Configure in build.gradle:** +```gradle +android { + flavorDimensions = ["channel"] + productFlavors { + create("play") { + dimension = "channel" + proguardFiles("proguard-play.pro") + } + create("fdroid") { + dimension = "channel" + proguardFiles("proguard-fdroid.pro") + } + } +} +``` + +## File Locations + +- `amethyst/proguard-rules.pro` - Main Proguard rules +- `amethyst/build/outputs/mapping/release/` - Proguard output files +- `amethyst/build.gradle` - Proguard configuration + +## Resources + +- [Android R8 Documentation](https://developer.android.com/build/shrink-code) +- [Proguard Manual](https://www.guardsquare.com/manual/configuration) +- [Kotlinx Serialization Proguard](https://github.com/Kotlin/kotlinx.serialization#android) diff --git a/.claude/skills/android-expert/scripts/analyze-apk-size.sh b/.claude/skills/android-expert/scripts/analyze-apk-size.sh new file mode 100755 index 000000000..023483130 --- /dev/null +++ b/.claude/skills/android-expert/scripts/analyze-apk-size.sh @@ -0,0 +1,230 @@ +#!/bin/bash +# +# APK Size Analysis Script for Amethyst +# +# Usage: +# ./analyze-apk-size.sh [apk-path] +# +# If no APK path provided, uses latest release build + +set -e + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Default APK path +DEFAULT_APK="amethyst/build/outputs/apk/release/amethyst-release.apk" +APK_PATH="${1:-$DEFAULT_APK}" + +# Check if APK exists +if [ ! -f "$APK_PATH" ]; then + echo -e "${RED}Error: APK not found at $APK_PATH${NC}" + echo "Build the APK first: ./gradlew :amethyst:assembleRelease" + exit 1 +fi + +echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}" +echo -e "${BLUE} Amethyst APK Size Analysis${NC}" +echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}" +echo + +# APK basic info +echo -e "${GREEN}APK Path:${NC} $APK_PATH" +APK_SIZE=$(du -h "$APK_PATH" | cut -f1) +APK_SIZE_BYTES=$(stat -f%z "$APK_PATH" 2>/dev/null || stat -c%s "$APK_PATH" 2>/dev/null) +echo -e "${GREEN}APK Size:${NC} $APK_SIZE ($APK_SIZE_BYTES bytes)" +echo + +# Extract APK to temp directory +TEMP_DIR=$(mktemp -d) +echo -e "${YELLOW}Extracting APK to $TEMP_DIR...${NC}" +unzip -q "$APK_PATH" -d "$TEMP_DIR" + +# Analyze APK contents +echo +echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}" +echo -e "${BLUE} Top-Level Contents${NC}" +echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}" +du -sh "$TEMP_DIR"/* | sort -hr + +# Analyze DEX files +echo +echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}" +echo -e "${BLUE} DEX Files (Code)${NC}" +echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}" +DEX_TOTAL=0 +for dex in "$TEMP_DIR"/*.dex; do + if [ -f "$dex" ]; then + DEX_NAME=$(basename "$dex") + DEX_SIZE=$(stat -f%z "$dex" 2>/dev/null || stat -c%s "$dex" 2>/dev/null) + DEX_SIZE_MB=$(echo "scale=2; $DEX_SIZE / 1024 / 1024" | bc) + echo -e " $DEX_NAME: ${GREEN}${DEX_SIZE_MB} MB${NC}" + DEX_TOTAL=$((DEX_TOTAL + DEX_SIZE)) + fi +done +DEX_TOTAL_MB=$(echo "scale=2; $DEX_TOTAL / 1024 / 1024" | bc) +echo -e "${YELLOW}Total DEX: $DEX_TOTAL_MB MB${NC}" + +# Analyze resources +echo +echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}" +echo -e "${BLUE} Resources${NC}" +echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}" +if [ -d "$TEMP_DIR/res" ]; then + RES_SIZE=$(du -sh "$TEMP_DIR/res" | cut -f1) + echo -e " res/: ${GREEN}$RES_SIZE${NC}" + + # Top resource folders + echo " Top resource folders:" + du -sh "$TEMP_DIR/res"/* | sort -hr | head -10 | sed 's/^/ /' +fi + +# Analyze assets +echo +if [ -d "$TEMP_DIR/assets" ]; then + ASSETS_SIZE=$(du -sh "$TEMP_DIR/assets" | cut -f1) + echo -e " assets/: ${GREEN}$ASSETS_SIZE${NC}" + + # Top asset files + echo " Top asset files:" + find "$TEMP_DIR/assets" -type f -exec du -h {} \; | sort -hr | head -10 | sed 's/^/ /' +fi + +# Analyze native libraries +echo +echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}" +echo -e "${BLUE} Native Libraries${NC}" +echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}" +if [ -d "$TEMP_DIR/lib" ]; then + LIB_SIZE=$(du -sh "$TEMP_DIR/lib" | cut -f1) + echo -e " lib/: ${GREEN}$LIB_SIZE${NC}" + + # By architecture + for arch in "$TEMP_DIR/lib"/*; do + if [ -d "$arch" ]; then + ARCH_NAME=$(basename "$arch") + ARCH_SIZE=$(du -sh "$arch" | cut -f1) + echo -e " $ARCH_NAME: ${GREEN}$ARCH_SIZE${NC}" + + # Top libraries in architecture + find "$arch" -type f -name "*.so" -exec du -h {} \; | sort -hr | head -5 | sed 's/^/ /' + fi + done +else + echo " No native libraries" +fi + +# Analyze Kotlin metadata +echo +echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}" +echo -e "${BLUE} Kotlin Metadata${NC}" +echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}" +if [ -d "$TEMP_DIR/kotlin" ]; then + KOTLIN_SIZE=$(du -sh "$TEMP_DIR/kotlin" | cut -f1) + echo -e " kotlin/: ${GREEN}$KOTLIN_SIZE${NC}" +fi + +# Method count (if dexdump available) +echo +echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}" +echo -e "${BLUE} Method Count${NC}" +echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}" + +# Try to find dexdump +DEXDUMP=$(which dexdump 2>/dev/null || find "$ANDROID_HOME/build-tools" -name dexdump 2>/dev/null | head -1 || echo "") + +if [ -n "$DEXDUMP" ] && [ -x "$DEXDUMP" ]; then + TOTAL_METHODS=0 + for dex in "$TEMP_DIR"/*.dex; do + if [ -f "$dex" ]; then + DEX_NAME=$(basename "$dex") + METHOD_COUNT=$("$DEXDUMP" -l xml "$dex" | grep -c "/dev/null | awk '{sum+=$1} END {print sum}' || echo "0") +RES_SIZE_BYTES=$(du -s "$TEMP_DIR/res" 2>/dev/null | awk '{print $1}' || echo "0") +ASSETS_SIZE_BYTES=$(du -s "$TEMP_DIR/assets" 2>/dev/null | awk '{print $1}' || echo "0") +LIB_SIZE_BYTES=$(du -s "$TEMP_DIR/lib" 2>/dev/null | awk '{print $1}' || echo "0") + +# Convert to KB for bc +APK_SIZE_KB=$((APK_SIZE_BYTES / 1024)) +CODE_PCT=$(echo "scale=1; $CODE_SIZE * 100 / $APK_SIZE_KB" | bc 2>/dev/null || echo "0") +RES_PCT=$(echo "scale=1; $RES_SIZE_BYTES * 100 / $APK_SIZE_KB" | bc 2>/dev/null || echo "0") +ASSETS_PCT=$(echo "scale=1; $ASSETS_SIZE_BYTES * 100 / $APK_SIZE_KB" | bc 2>/dev/null || echo "0") +LIB_PCT=$(echo "scale=1; $LIB_SIZE_BYTES * 100 / $APK_SIZE_KB" | bc 2>/dev/null || echo "0") + +echo -e " Code (DEX): ${CODE_PCT}%" +echo -e " Resources: ${RES_PCT}%" +echo -e " Assets: ${ASSETS_PCT}%" +echo -e " Native libs: ${LIB_PCT}%" + +# Recommendations +echo +echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}" +echo -e "${BLUE} Optimization Recommendations${NC}" +echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}" + +# Check if resources are large +if [ $(echo "$RES_PCT > 30" | bc 2>/dev/null || echo "0") -eq 1 ]; then + echo -e "${YELLOW} • Resources are ${RES_PCT}% of APK - consider:${NC}" + echo " - Enable resource shrinking (shrinkResources = true)" + echo " - Use WebP for images instead of PNG/JPG" + echo " - Remove unused resources" +fi + +# Check if native libs are large +if [ $(echo "$LIB_PCT > 40" | bc 2>/dev/null || echo "0") -eq 1 ]; then + echo -e "${YELLOW} • Native libraries are ${LIB_PCT}% of APK - consider:${NC}" + echo " - Use App Bundle to serve ABI-specific APKs" + echo " - Remove unused ABIs" +fi + +# Check if code is large +if [ $(echo "$CODE_PCT > 40" | bc 2>/dev/null || echo "0") -eq 1 ]; then + echo -e "${YELLOW} • Code is ${CODE_PCT}% of APK - consider:${NC}" + echo " - Enable Proguard/R8 (minifyEnabled = true)" + echo " - Review dependencies for bloat" + echo " - Enable code shrinking" +fi + +# General recommendations +echo -e "${GREEN} General optimizations:${NC}" +echo " - Use Android App Bundle (.aab) instead of APK" +echo " - Enable R8 optimization (minifyEnabled = true)" +echo " - Enable resource shrinking (shrinkResources = true)" +echo " - Analyze with APK Analyzer in Android Studio" + +# Cleanup +echo +echo -e "${YELLOW}Cleaning up temporary files...${NC}" +rm -rf "$TEMP_DIR" + +echo +echo -e "${GREEN}✓ Analysis complete${NC}" diff --git a/.claude/skills/desktop-expert/SKILL.md b/.claude/skills/desktop-expert/SKILL.md new file mode 100644 index 000000000..8cbcaaf8a --- /dev/null +++ b/.claude/skills/desktop-expert/SKILL.md @@ -0,0 +1,748 @@ +# Desktop Expert + +Expert in Compose Multiplatform Desktop development for AmethystMultiplatform. Covers Desktop-specific APIs, OS conventions, navigation patterns, and UX principles. + +## When to Use This Skill + +**Auto-invoke when:** +- Working with `desktopApp/` module files +- Using Desktop-only APIs: `Window`, `Tray`, `MenuBar`, `Dialog` +- Implementing keyboard shortcuts, menu systems +- Desktop navigation (NavigationRail, multi-window) +- File system operations (file pickers, drag-drop) +- OS-specific behavior (macOS, Windows, Linux) +- Desktop UX patterns (keyboard-first, tooltips) + +**Delegate to:** +- **kotlin-multiplatform**: Shared code questions, `jvmMain` source set structure +- **gradle-expert**: All `build.gradle.kts` issues, dependency conflicts +- **compose-expert**: General Compose patterns, `@Composable` best practices, Material3 + +## Scope + +**In scope:** +- Desktop-only Compose APIs +- Window management, positioning, state +- MenuBar + keyboard shortcuts (OS-specific) +- System Tray integration +- Desktop navigation patterns (NavigationRail) +- File dialogs, Desktop.getDesktop() +- OS conventions (macOS vs Windows vs Linux) +- Desktop UX principles + +**Out of scope:** +- Build configuration → **gradle-expert** +- Shared composables → **compose-expert** +- KMP structure → **kotlin-multiplatform** + +--- + +## 1. Desktop Entry Point + +### application {} DSL + +Desktop apps start with the `application {}` block: + +```kotlin +// desktopApp/src/jvmMain/kotlin/Main.kt +fun main() = application { + val windowState = rememberWindowState( + width = 1200.dp, + height = 800.dp, + position = WindowPosition.Aligned(Alignment.Center) + ) + + Window( + onCloseRequest = ::exitApplication, + state = windowState, + title = "Amethyst" + ) { + MenuBar { /* ... */ } + App() + } +} +``` + +**Key points:** +- `application {}` is the root composable (JVM-only) +- `Window()` creates the main window +- `rememberWindowState()` manages size/position +- `onCloseRequest` handles window close + +**See:** `desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt:87-138` + +--- + +## 2. Window Management + +### WindowState + +```kotlin +val windowState = rememberWindowState( + width = 1200.dp, + height = 800.dp, + position = WindowPosition.Aligned(Alignment.Center) +) + +Window( + state = windowState, + title = "My App", + resizable = true, + onCloseRequest = ::exitApplication +) { + // Content +} +``` + +### Multiple Windows + +```kotlin +fun main() = application { + var showSettings by remember { mutableStateOf(false) } + + Window(onCloseRequest = ::exitApplication, title = "Main") { + Button(onClick = { showSettings = true }) { + Text("Open Settings") + } + } + + if (showSettings) { + Window( + onCloseRequest = { showSettings = false }, + title = "Settings" + ) { + // Settings UI + } + } +} +``` + +**Pattern:** Use state to control window visibility conditionally. + +--- + +## 3. MenuBar System + +### Basic MenuBar + +```kotlin +Window(onCloseRequest = ::exitApplication, title = "App") { + MenuBar { + Menu("File") { + Item("New Note", onClick = { /* ... */ }) + Separator() + Item("Quit", onClick = ::exitApplication) + } + Menu("Edit") { + Item("Copy", onClick = { /* ... */ }) + Item("Paste", onClick = { /* ... */ }) + } + } + App() +} +``` + +### Keyboard Shortcuts (OS-Aware) + +**Current issue:** Main.kt hardcodes `ctrl = true` (Main.kt:105, 111, 117, 122, 123). + +**OS-specific shortcuts:** + +```kotlin +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyShortcut + +// Detect OS +val isMacOS = System.getProperty("os.name").lowercase().contains("mac") + +MenuBar { + Menu("File") { + Item( + "New Note", + shortcut = if (isMacOS) { + KeyShortcut(Key.N, meta = true) // Cmd+N on macOS + } else { + KeyShortcut(Key.N, ctrl = true) // Ctrl+N on Win/Linux + }, + onClick = { /* ... */ } + ) + Item( + "Settings", + shortcut = if (isMacOS) { + KeyShortcut(Key.Comma, meta = true) // Cmd+, on macOS + } else { + KeyShortcut(Key.Comma, ctrl = true) // Ctrl+, on Win/Linux + }, + onClick = { /* ... */ } + ) + Separator() + Item( + "Quit", + shortcut = if (isMacOS) { + KeyShortcut(Key.Q, meta = true) // Cmd+Q on macOS + } else { + KeyShortcut(Key.Q, ctrl = true) // Ctrl+Q on Win/Linux + }, + onClick = ::exitApplication + ) + } +} +``` + +**Standard shortcuts:** + +| Action | macOS | Windows/Linux | +|--------|-------|---------------| +| New | Cmd+N | Ctrl+N | +| Open | Cmd+O | Ctrl+O | +| Save | Cmd+S | Ctrl+S | +| Quit | Cmd+Q | Ctrl+Q (Alt+F4) | +| Settings | Cmd+, | Ctrl+, | +| Copy | Cmd+C | Ctrl+C | +| Paste | Cmd+V | Ctrl+V | +| Undo | Cmd+Z | Ctrl+Z | + +**See:** `references/keyboard-shortcuts.md` for full list. + +--- + +## 4. System Tray + +### Basic Tray + +```kotlin +application { + var isVisible by remember { mutableStateOf(true) } + + Tray( + icon = painterResource("icon.png"), + onAction = { isVisible = true }, + menu = { + Item("Show", onClick = { isVisible = true }) + Separator() + Item("Quit", onClick = ::exitApplication) + } + ) + + if (isVisible) { + Window( + onCloseRequest = { isVisible = false }, // Minimize to tray + title = "App" + ) { + // Content + } + } +} +``` + +**Pattern:** Hide window to tray instead of closing. + +**Current status:** Not implemented in Main.kt. Planned feature. + +--- + +## 5. Desktop Navigation Patterns + +### NavigationRail (Current Pattern) + +Desktop uses **NavigationRail** (vertical sidebar) instead of Android's bottom navigation. + +```kotlin +Row(Modifier.fillMaxSize()) { + // Sidebar + NavigationRail( + modifier = Modifier.width(80.dp).fillMaxHeight(), + containerColor = MaterialTheme.colorScheme.surfaceVariant + ) { + NavigationRailItem( + icon = { Icon(Icons.Default.Home, "Feed") }, + label = { Text("Feed") }, + selected = currentScreen == AppScreen.Feed, + onClick = { currentScreen = AppScreen.Feed } + ) + // More items... + } + + VerticalDivider() + + // Main content area + Box(Modifier.weight(1f).fillMaxHeight()) { + when (currentScreen) { + AppScreen.Feed -> FeedScreen() + // Other screens... + } + } +} +``` + +**See:** Main.kt:191-264 + +**Why NavigationRail?** +- Desktop has horizontal space (1200+ dp width) +- Vertical sidebar is standard desktop pattern +- Always visible (no tabs hidden) +- Icon + label both visible + +**Android comparison:** +- Android: `BottomNavigationBar` (horizontal, bottom) +- Desktop: `NavigationRail` (vertical, left) + +### Multi-Pane Layouts + +Desktop can leverage wide screens: + +```kotlin +Row { + // Left: Navigation + NavigationRail { /* ... */ } + + // Center: Main content + Box(Modifier.weight(0.6f)) { + FeedScreen() + } + + // Right: Details pane (desktop only) + if (selectedNote != null) { + VerticalDivider() + Box(Modifier.weight(0.4f)) { + NoteDetailPane(selectedNote) + } + } +} +``` + +**See:** `references/desktop-navigation.md` + +--- + +## 6. File System Integration + +### File Dialogs + +```kotlin +// File picker (load) +val fileDialog = FileDialog(Frame(), "Select file", FileDialog.LOAD) +fileDialog.isVisible = true +val filePath = fileDialog.file?.let { "${fileDialog.directory}$it" } + +// File picker (save) +val saveDialog = FileDialog(Frame(), "Save file", FileDialog.SAVE) +saveDialog.isVisible = true +val savePath = saveDialog.file?.let { "${saveDialog.directory}$it" } +``` + +**Note:** Compose Desktop doesn't have native file picker composable yet. Use AWT `FileDialog`. + +### Open External URLs + +```kotlin +// jvmMain actual implementation +actual fun openExternalUrl(url: String) { + if (Desktop.isDesktopSupported()) { + Desktop.getDesktop().browse(URI(url)) + } +} +``` + +**Pattern:** Define `expect` in `commonMain`, implement `actual` in `jvmMain`. + +### Drag & Drop (Future) + +```kotlin +// Compose Desktop drag-drop (experimental) +Box( + modifier = Modifier + .onExternalDrag( + onDragStart = { /* ... */ }, + onDrag = { /* ... */ }, + onDragExit = { /* ... */ }, + onDrop = { state -> + val dragData = state.dragData + // Handle dropped files + } + ) +) { + Text("Drop files here") +} +``` + +--- + +## 7. OS-Specific Conventions + +### Platform Detection + +```kotlin +val osName = System.getProperty("os.name").lowercase() + +val isMacOS = osName.contains("mac") +val isWindows = osName.contains("win") +val isLinux = osName.contains("nux") || osName.contains("nix") +``` + +### Menu Bar Placement + +| OS | Behavior | +|----|----------| +| **macOS** | System-wide menu bar at top of screen | +| **Windows** | In-window menu bar | +| **Linux** | Varies by desktop environment | + +Compose Desktop `MenuBar` adapts automatically. + +### Keyboard Modifier Keys + +| Modifier | macOS | Windows/Linux | +|----------|-------|---------------| +| Primary | `meta = true` (Cmd) | `ctrl = true` | +| Secondary | `ctrl = true` | `alt = true` | +| Shift | `shift = true` | `shift = true` | + +**Best practice:** Detect OS and use appropriate modifier. + +### System Tray Behavior + +| OS | Tray Location | +|----|---------------| +| **macOS** | Top-right menu bar | +| **Windows** | Bottom-right taskbar | +| **Linux** | Top panel (varies) | + +--- + +## 8. Desktop UX Principles + +### Keyboard-First Design + +**Every action should have:** +1. Mouse/touch interaction +2. Keyboard shortcut (if frequent) +3. Tooltip showing shortcut + +```kotlin +IconButton( + onClick = { /* refresh */ }, + modifier = Modifier.tooltipArea( + tooltip = { + Text("Refresh (${if (isMacOS) "Cmd" else "Ctrl"}+R)") + } + ) +) { + Icon(Icons.Default.Refresh, "Refresh") +} +``` + +### Tooltip Best Practices + +- Show keyboard shortcut in tooltip +- Use native modifier name (Cmd vs Ctrl) +- Brief description + shortcut + +### Context Menus + +Right-click should show context menu: + +```kotlin +// Future: Compose Desktop context menu API +Box( + modifier = Modifier.contextMenuArea( + items = { + listOf( + ContextMenuItem("Copy") { /* ... */ }, + ContextMenuItem("Paste") { /* ... */ } + ) + } + ) +) { + // Content +} +``` + +**Current:** Use popup or custom implementation. + +### Window State Persistence + +Save/restore window size/position: + +```kotlin +// Save on close +windowState.size // DpSize +windowState.position // WindowPosition + +// Restore on launch +val savedWidth = preferences.getInt("window.width", 1200) +val savedHeight = preferences.getInt("window.height", 800) + +val windowState = rememberWindowState( + width = savedWidth.dp, + height = savedHeight.dp +) +``` + +--- + +## 9. Desktop Module Structure + +``` +desktopApp/ +├── build.gradle.kts # Desktop-only build config +└── src/ + └── jvmMain/ + ├── kotlin/ + │ └── com/vitorpamplona/amethyst/desktop/ + │ ├── Main.kt # Entry point, Window, MenuBar + │ ├── network/ + │ │ ├── DesktopHttpClient.kt + │ │ └── DesktopRelayConnectionManager.kt + │ └── ui/ + │ ├── FeedScreen.kt # Desktop screen layouts + │ └── LoginScreen.kt + └── resources/ + ├── icon.icns # macOS icon + ├── icon.ico # Windows icon + └── icon.png # Linux icon +``` + +**Key files:** +- `Main.kt:87-138` - `application {}`, `Window`, `MenuBar` +- `Main.kt:183-264` - NavigationRail pattern +- `build.gradle.kts:45-73` - Desktop packaging config + +--- + +## 10. Packaging & Distribution + +### Build Configuration + +```kotlin +// desktopApp/build.gradle.kts +compose.desktop { + application { + mainClass = "com.vitorpamplona.amethyst.desktop.MainKt" + + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + + packageName = "Amethyst" + packageVersion = "1.0.0" + + macOS { + bundleID = "com.vitorpamplona.amethyst.desktop" + iconFile.set(project.file("src/jvmMain/resources/icon.icns")) + } + + windows { + iconFile.set(project.file("src/jvmMain/resources/icon.ico")) + menuGroup = "Amethyst" + } + + linux { + iconFile.set(project.file("src/jvmMain/resources/icon.png")) + } + } + } +} +``` + +**See:** desktopApp/build.gradle.kts:45-73 + +### Gradle Tasks + +```bash +# Run desktop app +./gradlew :desktopApp:run + +# Package for distribution +./gradlew :desktopApp:packageDmg # macOS +./gradlew :desktopApp:packageMsi # Windows +./gradlew :desktopApp:packageDeb # Linux +``` + +**Delegate packaging issues to gradle-expert.** + +--- + +## Common Patterns + +### Pattern: OS-Aware Shortcuts Helper + +```kotlin +// commons/src/jvmMain/kotlin/shortcuts/ShortcutUtils.kt +object DesktopShortcuts { + private val isMacOS = System.getProperty("os.name") + .lowercase().contains("mac") + + fun primary(key: Key) = if (isMacOS) { + KeyShortcut(key, meta = true) + } else { + KeyShortcut(key, ctrl = true) + } + + fun primaryShift(key: Key) = if (isMacOS) { + KeyShortcut(key, meta = true, shift = true) + } else { + KeyShortcut(key, ctrl = true, shift = true) + } + + val modifierName = if (isMacOS) "Cmd" else "Ctrl" +} + +// Usage in MenuBar +Item( + "New Note", + shortcut = DesktopShortcuts.primary(Key.N), + onClick = { /* ... */ } +) +``` + +### Pattern: Shared Composables, Platform Layouts + +```kotlin +// commons/commonMain - Shared NoteCard +@Composable +fun NoteCard(note: NoteDisplayData) { + // Business logic, UI component (shared) +} + +// desktopApp/jvmMain - Desktop layout +@Composable +fun FeedScreen() { + Column { + FeedHeader(/* ... */) // Shared from commons + LazyColumn { + items(notes) { note -> + NoteCard(note) // Shared composable + } + } + } +} + +// amethyst/androidMain - Android layout +@Composable +fun FeedScreen() { + Scaffold( + bottomBar = { BottomNavigationBar() } // Android-specific + ) { + LazyColumn { + items(notes) { note -> + NoteCard(note) // Same shared composable + } + } + } +} +``` + +**Philosophy:** Share UI components (cards, buttons), keep navigation/layout platform-specific. + +--- + +## Resources + +### Official Documentation +- [Desktop-only API | Kotlin Multiplatform](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-desktop-components.html) +- [Top-level windows management](https://kotlinlang.org/docs/multiplatform/compose-desktop-top-level-windows-management.html) +- [Tray/MenuBar Tutorial](https://github.com/JetBrains/compose-multiplatform/blob/master/tutorials/Tray_Notifications_MenuBar_new/README.md) + +### Bundled References +- `references/desktop-compose-apis.md` - Complete Desktop API catalog +- `references/desktop-navigation.md` - NavigationRail vs BottomNav patterns +- `references/keyboard-shortcuts.md` - Standard shortcuts by OS +- `references/os-detection.md` - Platform detection patterns + +### Codebase Examples +- Main.kt:87-138 - Window, MenuBar entry point +- Main.kt:183-264 - NavigationRail pattern +- FeedScreen.kt:49-136 - Desktop screen layout +- LoginScreen.kt:44-97 - Centered desktop login + +--- + +## Questions to Ask + +When working on desktop features: + +1. **Should this be shared or desktop-only?** + - Business logic → Share in `commonMain` + - Navigation/layout → Keep in `desktopApp/jvmMain` + +2. **Does this need OS-specific behavior?** + - Keyboard shortcuts → Yes (Cmd vs Ctrl) + - File paths → Yes (separators) + - Icons → Yes (per-OS formats) + +3. **Is there a desktop UX convention?** + - Check MenuBar standards + - Consider keyboard-first design + - Tooltips for all actions + +4. **Does this need gradle-expert?** + - Any `build.gradle.kts` changes → Delegate + - Packaging/distribution issues → Delegate + +--- + +## Anti-Patterns + +❌ **Hardcoding Ctrl everywhere** +```kotlin +// Main.kt:105 - Current issue +shortcut = KeyShortcut(Key.N, ctrl = true) // Wrong on macOS +``` + +✅ **OS-aware shortcuts** +```kotlin +shortcut = DesktopShortcuts.primary(Key.N) +``` + +--- + +❌ **Using Android navigation on Desktop** +```kotlin +Scaffold(bottomBar = { BottomNavigationBar() }) // Wrong for desktop +``` + +✅ **NavigationRail for desktop** +```kotlin +Row { + NavigationRail { /* ... */ } + MainContent() +} +``` + +--- + +❌ **No keyboard shortcuts** +```kotlin +IconButton(onClick = { refresh() }) { + Icon(Icons.Default.Refresh, "Refresh") +} +``` + +✅ **Shortcuts + tooltips** +```kotlin +IconButton( + onClick = { refresh() }, + modifier = Modifier.tooltipArea("Refresh (Cmd+R)") +) { + Icon(Icons.Default.Refresh, "Refresh") +} +``` + +--- + +## Next Steps + +When implementing desktop features: + +1. **Read** `references/desktop-compose-apis.md` for API catalog +2. **Check** `references/keyboard-shortcuts.md` for standard shortcuts +3. **Reference** Main.kt:87-264 for current patterns +4. **Test** on all 3 platforms (macOS, Windows, Linux) if possible +5. **Delegate** build issues to gradle-expert +6. **Share** UI components via compose-expert, not desktop-expert + +--- + +**Version:** 1.0.0 +**Last Updated:** 2025-12-30 +**Codebase Reference:** AmethystMultiplatform commit 258c4e011 diff --git a/.claude/skills/desktop-expert/references/desktop-compose-apis.md b/.claude/skills/desktop-expert/references/desktop-compose-apis.md new file mode 100644 index 000000000..bcb773321 --- /dev/null +++ b/.claude/skills/desktop-expert/references/desktop-compose-apis.md @@ -0,0 +1,597 @@ +# Desktop Compose APIs Catalog + +Complete reference for Compose Multiplatform Desktop-only APIs. + +## Window Management + +### application + +Root entry point for desktop apps. + +```kotlin +fun main() = application { + Window(onCloseRequest = ::exitApplication) { + Text("Hello Desktop") + } +} +``` + +### Window + +Creates a window. + +```kotlin +Window( + onCloseRequest: () -> Unit, + state: WindowState = rememberWindowState(), + visible: Boolean = true, + title: String = "Untitled", + icon: Painter? = null, + undecorated: Boolean = false, + transparent: Boolean = false, + resizable: Boolean = true, + enabled: Boolean = true, + focusable: Boolean = true, + alwaysOnTop: Boolean = false, + onPreviewKeyEvent: ((KeyEvent) -> Boolean) = { false }, + onKeyEvent: ((KeyEvent) -> Boolean) = { false }, + content: @Composable FrameWindowScope.() -> Unit +) +``` + +**Example:** +```kotlin +val windowState = rememberWindowState( + width = 1200.dp, + height = 800.dp, + position = WindowPosition.Aligned(Alignment.Center) +) + +Window( + onCloseRequest = ::exitApplication, + state = windowState, + title = "My App", + resizable = true +) { + // Content +} +``` + +### rememberWindowState + +Manages window size and position. + +```kotlin +@Composable +fun rememberWindowState( + placement: WindowPlacement = WindowPlacement.Floating, + isMinimized: Boolean = false, + position: WindowPosition = WindowPosition.PlatformDefault, + width: Dp = Dp.Unspecified, + height: Dp = Dp.Unspecified +): WindowState +``` + +**WindowPlacement:** +- `Floating` - Normal window +- `Maximized` - Fullscreen +- `Fullscreen` - Fullscreen without decorations + +**WindowPosition:** +- `PlatformDefault` - OS decides +- `Aligned(alignment)` - Center, TopStart, etc. +- `Absolute(x, y)` - Fixed position in pixels + +### DialogWindow + +Modal dialog. + +```kotlin +DialogWindow( + onCloseRequest: () -> Unit, + state: DialogState = rememberDialogState(), + visible: Boolean = true, + title: String = "Dialog", + icon: Painter? = null, + undecorated: Boolean = false, + transparent: Boolean = false, + resizable: Boolean = true, + enabled: Boolean = true, + focusable: Boolean = true, + content: @Composable DialogWindowScope.() -> Unit +) +``` + +**Example:** +```kotlin +var showDialog by remember { mutableStateOf(false) } + +if (showDialog) { + DialogWindow( + onCloseRequest = { showDialog = false }, + title = "Confirm" + ) { + Column(Modifier.padding(16.dp)) { + Text("Are you sure?") + Row { + Button(onClick = { showDialog = false }) { + Text("Cancel") + } + Button(onClick = { /* confirm */ }) { + Text("OK") + } + } + } + } +} +``` + +--- + +## MenuBar + +### MenuBar + +Native menu bar for windows. + +```kotlin +@Composable +fun FrameWindowScope.MenuBar( + content: @Composable MenuBarScope.() -> Unit +) +``` + +**Example:** +```kotlin +Window(onCloseRequest = ::exitApplication) { + MenuBar { + Menu("File") { + Item("New", onClick = { /* ... */ }) + Item("Open", onClick = { /* ... */ }) + Separator() + Item("Quit", onClick = ::exitApplication) + } + Menu("Edit") { + Item("Copy", onClick = { /* ... */ }) + Item("Paste", onClick = { /* ... */ }) + } + } +} +``` + +### Menu + +Top-level menu. + +```kotlin +@Composable +fun MenuBarScope.Menu( + text: String, + mnemonic: Char? = null, + enabled: Boolean = true, + content: @Composable MenuScope.() -> Unit +) +``` + +### Item + +Menu item. + +```kotlin +@Composable +fun MenuScope.Item( + text: String, + onClick: () -> Unit, + shortcut: KeyShortcut? = null, + mnemonic: Char? = null, + enabled: Boolean = true, + icon: Painter? = null +) +``` + +**With keyboard shortcut:** +```kotlin +Item( + text = "Save", + onClick = { save() }, + shortcut = KeyShortcut(Key.S, ctrl = true), + icon = painterResource("save.png") +) +``` + +### Separator + +Menu separator line. + +```kotlin +@Composable +fun MenuScope.Separator() +``` + +### CheckboxItem + +Toggleable menu item. + +```kotlin +@Composable +fun MenuScope.CheckboxItem( + text: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + shortcut: KeyShortcut? = null, + mnemonic: Char? = null, + enabled: Boolean = true +) +``` + +**Example:** +```kotlin +var darkMode by remember { mutableStateOf(false) } + +Menu("View") { + CheckboxItem( + text = "Dark Mode", + checked = darkMode, + onCheckedChange = { darkMode = it }, + shortcut = KeyShortcut(Key.D, ctrl = true) + ) +} +``` + +### RadioButtonItem + +Radio button menu item. + +```kotlin +@Composable +fun MenuScope.RadioButtonItem( + text: String, + selected: Boolean, + onClick: () -> Unit, + shortcut: KeyShortcut? = null, + mnemonic: Char? = null, + enabled: Boolean = true +) +``` + +--- + +## System Tray + +### Tray + +System tray icon with menu. + +```kotlin +@Composable +fun ApplicationScope.Tray( + icon: Painter, + state: TrayState = rememberTrayState(), + tooltip: String? = null, + onAction: () -> Unit = {}, + menu: @Composable MenuScope.() -> Unit = {} +) +``` + +**Example:** +```kotlin +application { + var isVisible by remember { mutableStateOf(true) } + + Tray( + icon = painterResource("tray-icon.png"), + tooltip = "My App", + onAction = { isVisible = true }, + menu = { + Item("Show Window", onClick = { isVisible = true }) + Separator() + Item("Quit", onClick = ::exitApplication) + } + ) + + if (isVisible) { + Window( + onCloseRequest = { isVisible = false }, + title = "App" + ) { + // Content + } + } +} +``` + +### rememberTrayState + +Manages tray state. + +```kotlin +@Composable +fun rememberTrayState(): TrayState +``` + +--- + +## Notifications + +### Notification (via Tray) + +Show desktop notifications through tray. + +```kotlin +val trayState = rememberTrayState() + +Tray( + icon = painterResource("icon.png"), + state = trayState +) + +// Send notification +LaunchedEffect(Unit) { + trayState.sendNotification( + Notification( + title = "Message", + message = "You have a new message", + type = Notification.Type.Info + ) + ) +} +``` + +**Notification types:** +- `Info` - Information +- `Warning` - Warning +- `Error` - Error + +--- + +## Keyboard + +### KeyShortcut + +Keyboard shortcut definition. + +```kotlin +data class KeyShortcut( + val key: Key, + val ctrl: Boolean = false, + val meta: Boolean = false, + val alt: Boolean = false, + val shift: Boolean = false +) +``` + +**Examples:** +```kotlin +// Ctrl+S (Windows/Linux) +KeyShortcut(Key.S, ctrl = true) + +// Cmd+S (macOS) +KeyShortcut(Key.S, meta = true) + +// Ctrl+Shift+N +KeyShortcut(Key.N, ctrl = true, shift = true) + +// Alt+F4 +KeyShortcut(Key.F4, alt = true) +``` + +### onPreviewKeyEvent / onKeyEvent + +Window-level keyboard handlers. + +```kotlin +Window( + onCloseRequest = ::exitApplication, + onPreviewKeyEvent = { event -> + if (event.key == Key.Escape && event.type == KeyEventType.KeyDown) { + // Handle Escape + true // Consume event + } else { + false // Propagate + } + } +) { + // Content +} +``` + +--- + +## Mouse + +### PointerMoveFilter (Deprecated, use Modifier.pointerInput) + +```kotlin +Box( + modifier = Modifier + .pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + val event = awaitPointerEvent() + // Handle mouse events + } + } + } +) +``` + +### Mouse cursor + +```kotlin +Box( + modifier = Modifier.pointerHoverIcon( + icon = PointerIcon(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)) + ) +) { + Text("Hover me") +} +``` + +**Cursor types:** +- `DEFAULT_CURSOR` +- `HAND_CURSOR` +- `TEXT_CURSOR` +- `CROSSHAIR_CURSOR` +- `WAIT_CURSOR` +- `MOVE_CURSOR` +- `E_RESIZE_CURSOR`, `W_RESIZE_CURSOR`, etc. + +--- + +## Drag & Drop (Experimental) + +### onExternalDrag + +Handle drag-and-drop from external sources. + +```kotlin +Box( + modifier = Modifier + .size(200.dp) + .background(Color.LightGray) + .onExternalDrag( + onDragStart = { externalDragValue -> + println("Drag started") + }, + onDrag = { externalDragValue -> + println("Dragging: ${externalDragValue.dragData}") + }, + onDragExit = { + println("Drag exited") + }, + onDrop = { externalDragValue -> + val dragData = externalDragValue.dragData + when (dragData) { + is DragData.FilesList -> { + println("Files dropped: ${dragData.readFiles()}") + } + is DragData.Text -> { + println("Text dropped: ${dragData.readText()}") + } + else -> {} + } + } + ) +) { + Text("Drop files here", Modifier.align(Alignment.Center)) +} +``` + +--- + +## Resources + +### painterResource + +Load images from resources. + +```kotlin +val icon = painterResource("icon.png") + +Icon( + painter = icon, + contentDescription = "App icon" +) +``` + +**Resource location:** `src/jvmMain/resources/` + +--- + +## Platform Integration + +### Desktop.getDesktop() (AWT) + +Access system desktop features (not Compose API, but commonly used). + +```kotlin +import java.awt.Desktop +import java.net.URI + +// Open URL in browser +if (Desktop.isDesktopSupported()) { + Desktop.getDesktop().browse(URI("https://example.com")) +} + +// Open file with default app +Desktop.getDesktop().open(File("/path/to/file.pdf")) + +// Open email client +Desktop.getDesktop().mail(URI("mailto:user@example.com")) +``` + +### FileDialog (AWT) + +File picker dialogs. + +```kotlin +import java.awt.FileDialog +import java.awt.Frame + +// Open file +val fileDialog = FileDialog(Frame(), "Select file", FileDialog.LOAD) +fileDialog.isVisible = true +val selectedFile = fileDialog.file +val directory = fileDialog.directory + +// Save file +val saveDialog = FileDialog(Frame(), "Save file", FileDialog.SAVE) +saveDialog.file = "document.txt" +saveDialog.isVisible = true +``` + +--- + +## SwingPanel (Interop) + +Embed Swing components in Compose. + +```kotlin +import androidx.compose.ui.awt.SwingPanel +import javax.swing.JButton + +SwingPanel( + factory = { + JButton("Swing Button").apply { + addActionListener { + println("Swing button clicked") + } + } + }, + modifier = Modifier.size(200.dp, 50.dp) +) +``` + +--- + +## ComposePanel (Reverse Interop) + +Embed Compose in Swing. + +```kotlin +import androidx.compose.ui.awt.ComposePanel +import javax.swing.JFrame + +val frame = JFrame("Swing Frame") +val composePanel = ComposePanel() + +composePanel.setContent { + Text("Compose in Swing") +} + +frame.contentPane.add(composePanel) +frame.setSize(400, 300) +frame.isVisible = true +``` + +--- + +## Version Requirements + +- **Kotlin:** 2.0+ +- **Compose Multiplatform:** 1.7.0+ +- **JVM Target:** 11+ (recommend 21) + +**See also:** +- [Official Desktop API docs](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-desktop-components.html) +- [Compose Multiplatform repo](https://github.com/JetBrains/compose-multiplatform) diff --git a/.claude/skills/desktop-expert/references/desktop-navigation.md b/.claude/skills/desktop-expert/references/desktop-navigation.md new file mode 100644 index 000000000..01d192767 --- /dev/null +++ b/.claude/skills/desktop-expert/references/desktop-navigation.md @@ -0,0 +1,464 @@ +# Desktop Navigation Patterns + +Comparison of mobile vs desktop navigation patterns in AmethystMultiplatform. + +## Core Difference + +| Platform | Pattern | Location | Rationale | +|----------|---------|----------|-----------| +| **Android** | Bottom Navigation Bar | Horizontal, bottom | Thumb reach on mobile | +| **Desktop** | Navigation Rail | Vertical, left sidebar | Horizontal screen space | + +--- + +## Desktop: NavigationRail + +### Current Implementation + +**File:** `desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt:191-264` + +```kotlin +@Composable +fun MainContent( + currentScreen: AppScreen, + onScreenChange: (AppScreen) -> Unit, + // ... +) { + Row(Modifier.fillMaxSize()) { + // LEFT: Vertical Sidebar (NavigationRail) + NavigationRail( + modifier = Modifier.width(80.dp).fillMaxHeight(), + containerColor = MaterialTheme.colorScheme.surfaceVariant + ) { + Spacer(Modifier.height(16.dp)) + + // Top navigation items + NavigationRailItem( + icon = { Icon(Icons.Default.Home, "Feed") }, + label = { Text("Feed") }, + selected = currentScreen == AppScreen.Feed, + onClick = { onScreenChange(AppScreen.Feed) } + ) + + NavigationRailItem( + icon = { Icon(Icons.Default.Search, "Search") }, + label = { Text("Search") }, + selected = currentScreen == AppScreen.Search, + onClick = { onScreenChange(AppScreen.Search) } + ) + + NavigationRailItem( + icon = { Icon(Icons.Default.Email, "Messages") }, + label = { Text("DMs") }, + selected = currentScreen == AppScreen.Messages, + onClick = { onScreenChange(AppScreen.Messages) } + ) + + NavigationRailItem( + icon = { Icon(Icons.Default.Notifications, "Notifications") }, + label = { Text("Alerts") }, + selected = currentScreen == AppScreen.Notifications, + onClick = { onScreenChange(AppScreen.Notifications) } + ) + + NavigationRailItem( + icon = { Icon(Icons.Default.Person, "Profile") }, + label = { Text("Profile") }, + selected = currentScreen == AppScreen.Profile, + onClick = { onScreenChange(AppScreen.Profile) } + ) + + // Push Settings to bottom + Spacer(Modifier.weight(1f)) + + HorizontalDivider(Modifier.padding(horizontal = 16.dp)) + + NavigationRailItem( + icon = { Icon(Icons.Default.Settings, "Settings") }, + label = { Text("Settings") }, + selected = currentScreen == AppScreen.Settings, + onClick = { onScreenChange(AppScreen.Settings) } + ) + + Spacer(Modifier.height(16.dp)) + } + + VerticalDivider() + + // RIGHT: Main Content Area + Box(modifier = Modifier.weight(1f).fillMaxHeight().padding(24.dp)) { + when (currentScreen) { + AppScreen.Feed -> FeedScreen(relayManager) + AppScreen.Search -> SearchPlaceholder() + AppScreen.Messages -> MessagesPlaceholder() + AppScreen.Notifications -> NotificationsPlaceholder() + AppScreen.Profile -> ProfileScreen(account, accountManager) + AppScreen.Settings -> RelaySettingsScreen(relayManager) + } + } + } +} +``` + +### Layout Structure + +``` +┌────────────────────────────────────────┐ +│ [Menu Bar: File, Edit, View, Help] │ ← MenuBar (OS-native) +├──────┬─────────────────────────────────┤ +│ │ │ +│ [🏠] │ │ +│ Feed │ │ +│ │ │ +│ [🔍] │ Main Content Area │ +│Search│ (Feed, Messages, etc.) │ +│ │ │ +│ [✉️] │ │ +│ DMs │ │ +│ │ │ +│ [🔔] │ │ +│Alerts│ │ +│ │ │ +│ [👤] │ │ +│Profile │ +│ │ │ +│ ─ │ │ +│ [⚙️] │ │ +│Settings │ +│ │ │ +└──────┴─────────────────────────────────┘ + 80dp Remaining width (weight=1f) +``` + +### Key Features + +1. **Always visible:** All nav items visible at once +2. **Icon + Label:** Both shown (not just icons) +3. **Vertical list:** Natural reading order +4. **Settings at bottom:** Separated by divider + Spacer.weight(1f) +5. **80dp width:** Standard NavigationRail width + +--- + +## Android: BottomNavigationBar (Future) + +### Expected Implementation + +**Location:** `amethyst/src/androidMain/kotlin/...` (not yet implemented) + +```kotlin +@Composable +fun MainScreen( + currentScreen: AppScreen, + onScreenChange: (AppScreen) -> Unit +) { + Scaffold( + bottomBar = { + NavigationBar { + NavigationBarItem( + icon = { Icon(Icons.Default.Home, "Feed") }, + label = { Text("Feed") }, + selected = currentScreen == AppScreen.Feed, + onClick = { onScreenChange(AppScreen.Feed) } + ) + NavigationBarItem( + icon = { Icon(Icons.Default.Search, "Search") }, + label = { Text("Search") }, + selected = currentScreen == AppScreen.Search, + onClick = { onScreenChange(AppScreen.Search) } + ) + NavigationBarItem( + icon = { Icon(Icons.Default.Email, "Messages") }, + label = { Text("Messages") }, + selected = currentScreen == AppScreen.Messages, + onClick = { onScreenChange(AppScreen.Messages) } + ) + NavigationBarItem( + icon = { Icon(Icons.Default.Person, "Profile") }, + label = { Text("Profile") }, + selected = currentScreen == AppScreen.Profile, + onClick = { onScreenChange(AppScreen.Profile) } + ) + } + } + ) { paddingValues -> + Box(Modifier.padding(paddingValues)) { + when (currentScreen) { + AppScreen.Feed -> FeedScreen() + AppScreen.Search -> SearchScreen() + AppScreen.Messages -> MessagesScreen() + AppScreen.Profile -> ProfileScreen() + // Settings accessed via Profile or overflow menu + } + } + } +} +``` + +### Layout Structure + +``` +┌─────────────────────────────────────┐ +│ │ +│ │ +│ Main Content Area │ +│ (Feed, Messages, etc.) │ +│ │ +│ │ +│ │ +├─────────────────────────────────────┤ +│ [🏠] [🔍] [✉️] [👤] │ ← NavigationBar +│ Feed Search DMs Profile │ +└─────────────────────────────────────┘ +``` + +### Key Differences from Desktop + +1. **Bottom placement:** Thumb reach +2. **Horizontal layout:** Limited vertical space +3. **Fewer items:** 3-5 primary destinations +4. **Label optional:** Can hide on small screens +5. **Settings hidden:** In profile or overflow + +--- + +## Shared Navigation State + +Both platforms use the same `AppScreen` enum from `commons`. + +**File:** `commons/src/commonMain/kotlin/.../navigation/AppScreen.kt` (expected) + +```kotlin +// Shared navigation destinations +enum class AppScreen { + Feed, + Search, + Messages, + Notifications, + Profile, + Settings +} +``` + +**State management (shared):** + +```kotlin +// commons/src/jvmAndroid/kotlin/.../navigation/NavigationViewModel.kt +class NavigationViewModel : ViewModel() { + private val _currentScreen = MutableStateFlow(AppScreen.Feed) + val currentScreen: StateFlow = _currentScreen.asStateFlow() + + fun navigateTo(screen: AppScreen) { + _currentScreen.value = screen + } +} +``` + +--- + +## Multi-Pane Desktop Layout (Advanced) + +Desktop can utilize horizontal space for multi-pane layouts. + +### Two-Pane Layout + +```kotlin +Row(Modifier.fillMaxSize()) { + // Left: NavigationRail (fixed 80dp) + NavigationRail { /* ... */ } + + VerticalDivider() + + // Center: Main content (60% width) + Box(Modifier.weight(0.6f)) { + FeedScreen() + } + + // Right: Detail pane (40% width, conditional) + if (selectedNote != null) { + VerticalDivider() + Box(Modifier.weight(0.4f)) { + NoteDetailPane(selectedNote) + } + } +} +``` + +### Layout: + +``` +┌──────┬───────────────────┬─────────────┐ +│ │ │ │ +│ Nav │ Feed List │ Detail │ +│ Rail │ (60%) │ Pane │ +│ │ │ (40%) │ +│ │ │ │ +└──────┴───────────────────┴─────────────┘ + 80dp weight(0.6f) weight(0.4f) +``` + +**Use cases:** +- Email: List + message detail +- Notes: List + editor +- Settings: Categories + options + +--- + +## Keyboard Navigation + +Desktop should support keyboard navigation. + +### Tab Navigation + +```kotlin +NavigationRail( + modifier = Modifier.focusable() +) { + NavigationRailItem( + icon = { Icon(Icons.Default.Home, "Feed") }, + label = { Text("Feed") }, + selected = currentScreen == AppScreen.Feed, + onClick = { onScreenChange(AppScreen.Feed) }, + modifier = Modifier.focusable() + ) + // More items... +} +``` + +### Keyboard Shortcuts + +```kotlin +Window( + onPreviewKeyEvent = { event -> + when { + event.key == Key.One && event.isCtrlPressed -> + onScreenChange(AppScreen.Feed).also { true } + event.key == Key.Two && event.isCtrlPressed -> + onScreenChange(AppScreen.Search).also { true } + event.key == Key.Three && event.isCtrlPressed -> + onScreenChange(AppScreen.Messages).also { true } + else -> false + } + } +) { + // Content +} +``` + +**Standard:** +- Ctrl+1: First nav item (Feed) +- Ctrl+2: Second nav item (Search) +- Ctrl+3: Third nav item (Messages) +- Ctrl+Comma: Settings + +--- + +## Navigation Transitions + +### Desktop (Instant) + +No fancy animations. Instant switch. + +```kotlin +Box { + when (currentScreen) { + AppScreen.Feed -> FeedScreen() + AppScreen.Search -> SearchScreen() + } +} +``` + +### Android (Animated, Future) + +Can use Navigation Compose for transitions. + +```kotlin +NavHost(navController, startDestination = "feed") { + composable("feed") { FeedScreen() } + composable("search") { SearchScreen() } +} +``` + +--- + +## Best Practices + +### Desktop NavigationRail + +✅ **DO:** +- Keep width 72-80dp +- Show both icon and label +- Use Spacer.weight(1f) for bottom items +- Separate sections with HorizontalDivider +- Limit to 5-7 primary items + +❌ **DON'T:** +- Use bottom navigation on desktop +- Hide labels (plenty of space) +- Make it collapsible (not standard) +- Use hamburger menu (not desktop pattern) + +### Android NavigationBar + +✅ **DO:** +- Limit to 3-5 items +- Use bottom placement +- Consider label visibility on small screens +- Use standard icons + +❌ **DON'T:** +- Put more than 5 items +- Use top placement (deprecated) +- Put critical actions only in nav bar + +--- + +## Migration Strategy + +When adding Android support: + +1. **Extract shared state:** Move `AppScreen` to `commons/commonMain` +2. **Platform layouts:** Keep `NavigationRail` in `desktopApp/jvmMain`, `NavigationBar` in `amethyst/androidMain` +3. **Shared screens:** Composables in `commons/commonMain` (FeedScreen content) +4. **Platform chrome:** Navigation containers in platform modules + +**Example:** + +```kotlin +// commons/commonMain - Shared screen content +@Composable +fun FeedContent(notes: List) { + LazyColumn { + items(notes) { note -> + NoteCard(note) + } + } +} + +// desktopApp/jvmMain - Desktop wrapper +@Composable +fun FeedScreen() { + Column { + FeedHeader() // Desktop-specific header + FeedContent(notes) // Shared content + } +} + +// amethyst/androidMain - Android wrapper +@Composable +fun FeedScreen() { + Scaffold( + topBar = { TopAppBar { Text("Feed") } } + ) { + FeedContent(notes) // Same shared content + } +} +``` + +--- + +## References + +- **Current Desktop:** Main.kt:191-264 +- **Material3 NavigationRail:** [Material Design Docs](https://m3.material.io/components/navigation-rail) +- **Material3 NavigationBar:** [Material Design Docs](https://m3.material.io/components/navigation-bar) diff --git a/.claude/skills/desktop-expert/references/keyboard-shortcuts.md b/.claude/skills/desktop-expert/references/keyboard-shortcuts.md new file mode 100644 index 000000000..fd9257ef4 --- /dev/null +++ b/.claude/skills/desktop-expert/references/keyboard-shortcuts.md @@ -0,0 +1,400 @@ +# Keyboard Shortcuts Reference + +Standard keyboard shortcuts for desktop applications across macOS, Windows, and Linux. + +## Primary Modifier Keys + +| Platform | Primary | Secondary | Tertiary | +|----------|---------|-----------|----------| +| **macOS** | Cmd (⌘) / `meta` | Option (⌥) / `alt` | Ctrl (⌃) / `ctrl` | +| **Windows** | Ctrl / `ctrl` | Alt / `alt` | Win / `meta` | +| **Linux** | Ctrl / `ctrl` | Alt / `alt` | Super / `meta` | + +**In Compose Desktop:** + +```kotlin +// macOS +KeyShortcut(Key.N, meta = true) // Cmd+N + +// Windows/Linux +KeyShortcut(Key.N, ctrl = true) // Ctrl+N +``` + +--- + +## File Operations + +| Action | macOS | Windows | Linux | Notes | +|--------|-------|---------|-------|-------| +| **New** | Cmd+N | Ctrl+N | Ctrl+N | Create new | +| **Open** | Cmd+O | Ctrl+O | Ctrl+O | Open file | +| **Save** | Cmd+S | Ctrl+S | Ctrl+S | Save current | +| **Save As** | Cmd+Shift+S | Ctrl+Shift+S | Ctrl+Shift+S | Save with new name | +| **Close** | Cmd+W | Ctrl+W | Ctrl+W | Close window/tab | +| **Quit** | Cmd+Q | Ctrl+Q | Ctrl+Q | Exit app | +| **Print** | Cmd+P | Ctrl+P | Ctrl+P | Print | + +**Compose Implementation:** + +```kotlin +val isMacOS = System.getProperty("os.name").lowercase().contains("mac") + +MenuBar { + Menu("File") { + Item( + "New Note", + shortcut = if (isMacOS) { + KeyShortcut(Key.N, meta = true) + } else { + KeyShortcut(Key.N, ctrl = true) + }, + onClick = { createNewNote() } + ) + Item( + "Save", + shortcut = if (isMacOS) { + KeyShortcut(Key.S, meta = true) + } else { + KeyShortcut(Key.S, ctrl = true) + }, + onClick = { save() } + ) + Separator() + Item( + "Quit", + shortcut = if (isMacOS) { + KeyShortcut(Key.Q, meta = true) + } else { + KeyShortcut(Key.Q, ctrl = true) + }, + onClick = ::exitApplication + ) + } +} +``` + +--- + +## Edit Operations + +| Action | macOS | Windows | Linux | Notes | +|--------|-------|---------|-------|-------| +| **Undo** | Cmd+Z | Ctrl+Z | Ctrl+Z | Universal | +| **Redo** | Cmd+Shift+Z | Ctrl+Y | Ctrl+Y | Windows/Linux use Y | +| **Cut** | Cmd+X | Ctrl+X | Ctrl+X | Universal | +| **Copy** | Cmd+C | Ctrl+C | Ctrl+C | Universal | +| **Paste** | Cmd+V | Ctrl+V | Ctrl+V | Universal | +| **Select All** | Cmd+A | Ctrl+A | Ctrl+A | Universal | +| **Find** | Cmd+F | Ctrl+F | Ctrl+F | Search | +| **Find Next** | Cmd+G | F3 | F3 | Next result | +| **Replace** | Cmd+Option+F | Ctrl+H | Ctrl+H | Find & replace | + +**Note:** Undo/Redo typically handled by text fields automatically. + +--- + +## Navigation + +| Action | macOS | Windows | Linux | Notes | +|--------|-------|---------|-------|-------| +| **Tab 1** | Cmd+1 | Ctrl+1 | Ctrl+1 | First tab/view | +| **Tab 2** | Cmd+2 | Ctrl+2 | Ctrl+2 | Second tab/view | +| **Tab 3** | Cmd+3 | Ctrl+3 | Ctrl+3 | Third tab/view | +| **Next Tab** | Cmd+Option+→ | Ctrl+Tab | Ctrl+Tab | Cycle forward | +| **Prev Tab** | Cmd+Option+← | Ctrl+Shift+Tab | Ctrl+Shift+Tab | Cycle back | +| **Go Back** | Cmd+[ | Alt+← | Alt+← | Browser-style | +| **Go Forward** | Cmd+] | Alt+→ | Alt+→ | Browser-style | + +**Compose Implementation:** + +```kotlin +Window( + onPreviewKeyEvent = { event -> + if (event.type == KeyEventType.KeyDown) { + when { + event.key == Key.One && event.isPrimaryPressed() -> { + navigateTo(AppScreen.Feed) + true + } + event.key == Key.Two && event.isPrimaryPressed() -> { + navigateTo(AppScreen.Search) + true + } + event.key == Key.Three && event.isPrimaryPressed() -> { + navigateTo(AppScreen.Messages) + true + } + else -> false + } + } else false + } +) { + // Content +} + +// Helper extension +fun KeyEvent.isPrimaryPressed() = if (isMacOS) isMetaPressed else isCtrlPressed +``` + +--- + +## Window Management + +| Action | macOS | Windows | Linux | Notes | +|--------|-------|---------|-------|-------| +| **New Window** | Cmd+N | Ctrl+N | Ctrl+N | New instance | +| **Close Window** | Cmd+W | Alt+F4 | Alt+F4 | Close current | +| **Minimize** | Cmd+M | Win+Down | Super+Down | Minimize to dock/taskbar | +| **Maximize** | Cmd+Ctrl+F | Win+Up | Super+Up | Fullscreen/maximize | +| **Hide App** | Cmd+H | - | - | macOS only | +| **Switch Window** | Cmd+` | Alt+Tab | Alt+Tab | Between app windows | + +**Note:** Window management often handled by OS, not app shortcuts. + +--- + +## App-Specific (Amethyst) + +### Nostr Actions + +| Action | macOS | Windows | Linux | Description | +|--------|-------|---------|-------|-------------| +| **New Note** | Cmd+N | Ctrl+N | Ctrl+N | Compose new post | +| **Refresh Feed** | Cmd+R | Ctrl+R | Ctrl+R | Reload timeline | +| **Search** | Cmd+K | Ctrl+K | Ctrl+K | Quick search | +| **DMs** | Cmd+Shift+M | Ctrl+Shift+M | Ctrl+Shift+M | Open messages | +| **Settings** | Cmd+, | Ctrl+, | Ctrl+, | Open preferences | +| **Notifications** | Cmd+Shift+N | Ctrl+Shift+N | Ctrl+Shift+N | View alerts | + +**Implementation:** + +```kotlin +MenuBar { + Menu("File") { + Item( + "New Note", + shortcut = DesktopShortcuts.primary(Key.N), + onClick = { showComposeDialog() } + ) + Item( + "Settings", + shortcut = DesktopShortcuts.primary(Key.Comma), + onClick = { navigateTo(AppScreen.Settings) } + ) + } + Menu("View") { + Item( + "Refresh Feed", + shortcut = DesktopShortcuts.primary(Key.R), + onClick = { refreshFeed() } + ) + Item( + "Search", + shortcut = DesktopShortcuts.primary(Key.K), + onClick = { focusSearch() } + ) + } +} +``` + +--- + +## Accessibility + +| Action | macOS | Windows | Linux | Description | +|--------|-------|---------|-------|-------------| +| **Zoom In** | Cmd++ | Ctrl++ | Ctrl++ | Increase size | +| **Zoom Out** | Cmd+- | Ctrl+- | Ctrl+- | Decrease size | +| **Reset Zoom** | Cmd+0 | Ctrl+0 | Ctrl+0 | Default size | +| **Help** | Cmd+? | F1 | F1 | Show help | + +--- + +## Best Practices + +### 1. OS-Aware Helper + +Create a utility for OS detection: + +```kotlin +// commons/src/jvmMain/kotlin/utils/PlatformShortcuts.kt +object DesktopShortcuts { + private val isMacOS = System.getProperty("os.name") + .lowercase() + .contains("mac") + + fun primary(key: Key) = if (isMacOS) { + KeyShortcut(key, meta = true) + } else { + KeyShortcut(key, ctrl = true) + } + + fun primaryShift(key: Key) = if (isMacOS) { + KeyShortcut(key, meta = true, shift = true) + } else { + KeyShortcut(key, ctrl = true, shift = true) + } + + fun primaryAlt(key: Key) = if (isMacOS) { + KeyShortcut(key, meta = true, alt = true) + } else { + KeyShortcut(key, ctrl = true, alt = true) + } + + val modifierName = if (isMacOS) "Cmd" else "Ctrl" + val secondaryName = if (isMacOS) "Option" else "Alt" +} +``` + +**Usage:** + +```kotlin +Item( + "Save", + shortcut = DesktopShortcuts.primary(Key.S), + onClick = { save() } +) +``` + +### 2. Show Shortcuts in Tooltips + +```kotlin +IconButton( + onClick = { refresh() }, + modifier = Modifier.tooltipArea { + Text("Refresh (${DesktopShortcuts.modifierName}+R)") + } +) { + Icon(Icons.Default.Refresh, "Refresh") +} +``` + +### 3. Shortcuts Menu + +Provide a "Keyboard Shortcuts" help menu: + +```kotlin +Menu("Help") { + Item("Keyboard Shortcuts", onClick = { showShortcutsDialog() }) +} + +// Dialog content +@Composable +fun ShortcutsDialog() { + Dialog(onDismissRequest = { /* close */ }) { + Surface { + Column(Modifier.padding(16.dp)) { + Text("Keyboard Shortcuts", style = MaterialTheme.typography.headlineMedium) + Spacer(Modifier.height(16.dp)) + + ShortcutRow("New Note", "${DesktopShortcuts.modifierName}+N") + ShortcutRow("Save", "${DesktopShortcuts.modifierName}+S") + ShortcutRow("Search", "${DesktopShortcuts.modifierName}+K") + ShortcutRow("Settings", "${DesktopShortcuts.modifierName}+,") + // ... + } + } + } +} + +@Composable +fun ShortcutRow(action: String, shortcut: String) { + Row( + Modifier.fillMaxWidth().padding(vertical = 4.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(action, style = MaterialTheme.typography.bodyMedium) + Text( + shortcut, + style = MaterialTheme.typography.bodySmall, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +} +``` + +### 4. Avoid Conflicts + +**Check for OS-level shortcuts:** + +| macOS Reserved | Description | +|----------------|-------------| +| Cmd+Tab | Switch apps | +| Cmd+Space | Spotlight | +| Cmd+H | Hide window | +| Cmd+M | Minimize | +| Cmd+Q | Quit | +| Cmd+W | Close window | + +**Windows Reserved:** + +| Windows Reserved | Description | +|-----------------|-------------| +| Win+D | Show desktop | +| Win+E | File Explorer | +| Win+L | Lock screen | +| Alt+Tab | Switch apps | +| Alt+F4 | Close window | + +**Don't override these unless critical.** + +--- + +## Testing Shortcuts + +```kotlin +// Test OS detection +@Test +fun testOsDetection() { + val osName = System.getProperty("os.name") + println("OS: $osName") + + val isMacOS = osName.lowercase().contains("mac") + println("Is macOS: $isMacOS") + + val shortcut = if (isMacOS) { + KeyShortcut(Key.N, meta = true) + } else { + KeyShortcut(Key.N, ctrl = true) + } + + println("Primary modifier for New: $shortcut") +} +``` + +--- + +## Current Issues in Amethyst + +**Main.kt:105-123** hardcodes `ctrl = true`: + +```kotlin +// ❌ WRONG: Hardcoded Ctrl (doesn't work on macOS) +Item( + "New Note", + shortcut = KeyShortcut(Key.N, ctrl = true), // Should be Cmd on macOS + onClick = { /* ... */ } +) +``` + +**Fix:** + +```kotlin +// ✅ CORRECT: OS-aware +Item( + "New Note", + shortcut = DesktopShortcuts.primary(Key.N), + onClick = { /* ... */ } +) +``` + +--- + +## References + +- [macOS Keyboard Shortcuts](https://support.apple.com/en-us/102650) +- [Windows Keyboard Shortcuts](https://support.microsoft.com/en-us/windows/keyboard-shortcuts-in-windows-dcc61a57-8ff0-cffe-9796-cb9706c75eec) +- [GNOME Keyboard Shortcuts](https://help.gnome.org/users/gnome-help/stable/shell-keyboard-shortcuts.html) +- [Material Design: Keyboard Shortcuts](https://m3.material.io/foundations/interaction/keyboard) +- [Compose Desktop: Keyboard Events](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-desktop-keyboard.html) diff --git a/.claude/skills/desktop-expert/references/os-detection.md b/.claude/skills/desktop-expert/references/os-detection.md new file mode 100644 index 000000000..0330308f5 --- /dev/null +++ b/.claude/skills/desktop-expert/references/os-detection.md @@ -0,0 +1,579 @@ +# OS Detection & Platform-Specific Code + +Patterns for detecting operating system and implementing platform-specific behavior in Compose Desktop. + +## OS Detection + +### Basic Detection + +```kotlin +val osName = System.getProperty("os.name").lowercase() + +val isMacOS = osName.contains("mac") +val isWindows = osName.contains("win") +val isLinux = osName.contains("nux") || osName.contains("nix") +``` + +### System Properties + +```kotlin +// OS name +System.getProperty("os.name") +// Examples: "Mac OS X", "Windows 10", "Linux" + +// OS version +System.getProperty("os.version") +// Examples: "14.2.1", "10.0", "6.5.0-14-generic" + +// OS architecture +System.getProperty("os.arch") +// Examples: "aarch64", "x86_64", "amd64" + +// User home directory +System.getProperty("user.home") +// Examples: "/Users/username", "C:\Users\username", "/home/username" + +// File separator +System.getProperty("file.separator") +// Examples: "/" (Unix), "\" (Windows) + +// Path separator +System.getProperty("path.separator") +// Examples: ":" (Unix), ";" (Windows) +``` + +--- + +## PlatformDetector Utility + +Create a centralized utility for platform detection. + +**File:** `commons/src/jvmMain/kotlin/utils/PlatformDetector.kt` + +```kotlin +package com.vitorpamplona.amethyst.commons.utils + +object PlatformDetector { + private val osName = System.getProperty("os.name").lowercase() + + val isMacOS: Boolean = osName.contains("mac") + val isWindows: Boolean = osName.contains("win") + val isLinux: Boolean = osName.contains("nux") || osName.contains("nix") + + val platform: Platform = when { + isMacOS -> Platform.MacOS + isWindows -> Platform.Windows + isLinux -> Platform.Linux + else -> Platform.Unknown + } + + enum class Platform { + MacOS, + Windows, + Linux, + Unknown + } + + // File paths + val fileSeparator: String = System.getProperty("file.separator") + val pathSeparator: String = System.getProperty("path.separator") + + // User directories + val userHome: String = System.getProperty("user.home") + + val appDataDir: String = when (platform) { + Platform.MacOS -> "$userHome/Library/Application Support" + Platform.Windows -> System.getenv("APPDATA") ?: "$userHome\\AppData\\Roaming" + Platform.Linux -> System.getenv("XDG_CONFIG_HOME") ?: "$userHome/.config" + Platform.Unknown -> userHome + } + + // Modifier key names + val primaryModifierName: String = if (isMacOS) "Cmd" else "Ctrl" + val secondaryModifierName: String = if (isMacOS) "Option" else "Alt" + + fun platformSpecific( + macOS: () -> Unit = {}, + windows: () -> Unit = {}, + linux: () -> Unit = {}, + fallback: () -> Unit = {} + ) { + when (platform) { + Platform.MacOS -> macOS() + Platform.Windows -> windows() + Platform.Linux -> linux() + Platform.Unknown -> fallback() + } + } +} +``` + +**Usage:** + +```kotlin +// Simple check +if (PlatformDetector.isMacOS) { + // macOS-specific code +} + +// Pattern matching +when (PlatformDetector.platform) { + Platform.MacOS -> setupMacDock() + Platform.Windows -> setupWindowsTray() + Platform.Linux -> setupLinuxTray() + Platform.Unknown -> showWarning() +} + +// Platform-specific execution +PlatformDetector.platformSpecific( + macOS = { setupMacMenuBar() }, + windows = { setupWindowsMenu() }, + linux = { setupLinuxMenu() } +) + +// File paths +val configPath = "${PlatformDetector.appDataDir}${PlatformDetector.fileSeparator}amethyst" +``` + +--- + +## Platform-Specific UI + +### Keyboard Shortcuts Helper + +```kotlin +package com.vitorpamplona.amethyst.commons.utils + +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyShortcut + +object DesktopShortcuts { + private val isMacOS = PlatformDetector.isMacOS + + fun primary(key: Key) = if (isMacOS) { + KeyShortcut(key, meta = true) + } else { + KeyShortcut(key, ctrl = true) + } + + fun primaryShift(key: Key) = if (isMacOS) { + KeyShortcut(key, meta = true, shift = true) + } else { + KeyShortcut(key, ctrl = true, shift = true) + } + + fun primaryAlt(key: Key) = if (isMacOS) { + KeyShortcut(key, meta = true, alt = true) + } else { + KeyShortcut(key, ctrl = true, alt = true) + } + + val modifierName = PlatformDetector.primaryModifierName + val secondaryName = PlatformDetector.secondaryModifierName + + fun formatShortcut(key: String, withPrimary: Boolean = true): String { + return if (withPrimary) "$modifierName+$key" else key + } +} +``` + +### File Paths Helper + +```kotlin +package com.vitorpamplona.amethyst.commons.utils + +import java.io.File + +object FilePaths { + private val separator = PlatformDetector.fileSeparator + + fun join(vararg parts: String): String { + return parts.joinToString(separator) + } + + fun appConfig(appName: String): String { + return join(PlatformDetector.appDataDir, appName) + } + + fun appCache(appName: String): String { + return when (PlatformDetector.platform) { + PlatformDetector.Platform.MacOS -> + join(PlatformDetector.userHome, "Library", "Caches", appName) + PlatformDetector.Platform.Windows -> + join(System.getenv("LOCALAPPDATA") ?: "${PlatformDetector.userHome}\\AppData\\Local", appName) + PlatformDetector.Platform.Linux -> + join(System.getenv("XDG_CACHE_HOME") ?: "${PlatformDetector.userHome}/.cache", appName) + else -> join(PlatformDetector.userHome, ".cache", appName) + } + } + + fun ensureDirectory(path: String): File { + return File(path).apply { + if (!exists()) { + mkdirs() + } + } + } +} + +// Usage +val configDir = FilePaths.ensureDirectory(FilePaths.appConfig("amethyst")) +val cacheDir = FilePaths.ensureDirectory(FilePaths.appCache("amethyst")) +``` + +--- + +## Platform-Specific Features + +### Open External URL + +```kotlin +// commons/src/commonMain/kotlin/utils/ExternalUrl.kt +expect fun openExternalUrl(url: String) + +// commons/src/jvmMain/kotlin/utils/ExternalUrl.jvm.kt +import java.awt.Desktop +import java.net.URI + +actual fun openExternalUrl(url: String) { + if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { + Desktop.getDesktop().browse(URI(url)) + } +} +``` + +### File Picker + +```kotlin +// Platform-specific file picker +fun showFilePicker( + title: String = "Select file", + mode: FilePickerMode = FilePickerMode.Load +): String? { + val fileDialog = java.awt.FileDialog( + java.awt.Frame(), + title, + when (mode) { + FilePickerMode.Load -> java.awt.FileDialog.LOAD + FilePickerMode.Save -> java.awt.FileDialog.SAVE + } + ) + + // macOS-specific: Enable file selection features + if (PlatformDetector.isMacOS) { + System.setProperty("apple.awt.fileDialogForDirectories", "false") + } + + fileDialog.isVisible = true + + return fileDialog.file?.let { "${fileDialog.directory}$it" } +} + +enum class FilePickerMode { + Load, + Save +} +``` + +### Directory Picker (macOS) + +```kotlin +fun showDirectoryPicker(title: String = "Select directory"): String? { + if (PlatformDetector.isMacOS) { + // macOS-specific directory picker + System.setProperty("apple.awt.fileDialogForDirectories", "true") + } + + val fileDialog = java.awt.FileDialog(java.awt.Frame(), title, java.awt.FileDialog.LOAD) + fileDialog.isVisible = true + + if (PlatformDetector.isMacOS) { + System.setProperty("apple.awt.fileDialogForDirectories", "false") + } + + return fileDialog.directory +} +``` + +--- + +## Window Decorations + +### macOS-Specific + +```kotlin +// Unified title bar (macOS Big Sur+) +if (PlatformDetector.isMacOS) { + Window( + undecorated = false, + transparent = true, + // ... + ) { + // Custom title bar + } +} +``` + +### Windows-Specific + +```kotlin +// Custom window chrome (Windows) +if (PlatformDetector.isWindows) { + Window( + undecorated = true, + // Custom decorations + ) { + Column { + // Custom title bar with min/max/close buttons + WindowTitleBar() + // Content + } + } +} +``` + +--- + +## System Tray Icons + +Different icon formats per OS: + +```kotlin +fun getTrayIcon(): Painter { + return when (PlatformDetector.platform) { + Platform.MacOS -> painterResource("tray-icon-mac.png") // Template icon + Platform.Windows -> painterResource("tray-icon-win.ico") + Platform.Linux -> painterResource("tray-icon-linux.png") + else -> painterResource("tray-icon.png") + } +} + +// macOS: Template icons (black/transparent) +// Windows: ICO format, 16x16 +// Linux: PNG, typically 24x24 +``` + +--- + +## Native Notifications + +```kotlin +// Platform-specific notification implementation +fun sendNotification(title: String, message: String) { + PlatformDetector.platformSpecific( + macOS = { + // macOS: Use NSUserNotification (via tray) + trayState.sendNotification( + Notification(title, message, Notification.Type.Info) + ) + }, + windows = { + // Windows: Use Windows toast notifications + trayState.sendNotification( + Notification(title, message, Notification.Type.Info) + ) + }, + linux = { + // Linux: Use libnotify (via tray) + trayState.sendNotification( + Notification(title, message, Notification.Type.Info) + ) + } + ) +} +``` + +--- + +## Architecture Detection + +```kotlin +object ArchDetector { + private val arch = System.getProperty("os.arch").lowercase() + + val isArm: Boolean = arch.contains("aarch") || arch.contains("arm") + val isX64: Boolean = arch.contains("x86_64") || arch.contains("amd64") + val isX86: Boolean = arch.contains("x86") && !isX64 + + val architecture: Architecture = when { + isArm -> Architecture.ARM + isX64 -> Architecture.X64 + isX86 -> Architecture.X86 + else -> Architecture.Unknown + } + + enum class Architecture { + ARM, + X64, + X86, + Unknown + } +} + +// Usage: Load correct native library +fun loadNativeLib() { + val libName = when { + PlatformDetector.isMacOS && ArchDetector.isArm -> "libsecp256k1-macos-arm64" + PlatformDetector.isMacOS && ArchDetector.isX64 -> "libsecp256k1-macos-x64" + PlatformDetector.isWindows && ArchDetector.isX64 -> "libsecp256k1-win-x64" + PlatformDetector.isLinux && ArchDetector.isX64 -> "libsecp256k1-linux-x64" + else -> throw UnsupportedOperationException("Unsupported platform") + } + + System.loadLibrary(libName) +} +``` + +--- + +## Testing Platform Detection + +```kotlin +@Test +fun testPlatformDetection() { + println("OS: ${System.getProperty("os.name")}") + println("Version: ${System.getProperty("os.version")}") + println("Arch: ${System.getProperty("os.arch")}") + println() + println("Is macOS: ${PlatformDetector.isMacOS}") + println("Is Windows: ${PlatformDetector.isWindows}") + println("Is Linux: ${PlatformDetector.isLinux}") + println("Platform: ${PlatformDetector.platform}") + println() + println("User home: ${PlatformDetector.userHome}") + println("App data: ${PlatformDetector.appDataDir}") + println("File separator: ${PlatformDetector.fileSeparator}") +} + +// Example output (macOS): +// OS: Mac OS X +// Version: 14.2.1 +// Arch: aarch64 +// +// Is macOS: true +// Is Windows: false +// Is Linux: false +// Platform: MacOS +// +// User home: /Users/username +// App data: /Users/username/Library/Application Support +// File separator: / +``` + +--- + +## Best Practices + +### 1. Centralize Detection + +✅ **DO:** Use PlatformDetector singleton +```kotlin +if (PlatformDetector.isMacOS) { /* ... */ } +``` + +❌ **DON'T:** Repeat detection everywhere +```kotlin +if (System.getProperty("os.name").lowercase().contains("mac")) { /* ... */ } +``` + +### 2. Use expect/actual for Platform APIs + +```kotlin +// commonMain +expect fun openFile(path: String) + +// jvmMain (Desktop) +actual fun openFile(path: String) { + Desktop.getDesktop().open(File(path)) +} + +// androidMain +actual fun openFile(path: String) { + context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(path))) +} +``` + +### 3. Graceful Degradation + +```kotlin +fun openBrowser(url: String) { + try { + if (Desktop.isDesktopSupported()) { + Desktop.getDesktop().browse(URI(url)) + } else { + // Fallback: Copy to clipboard + Toolkit.getDefaultToolkit().systemClipboard.setContents( + StringSelection(url), + null + ) + showMessage("URL copied to clipboard: $url") + } + } catch (e: Exception) { + showError("Failed to open browser: ${e.message}") + } +} +``` + +### 4. Test on All Platforms + +Always test platform-specific code on: +- macOS (Intel + Apple Silicon if possible) +- Windows (10/11) +- Linux (Ubuntu/Fedora) + +--- + +## Common Patterns + +### Pattern: Config File Location + +```kotlin +fun getConfigFile(filename: String): File { + val configDir = when (PlatformDetector.platform) { + Platform.MacOS -> + File("${PlatformDetector.userHome}/Library/Application Support/Amethyst") + Platform.Windows -> + File("${System.getenv("APPDATA")}\\Amethyst") + Platform.Linux -> + File("${PlatformDetector.userHome}/.config/amethyst") + else -> + File("${PlatformDetector.userHome}/.amethyst") + } + + if (!configDir.exists()) { + configDir.mkdirs() + } + + return File(configDir, filename) +} + +// Usage +val settingsFile = getConfigFile("settings.json") +``` + +### Pattern: Platform-Specific Resources + +```kotlin +fun getPlatformIcon(name: String): Painter { + val extension = when (PlatformDetector.platform) { + Platform.MacOS -> "icns" + Platform.Windows -> "ico" + else -> "png" + } + + return painterResource("$name.$extension") +} + +// Resources: +// src/jvmMain/resources/app-icon.icns (macOS) +// src/jvmMain/resources/app-icon.ico (Windows) +// src/jvmMain/resources/app-icon.png (Linux) +``` + +--- + +## References + +- [System Properties (Java)](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/System.html#getProperties()) +- [Desktop API (Java)](https://docs.oracle.com/en/java/javase/21/docs/api/java.desktop/java/awt/Desktop.html) +- [File System Standards (XDG)](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) diff --git a/.claude/skills/kotlin-expert/SKILL.md b/.claude/skills/kotlin-expert/SKILL.md new file mode 100644 index 000000000..6bd7eb097 --- /dev/null +++ b/.claude/skills/kotlin-expert/SKILL.md @@ -0,0 +1,811 @@ +--- +name: kotlin-expert +description: Advanced Kotlin patterns for AmethystMultiplatform. Flow state management (StateFlow/SharedFlow), sealed hierarchies (classes vs interfaces), immutability (@Immutable, data classes), DSL builders (type-safe fluent APIs), inline functions (reified generics, performance). Use when working with: (1) State management patterns (StateFlow/SharedFlow/MutableStateFlow), (2) Sealed classes or sealed interfaces, (3) @Immutable annotations for Compose, (4) DSL builders with lambda receivers, (5) inline/reified functions, (6) Kotlin performance optimization. Complements kotlin-coroutines agent (async patterns) - this skill focuses on Amethyst-specific Kotlin idioms. +--- + +# Kotlin Expert + +Advanced Kotlin patterns for AmethystMultiplatform. Covers Flow state management, sealed hierarchies, immutability, DSL builders, and inline functions with real codebase examples. + +## Mental Model + +**Kotlin in Amethyst:** + +``` +State Management (Hot Flows) + ├── StateFlow # Single value, always has value, replays to new subscribers + ├── SharedFlow # Event stream, configurable replay, multiple subscribers + └── MutableStateFlow # Private mutable, public via .asStateFlow() + +Type Safety (Sealed Hierarchies) + ├── sealed class # State variants with data (AccountState.LoggedIn/LoggedOut) + └── sealed interface # Generic result types (SignerResult) + +Compose Performance (@Immutable) + ├── @Immutable # 173+ event classes - prevents recomposition + └── data class # Structural equality, copy(), immutable by convention + +DSL Patterns + ├── Builder classes # Fluent APIs (TagArrayBuilder) + ├── Lambda receivers # inline fun tagArray { ... } + └── Method chaining # return this + +Performance + ├── inline fun # Eliminate lambda overhead + ├── reified type params # Runtime type info (OptimizedJsonMapper) + └── value class # Zero-cost wrappers (NOT USED yet in Amethyst) +``` + +**Delegation:** +- **kotlin-coroutines agent**: Deep async (structured concurrency, channels, operators) +- **kotlin-multiplatform skill**: expect/actual, source sets +- **This skill**: Amethyst Kotlin idioms, state patterns, type safety + +--- + +## 1. Flow State Management + +### StateFlow: State that Changes + +**Mental model:** StateFlow is a "hot" observable state holder. Always has a value, new collectors immediately get current state. + +**Amethyst pattern:** + +```kotlin +// AccountManager.kt:48-50 +class AccountManager { + private val _accountState = MutableStateFlow(AccountState.LoggedOut) + val accountState: StateFlow = _accountState.asStateFlow() + + fun login(key: String) { + _accountState.value = AccountState.LoggedIn(...) + } +} +``` + +**Key principles:** +1. **Private mutable, public immutable**: `_accountState` (MutableStateFlow) private, `accountState` (StateFlow) public +2. **Always has value**: Initial value required (`LoggedOut`) +3. **Single value**: Replays ONE most recent value to new subscribers +4. **Hot**: Stays in memory, all collectors share same instance + +**See:** AccountManager.kt:48-50, RelayConnectionManager.kt:49-52 + +### SharedFlow: Event Streams + +**Mental model:** SharedFlow is a "hot" broadcast stream for events. Configurable replay buffer, doesn't require initial value. + +**Amethyst pattern:** + +```kotlin +// RelayConnectionManager.kt:52-53 +val connectedRelays: StateFlow> = client.connectedRelaysFlow() +val availableRelays: StateFlow> = client.availableRelaysFlow() +``` + +**When to use StateFlow vs SharedFlow:** + +| Scenario | Use StateFlow | Use SharedFlow | +|----------|---------------|----------------| +| **UI state** | ✅ Current screen data, login status | ❌ | +| **One-time events** | ❌ | ✅ Navigation, snackbars, toasts | +| **Always has value** | ✅ | ❌ Optional | +| **Replay count** | 1 (latest only) | Configurable (0, 1, n) | +| **Backpressure** | Conflates (drops old) | Configurable buffer | + +**Best practice:** +```kotlin +// State: Use StateFlow +private val _uiState = MutableStateFlow(UiState.Loading) +val uiState: StateFlow = _uiState.asStateFlow() + +// Events: Use SharedFlow +private val _navigationEvents = MutableSharedFlow(replay = 0) +val navigationEvents: SharedFlow = _navigationEvents.asSharedFlow() +``` + +### Flow Anti-Patterns + +❌ **Exposing mutable state:** +```kotlin +val accountState: MutableStateFlow // BAD: Can be mutated externally +``` + +✅ **Expose immutable:** +```kotlin +val accountState: StateFlow = _accountState.asStateFlow() // GOOD +``` + +--- + +❌ **SharedFlow for state:** +```kotlin +val loginState = MutableSharedFlow() // BAD: State might get lost +``` + +✅ **StateFlow for state:** +```kotlin +val loginState = MutableStateFlow(LoginState.LoggedOut) // GOOD: Always has value +``` + +**See:** `references/flow-patterns.md` for comprehensive examples. + +--- + +## 2. Sealed Hierarchies + +### Sealed Classes: State Variants + +**Mental model:** Sealed classes represent a closed set of variants that share common data/behavior. + +**Amethyst pattern:** + +```kotlin +// AccountManager.kt:36-46 +sealed class AccountState { + data object LoggedOut : AccountState() + + data class LoggedIn( + val signer: NostrSigner, + val pubKeyHex: String, + val npub: String, + val nsec: String?, + val isReadOnly: Boolean + ) : AccountState() +} + +// Usage +when (state) { + is AccountState.LoggedOut -> showLogin() + is AccountState.LoggedIn -> showFeed(state.pubKeyHex) +} // Exhaustive - compiler enforces all cases +``` + +**Key principles:** +1. **Closed hierarchy**: All subclasses known at compile-time +2. **Exhaustive when**: Compiler ensures all cases handled +3. **Shared data**: Sealed class can hold common properties +4. **Single inheritance**: Subclass can't extend another class + +**When to use:** +- Modeling UI states (Loading, Success, Error) +- Login states (LoggedOut, LoggedIn) +- Result types with different data per variant + +### Sealed Interfaces: Generic Result Types + +**Mental model:** Sealed interfaces for contracts with multiple implementations that need generics or multiple inheritance. + +**Amethyst pattern:** + +```kotlin +// SignerResult.kt:25-46 +sealed interface SignerResult { + sealed interface RequestAddressed : SignerResult { + class Successful(val result: T) : RequestAddressed + class Rejected : RequestAddressed + class TimedOut : RequestAddressed + class ReceivedButCouldNotPerform( + val message: String? + ) : RequestAddressed + } +} + +// Usage with generics +fun handleResult(result: SignerResult) { + when (result) { + is SignerResult.RequestAddressed.Successful -> processEvent(result.result.event) + is SignerResult.RequestAddressed.Rejected -> showRejected() + is SignerResult.RequestAddressed.TimedOut -> showTimeout() + } +} +``` + +**Key principles:** +1. **Multiple inheritance**: Subtype can implement other interfaces +2. **Variance**: Supports `out`/`in` modifiers for generics +3. **No constructor**: Can't hold state directly (subtypes can) +4. **Nested hierarchies**: Can create sub-sealed hierarchies + +### Sealed Class vs Sealed Interface + +| Feature | Sealed Class | Sealed Interface | +|---------|--------------|------------------| +| **Constructor** | ✅ Can hold common state | ❌ No constructor | +| **Inheritance** | ❌ Single parent only | ✅ Multiple interfaces | +| **Generics** | ❌ No variance | ✅ Covariance/contravariance | +| **Use case** | State variants | Result types, contracts | + +**Decision tree:** + +``` +Need to hold common data in base? + YES → sealed class + NO → sealed interface + +Need generics with variance (out/in)? + YES → sealed interface + NO → Either works + +Subtypes need multiple inheritance? + YES → sealed interface + NO → Either works +``` + +**Amethyst examples:** +- `sealed class AccountState` - state variants with different data +- `sealed interface SignerResult` - generic result types with variance + +**See:** `references/sealed-class-catalog.md` for all sealed types in quartz. + +--- + +## 3. Immutability & Compose Performance + +### @Immutable Annotation + +**Mental model:** @Immutable tells Compose "this value never changes after construction." Compose can skip recomposition if @Immutable object reference doesn't change. + +**Amethyst pattern:** + +```kotlin +// TextNoteEvent.kt:51-63 +@Immutable +class TextNoteEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey +) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + // All properties immutable (val), no mutable state +} +``` + +**Key principles:** +1. **All properties immutable**: Only `val`, never `var` +2. **No mutable collections**: Use `ImmutableList`, `Array`, not `MutableList` +3. **Deep immutability**: Nested objects also immutable +4. **Compose optimization**: Skips recomposition if reference equals + +**Why it matters:** + +```kotlin +// Without @Immutable +@Composable +fun NoteCard(note: TextNoteEvent) { // Recomposes every time parent recomposes + Text(note.content) +} + +// With @Immutable +@Composable +fun NoteCard(note: TextNoteEvent) { // Only recomposes if note reference changes + Text(note.content) +} +``` + +**173+ @Immutable classes** in quartz - all events immutable for Compose performance. + +### Data Classes & Immutability + +**Pattern:** + +```kotlin +@Immutable +data class RelayStatus( + val url: NormalizedRelayUrl, + val connected: Boolean, + val error: String? = null +) { + // Implicit: equals(), hashCode(), copy(), toString() +} + +// Usage +val oldStatus = RelayStatus(url, connected = false) +val newStatus = oldStatus.copy(connected = true) // Immutable update +``` + +**Key principles:** +1. **Structural equality**: `equals()` compares properties, not reference +2. **copy()**: Create modified copies without mutating +3. **All properties in constructor**: For proper `equals()`/`hashCode()` +4. **Prefer val**: Make properties immutable + +### kotlinx.collections.immutable + +**Pattern:** + +```kotlin +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +// Instead of List (which could be mutable internally) +val relays: ImmutableList = persistentListOf("wss://relay1.com", "wss://relay2.com") + +// Add returns new instance +val updated = relays.add("wss://relay3.com") // relays unchanged, updated has 3 items +``` + +**When to use:** +- Compose state that needs collection +- Publicly exposed collections +- Shared state across threads + +**See:** `references/immutability-patterns.md` + +--- + +## 4. DSL Builders + +### Type-Safe Fluent APIs + +**Mental model:** DSL (Domain-Specific Language) builders use lambda receivers and method chaining to create readable, type-safe APIs. + +**Amethyst pattern:** + +```kotlin +// TagArrayBuilder.kt:23-90 +class TagArrayBuilder { + private val tagList = mutableMapOf>() + + fun add(tag: Array): TagArrayBuilder { + if (tag.isEmpty() || tag[0].isEmpty()) return this + tagList.getOrPut(tag[0], ::mutableListOf).add(tag) + return this // Method chaining + } + + fun remove(tagName: String): TagArrayBuilder { + tagList.remove(tagName) + return this // Method chaining + } + + fun build() = tagList.flatMap { it.value }.toTypedArray() +} + +// Inline function with lambda receiver (line 90) +inline fun tagArray(initializer: TagArrayBuilder.() -> Unit = {}): TagArray = + TagArrayBuilder().apply(initializer).build() +``` + +**Usage:** + +```kotlin +val tags = tagArray { + add(arrayOf("e", eventId, relay, "reply")) + add(arrayOf("p", pubkey)) + remove("a") // Remove address tags +} +``` + +**Key patterns:** +1. **Method chaining**: Return `this` from mutator methods +2. **Lambda receiver**: `TagArrayBuilder.() -> Unit` - lambda has `this: TagArrayBuilder` +3. **inline function**: Eliminates lambda overhead +4. **apply()**: Executes lambda with receiver, returns receiver + +### DSL Pattern Template + +```kotlin +class MyBuilder { + private val items = mutableListOf() + + fun add(item: Item): MyBuilder { + items.add(item) + return this + } + + fun build(): Result = Result(items.toList()) +} + +inline fun myDsl(init: MyBuilder.() -> Unit): Result = + MyBuilder().apply(init).build() + +// Usage +val result = myDsl { + add(Item("foo")) + add(Item("bar")) +} +``` + +**Why inline?** +- Eliminates lambda object allocation +- Enables `reified` type parameters +- Better performance for frequently-called DSLs + +**See:** `references/dsl-builder-examples.md` for more patterns. + +--- + +## 5. Inline Functions & reified + +### inline fun: Eliminate Overhead + +**Mental model:** `inline` copies function body to call site. No lambda object created, direct code insertion. + +**Pattern:** + +```kotlin +// Without inline +fun measureTime(block: () -> T): T { + val start = System.currentTimeMillis() + val result = block() // Lambda object allocated + println("Time: ${System.currentTimeMillis() - start}ms") + return result +} + +// With inline +inline fun measureTime(block: () -> T): T { + val start = System.currentTimeMillis() + val result = block() // No allocation, code inlined + println("Time: ${System.currentTimeMillis() - start}ms") + return result +} +``` + +**Benefits:** +1. **Zero overhead**: No lambda object allocation +2. **Non-local returns**: Can `return` from outer function inside lambda +3. **reified enabled**: Access to type parameter at runtime + +### reified: Runtime Type Access + +**Mental model:** `reified` makes generic type `T` available at runtime. Only works with `inline`. + +**Amethyst pattern:** + +```kotlin +// OptimizedJsonMapper.kt:48 +expect object OptimizedJsonMapper { + inline fun fromJsonTo(json: String): T +} + +// Usage +val event: TextNoteEvent = OptimizedJsonMapper.fromJsonTo(jsonString) +// Compiler inlines and passes TextNoteEvent::class info +``` + +**Without reified:** + +```kotlin +// Would need to pass class explicitly +fun fromJson(json: String, clazz: KClass): T { + return when (clazz) { + TextNoteEvent::class -> parseTextNote(json) as T + // ... + } +} + +val event = fromJson(json, TextNoteEvent::class) // Verbose +``` + +**With reified:** + +```kotlin +inline fun fromJson(json: String): T { + return when (T::class) { // Can access T::class! + TextNoteEvent::class -> parseTextNote(json) as T + // ... + } +} + +val event = fromJson(json) // Clean +``` + +### noinline & crossinline + +**noinline**: Prevent specific lambda from being inlined + +```kotlin +inline fun foo( + inlined: () -> Unit, + noinline notInlined: () -> Unit // Can be stored, passed around +) { + inlined() + someFunction(notInlined) // Can pass to non-inline function +} +``` + +**crossinline**: Lambda can't do non-local returns + +```kotlin +inline fun foo(crossinline block: () -> Unit) { + launch { + block() // OK: crossinline allows lambda in different context + } +} +``` + +--- + +## 6. Value Classes (Opportunity) + +**Mental model:** `value class` is a compile-time wrapper with zero runtime overhead. Single property, no boxing. + +**Not currently used in Amethyst** - potential optimization. + +**Pattern:** + +```kotlin +@JvmInline +value class EventId(val hex: String) + +@JvmInline +value class PubKey(val hex: String) + +// Type safety without runtime cost +fun fetchEvent(eventId: EventId): Event { + // eventId.hex accessed without wrapper object +} + +val id = EventId("abc123") +fetchEvent(id) // Type safe +// fetchEvent(PubKey("xyz")) // Compile error! +``` + +**When to use:** +- Type safety for primitives (IDs, hex strings, timestamps) +- High-frequency allocations (event processing) +- Clear domain types without overhead + +**Restrictions:** +- Single property only +- Must be `val` +- Can't have `init` block with logic +- Inline at compile-time, may box in some cases + +**Amethyst opportunity:** + +```kotlin +// Current (String everywhere, no type safety) +fun fetchEvent(id: String): Event // Could pass wrong string + +// With value class +@JvmInline value class EventId(val hex: String) +@JvmInline value class PubKeyHex(val hex: String) +@JvmInline value class Bech32(val encoded: String) + +fun fetchEvent(id: EventId): Event // Type safe, zero cost +``` + +--- + +## Common Patterns + +### Pattern: StateFlow State Management + +```kotlin +class MyViewModel { + private val _state = MutableStateFlow(State.Initial) + val state: StateFlow = _state.asStateFlow() + + fun loadData() { + viewModelScope.launch { + _state.value = State.Loading + val result = repository.getData() + _state.value = when (result) { + is Success -> State.Success(result.data) + is Error -> State.Error(result.message) + } + } + } +} + +sealed class State { + data object Initial : State() + data object Loading : State() + data class Success(val data: List) : State() + data class Error(val message: String) : State() +} +``` + +### Pattern: Sealed Result with Generics + +```kotlin +sealed interface Result { + data class Success(val value: T) : Result + data class Error(val exception: Exception) : Result + data object Loading : Result +} + +// Use with variance +fun fetchData(): Result = ... + +val userResult: Result = fetchData() +val itemResult: Result> = fetchData() +``` + +### Pattern: Immutable Event Builder + +```kotlin +@Immutable +data class Event( + val id: String, + val kind: Int, + val content: String, + val tags: ImmutableList +) { + companion object { + fun builder() = EventBuilder() + } +} + +class EventBuilder { + private var id: String = "" + private var kind: Int = 1 + private var content: String = "" + private val tags = mutableListOf() + + fun id(value: String) = apply { id = value } + fun kind(value: Int) = apply { kind = value } + fun content(value: String) = apply { content = value } + fun tag(tag: Tag) = apply { tags.add(tag) } + + fun build() = Event(id, kind, content, tags.toImmutableList()) +} + +// Usage +val event = Event.builder() + .id("abc") + .kind(1) + .content("Hello") + .tag(Tag.P("pubkey")) + .build() +``` + +--- + +## Delegation Guide + +**When to delegate:** + +| Topic | Delegate To | This Skill Covers | +|-------|-------------|-------------------| +| Structured concurrency, channels | kotlin-coroutines agent | Flow state patterns only | +| expect/actual, source sets | kotlin-multiplatform skill | Platform-agnostic Kotlin | +| General Compose patterns | compose-expert skill | @Immutable for performance | +| Build configuration | gradle-expert skill | - | + +**Ask kotlin-coroutines agent for:** +- Advanced Flow operators (flatMapLatest, combine, zip) +- Channel patterns +- Structured concurrency (supervisorScope, coroutineScope) +- Error handling in coroutines + +**This skill teaches:** +- StateFlow/SharedFlow state management +- Sealed hierarchies +- @Immutable for Compose +- DSL builders +- Inline/reified patterns + +--- + +## Anti-Patterns + +❌ **Mutable public state:** +```kotlin +val accountState: MutableStateFlow // BAD +``` + +✅ **Immutable public interface:** +```kotlin +val accountState: StateFlow = _accountState.asStateFlow() +``` + +--- + +❌ **Sealed class for generic results:** +```kotlin +sealed class Result { // BAD: Can't use variance + data class Success(val value: T) : Result() +} +``` + +✅ **Sealed interface for generics:** +```kotlin +sealed interface Result { // GOOD: Covariance + data class Success(val value: T) : Result +} +``` + +--- + +❌ **Mutable properties in @Immutable class:** +```kotlin +@Immutable +data class Event( + var content: String // BAD: var breaks immutability +) +``` + +✅ **All val:** +```kotlin +@Immutable +data class Event( + val content: String +) +``` + +--- + +❌ **Passing class explicitly when reified available:** +```kotlin +inline fun parse(json: String, clazz: KClass): T // BAD +``` + +✅ **Use reified:** +```kotlin +inline fun parse(json: String): T // GOOD +``` + +--- + +## Quick Reference + +### Flow Decision Tree + +``` +Need to expose state? + YES → StateFlow (always has value, single latest) + NO → Need events? → SharedFlow (optional replay, broadcast) + +Need to mutate? + Internal only → MutableStateFlow (private) + Expose publicly → StateFlow via .asStateFlow() +``` + +### Sealed Decision Tree + +``` +Need common data in base type? + YES → sealed class + NO → sealed interface + +Need generics with variance? + YES → sealed interface + NO → Either works + +Need multiple inheritance? + YES → sealed interface + NO → Either works +``` + +### Inline Decision Tree + +``` +Passing lambda to function? + Called frequently? → inline (performance) + Need reified? → inline (required) + Need to store/pass lambda? → regular fun (can't inline) +``` + +--- + +## Resources + +### Official Docs +- [StateFlow and SharedFlow | Android Developers](https://developer.android.com/kotlin/flow/stateflow-and-sharedflow) +- [Sealed Classes | Kotlin Docs](https://kotlinlang.org/docs/sealed-classes.html) +- [Inline Functions | Kotlin Docs](https://kotlinlang.org/docs/inline-functions.html) + +### Bundled References +- `references/flow-patterns.md` - StateFlow/SharedFlow examples from AccountManager, RelayManager +- `references/sealed-class-catalog.md` - All sealed types in quartz +- `references/dsl-builder-examples.md` - TagArrayBuilder, other DSL patterns +- `references/immutability-patterns.md` - @Immutable usage, data classes, collections + +### Codebase Examples +- AccountManager.kt:36-50 - sealed class AccountState, StateFlow pattern +- RelayConnectionManager.kt:44-52 - StateFlow state management +- SignerResult.kt:25-46 - sealed interface with generics +- TextNoteEvent.kt:51-63 - @Immutable event class +- TagArrayBuilder.kt:23-90 - DSL builder pattern, inline function +- OptimizedJsonMapper.kt:48 - inline fun with reified + +--- + +**Version:** 1.0.0 +**Last Updated:** 2025-12-30 +**Codebase Reference:** AmethystMultiplatform commit 258c4e011 diff --git a/.claude/skills/kotlin-expert/references/dsl-builder-examples.md b/.claude/skills/kotlin-expert/references/dsl-builder-examples.md new file mode 100644 index 000000000..231b82d45 --- /dev/null +++ b/.claude/skills/kotlin-expert/references/dsl-builder-examples.md @@ -0,0 +1,602 @@ +# DSL Builder Examples + +Type-safe fluent APIs and DSL patterns from the codebase. + +## Table of Contents +- [TagArrayBuilder Pattern](#tagarraybuilder-pattern) +- [Builder Variations](#builder-variations) +- [DSL Principles](#dsl-principles) +- [Creating Custom DSLs](#creating-custom-dsls) + +--- + +## TagArrayBuilder Pattern + +### Core Implementation + +**File:** `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/TagArrayBuilder.kt:23-91` + +```kotlin +class TagArrayBuilder { + private val tagList = mutableMapOf>() + + fun remove(tagName: String): TagArrayBuilder { + tagList.remove(tagName) + return this // Method chaining + } + + fun remove(tagName: String, tagValue: String): TagArrayBuilder { + tagList[tagName]?.removeAll { it.valueOrNull() == tagValue } + if (tagList[tagName]?.isEmpty() == true) { + tagList.remove(tagName) + } + return this + } + + fun removeIf( + predicate: (Tag, Tag) -> Boolean, + toCompare: Tag + ): TagArrayBuilder { + val tagName = toCompare.nameOrNull() ?: return this + tagList[tagName]?.removeAll { predicate(it, toCompare) } + if (tagList[tagName]?.isEmpty() == true) { + tagList.remove(tagName) + } + return this + } + + fun add(tag: Array): TagArrayBuilder { + if (tag.isEmpty() || tag[0].isEmpty()) return this + tagList.getOrPut(tag[0], ::mutableListOf).add(tag) + return this + } + + fun addFirst(tag: Array): TagArrayBuilder { + if (tag.isEmpty() || tag[0].isEmpty()) return this + tagList.getOrPut(tag[0], ::mutableListOf).add(0, tag) + return this + } + + fun addUnique(tag: Array): TagArrayBuilder { + if (tag.isEmpty() || tag[0].isEmpty()) return this + tagList[tag[0]] = mutableListOf(tag) // Replace existing + return this + } + + fun addAll(tag: List>): TagArrayBuilder { + tag.forEach(::add) + return this + } + + fun toTypedArray() = tagList.flatMap { it.value }.toTypedArray() + + fun build() = toTypedArray() +} + +// Inline DSL function with lambda receiver +inline fun tagArray( + initializer: TagArrayBuilder.() -> Unit = {} +): TagArray = TagArrayBuilder().apply(initializer).build() +``` + +### Usage Examples + +**Basic usage:** + +```kotlin +val tags = tagArray { + add(arrayOf("e", eventId, relay, "reply")) + add(arrayOf("p", pubkey)) + add(arrayOf("t", "bitcoin")) +} +``` + +**Advanced patterns:** + +```kotlin +// Remove and add +val tags = tagArray { + addAll(existingTags) + remove("a") // Remove all address tags + addUnique(arrayOf("client", "Amethyst")) // Replace client tag +} + +// Conditional building +val tags = tagArray { + add(arrayOf("e", rootId, "", "root")) + + if (replyToId != null) { + add(arrayOf("e", replyToId, "", "reply")) + } + + mentionedPubkeys.forEach { pubkey -> + add(arrayOf("p", pubkey)) + } + + hashtags.forEach { tag -> + add(arrayOf("t", tag.lowercase())) + } +} + +// Custom predicate removal +val tags = tagArray { + addAll(originalTags) + removeIf( + predicate = { tag, compare -> tag[1] == compare[1] }, + toCompare = arrayOf("e", eventIdToRemove) + ) +} +``` + +--- + +## Builder Variations + +### PrivateTagArrayBuilder + +**File:** `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/PrivateTagArrayBuilder.kt` + +```kotlin +class PrivateTagArrayBuilder { + private val builder = TagArrayBuilder() + + fun add(tag: PrivateTag): PrivateTagArrayBuilder { + builder.add(tag.toArray()) + return this + } + + fun addAll(tags: List): PrivateTagArrayBuilder { + tags.forEach { add(it) } + return this + } + + fun build(): Array> = builder.build() +} + +// DSL function +inline fun privateTagArray( + initializer: PrivateTagArrayBuilder.() -> Unit +): Array> = PrivateTagArrayBuilder().apply(initializer).build() +``` + +**Usage:** + +```kotlin +val privateTags = privateTagArray { + add(PrivateTag.Event(eventId, marker = "bookmark")) + add(PrivateTag.Profile(pubkey)) + addAll(existingPrivateTags) +} +``` + +### TlvBuilder + +**File:** `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip19Bech32/tlv/TlvBuilder.kt` + +```kotlin +class TlvBuilder { + private val entries = mutableListOf() + + fun add(type: TlvType, value: ByteArray): TlvBuilder { + entries.add(TlvEntry(type, value)) + return this + } + + fun addRelay(relay: String): TlvBuilder { + add(TlvType.Relay, relay.encodeToByteArray()) + return this + } + + fun addAuthor(pubkey: ByteArray): TlvBuilder { + add(TlvType.Author, pubkey) + return this + } + + fun addKind(kind: Int): TlvBuilder { + add(TlvType.Kind, kind.toByteArray()) + return this + } + + fun build(): ByteArray { + return entries.flatMap { it.encode() }.toByteArray() + } +} + +fun tlv(init: TlvBuilder.() -> Unit): ByteArray = + TlvBuilder().apply(init).build() +``` + +**Usage:** + +```kotlin +val tlvData = tlv { + addAuthor(pubkeyBytes) + addRelay("wss://relay.damus.io") + addKind(1) +} +``` + +### MapOfSetBuilder + +**File:** `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/MapOfSetBuilder.kt` + +```kotlin +class MapOfSetBuilder { + private val map = mutableMapOf>() + + fun add(key: K, value: V): MapOfSetBuilder { + map.getOrPut(key) { mutableSetOf() }.add(value) + return this + } + + fun addAll(key: K, values: Collection): MapOfSetBuilder { + map.getOrPut(key) { mutableSetOf() }.addAll(values) + return this + } + + fun remove(key: K, value: V): MapOfSetBuilder { + map[key]?.remove(value) + if (map[key]?.isEmpty() == true) { + map.remove(key) + } + return this + } + + fun build(): Map> = map.mapValues { it.value.toSet() } +} + +inline fun mapOfSets( + init: MapOfSetBuilder.() -> Unit +): Map> = MapOfSetBuilder().apply(init).build() +``` + +**Usage:** + +```kotlin +val relayMap = mapOfSets { + add("wss://relay1.com", eventId1) + add("wss://relay1.com", eventId2) + add("wss://relay2.com", eventId3) +} +// Result: {"wss://relay1.com": [eventId1, eventId2], "wss://relay2.com": [eventId3]} +``` + +--- + +## DSL Principles + +### 1. Lambda with Receiver + +**Mental model:** Lambda receiver makes `this` refer to builder instance inside lambda. + +```kotlin +// Without receiver +fun buildTags(config: (TagArrayBuilder) -> Unit) { + val builder = TagArrayBuilder() + config(builder) // Must pass builder explicitly + builder.build() +} + +buildTags { builder -> + builder.add(...) // Verbose +} + +// With receiver +inline fun buildTags(config: TagArrayBuilder.() -> Unit) { + TagArrayBuilder().apply(config).build() +} + +buildTags { + add(...) // Clean - 'this' is builder +} +``` + +### 2. Method Chaining + +**Pattern:** Return `this` from mutator methods. + +```kotlin +class Builder { + private var value: String = "" + + fun setValue(v: String): Builder { + value = v + return this // Enable chaining + } + + fun append(s: String): Builder { + value += s + return this + } + + fun build(): String = value +} + +// Usage +val result = Builder() + .setValue("Hello") + .append(" ") + .append("World") + .build() +``` + +### 3. Inline for Performance + +**Why inline:** +- Eliminates lambda allocation +- Allows `reified` type parameters +- Better for hot paths (frequently called) + +```kotlin +// NOT inline - lambda object created each call +fun myDsl(init: Builder.() -> Unit): Result { + return Builder().apply(init).build() +} + +// Inline - lambda code inlined at call site +inline fun myDsl(init: Builder.() -> Unit): Result { + return Builder().apply(init).build() +} +``` + +### 4. Type Safety + +**Use generics for compile-time safety:** + +```kotlin +// Type-safe builder +class EventBuilder { + fun addTag(tag: Tag): EventBuilder { // Only accepts tags for this event type + tags.add(tag) + return this + } +} + +// Usage +val textNote = EventBuilder() + .addTag(TextNoteTag.Subject("Hello")) // OK + // .addTag(ChannelTag.Name("test")) // Compile error! + .build() +``` + +--- + +## Creating Custom DSLs + +### Pattern: Simple Builder DSL + +```kotlin +class QueryBuilder { + private val filters = mutableListOf() + private var limit: Int? = null + private var offset: Int? = null + + fun filter(field: String, value: String): QueryBuilder { + filters.add("$field:$value") + return this + } + + fun limit(n: Int): QueryBuilder { + limit = n + return this + } + + fun offset(n: Int): QueryBuilder { + offset = n + return this + } + + fun build(): String { + val parts = mutableListOf() + if (filters.isNotEmpty()) { + parts.add(filters.joinToString(" AND ")) + } + if (limit != null) { + parts.add("LIMIT $limit") + } + if (offset != null) { + parts.add("OFFSET $offset") + } + return parts.joinToString(" ") + } +} + +inline fun query(init: QueryBuilder.() -> Unit): String = + QueryBuilder().apply(init).build() + +// Usage +val sql = query { + filter("status", "active") + filter("age", ">18") + limit(10) + offset(20) +} +// Result: "status:active AND age:>18 LIMIT 10 OFFSET 20" +``` + +### Pattern: Nested Builders + +```kotlin +class FilterBuilder { + private val conditions = mutableListOf() + + fun equals(field: String, value: String) { + conditions.add("$field = '$value'") + } + + fun greaterThan(field: String, value: Int) { + conditions.add("$field > $value") + } + + fun build(): String = conditions.joinToString(" AND ") +} + +class QueryBuilder { + private var filterClause: String = "" + private var selectClause: String = "*" + + fun select(vararg fields: String): QueryBuilder { + selectClause = fields.joinToString(", ") + return this + } + + fun where(init: FilterBuilder.() -> Unit): QueryBuilder { + filterClause = FilterBuilder().apply(init).build() + return this + } + + fun build(): String { + return "SELECT $selectClause WHERE $filterClause" + } +} + +inline fun query(init: QueryBuilder.() -> Unit): String = + QueryBuilder().apply(init).build() + +// Usage +val sql = query { + select("id", "name", "age") + where { + equals("status", "active") + greaterThan("age", 18) + } +} +// Result: "SELECT id, name, age WHERE status = 'active' AND age > 18" +``` + +### Pattern: Type-Safe HTML DSL + +```kotlin +abstract class Tag(val name: String) { + private val children = mutableListOf() + private val attributes = mutableMapOf() + + fun tag(tag: T, init: T.() -> Unit): T { + tag.init() + children.add(tag) + return tag + } + + fun attr(name: String, value: String) { + attributes[name] = value + } + + fun render(builder: StringBuilder, indent: String) { + builder.append("$indent<$name") + attributes.forEach { (k, v) -> builder.append(" $k=\"$v\"") } + if (children.isEmpty()) { + builder.append("/>\n") + } else { + builder.append(">\n") + children.forEach { it.render(builder, "$indent ") } + builder.append("$indent\n") + } + } +} + +class HTML : Tag("html") +class Head : Tag("head") +class Body : Tag("body") +class Div : Tag("div") +class P : Tag("p") +class A : Tag("a") + +fun HTML.head(init: Head.() -> Unit) = tag(Head(), init) +fun HTML.body(init: Body.() -> Unit) = tag(Body(), init) +fun Body.div(init: Div.() -> Unit) = tag(Div(), init) +fun Div.p(init: P.() -> Unit) = tag(P(), init) +fun Div.a(init: A.() -> Unit) = tag(A(), init) + +fun html(init: HTML.() -> Unit): HTML = HTML().apply(init) + +// Usage +val page = html { + head { + // ... + } + body { + div { + attr("class", "container") + p { + attr("id", "intro") + } + a { + attr("href", "https://example.com") + } + } + } +} +``` + +--- + +## Best Practices + +### ✅ DO + +1. **Return `this` for chaining:** + ```kotlin + fun add(item: Item): Builder { + items.add(item) + return this + } + ``` + +2. **Use `inline` for DSL functions:** + ```kotlin + inline fun myDsl(init: Builder.() -> Unit) = Builder().apply(init).build() + ``` + +3. **Provide sensible defaults:** + ```kotlin + inline fun query( + init: QueryBuilder.() -> Unit = {} // Empty lambda as default + ) = QueryBuilder().apply(init).build() + ``` + +4. **Validate in `build()`:** + ```kotlin + fun build(): Result { + require(fields.isNotEmpty()) { "Must specify at least one field" } + return Result(fields) + } + ``` + +### ❌ DON'T + +1. **Forget to return `this`:** + ```kotlin + fun add(item: Item) { // BAD: Can't chain + items.add(item) + } + ``` + +2. **Mutate after build:** + ```kotlin + val builder = Builder() + builder.add("foo") + val result = builder.build() + builder.add("bar") // BAD: Confusing state + ``` + +3. **Expose mutable state:** + ```kotlin + class Builder { + val items = mutableListOf() // BAD: Can be mutated externally + } + ``` + +4. **Make DSL functions non-inline unnecessarily:** + ```kotlin + fun myDsl(init: Builder.() -> Unit) = ... // BAD: Lambda allocation overhead + ``` + +--- + +## References + +- TagArrayBuilder.kt:23-91 +- PrivateTagArrayBuilder.kt +- TlvBuilder.kt +- [Type-Safe Builders | Kotlin Docs](https://kotlinlang.org/docs/type-safe-builders.html) +- [DSLs with Kotlin](https://kt.academy/article/dsl-intro) diff --git a/.claude/skills/kotlin-expert/references/flow-patterns.md b/.claude/skills/kotlin-expert/references/flow-patterns.md new file mode 100644 index 000000000..4a15b1d91 --- /dev/null +++ b/.claude/skills/kotlin-expert/references/flow-patterns.md @@ -0,0 +1,405 @@ +# Flow Patterns in Amethyst + +StateFlow and SharedFlow usage patterns from the codebase. + +## Table of Contents +- [StateFlow for State Management](#stateflow-for-state-management) +- [Flow Composition](#flow-composition) +- [Common Patterns](#common-patterns) +- [Anti-Patterns](#anti-patterns) + +--- + +## StateFlow for State Management + +### AccountManager Pattern + +**File:** `commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt:36-115` + +```kotlin +sealed class AccountState { + data object LoggedOut : AccountState() + + data class LoggedIn( + val signer: NostrSigner, + val pubKeyHex: String, + val npub: String, + val nsec: String?, + val isReadOnly: Boolean, + ) : AccountState() +} + +class AccountManager { + private val _accountState = MutableStateFlow(AccountState.LoggedOut) + val accountState: StateFlow = _accountState.asStateFlow() + + fun generateNewAccount(): AccountState.LoggedIn { + val keyPair = KeyPair() + val signer = NostrSignerInternal(keyPair) + + val state = AccountState.LoggedIn( + signer = signer, + pubKeyHex = keyPair.pubKey.toHexKey(), + npub = keyPair.pubKey.toNpub(), + nsec = keyPair.privKey?.toNsec(), + isReadOnly = false + ) + _accountState.value = state // Update state + return state + } + + fun loginWithKey(keyInput: String): Result { + // ... validation ... + + val state = AccountState.LoggedIn(...) + _accountState.value = state + return Result.success(state) + } + + fun logout() { + _accountState.value = AccountState.LoggedOut + } +} +``` + +**Pattern highlights:** +- Private `MutableStateFlow` for internal mutations +- Public `StateFlow` via `.asStateFlow()` for read-only access +- Sealed class for type-safe state variants +- Initial value required (`AccountState.LoggedOut`) + +### RelayConnectionManager Pattern + +**File:** `commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayConnectionManager.kt:44-80` + +```kotlin +data class RelayStatus( + val url: NormalizedRelayUrl, + val connected: Boolean, + val error: String? = null, + val messageCount: Int = 0 +) + +open class RelayConnectionManager( + websocketBuilder: WebsocketBuilder +) : IRelayClientListener { + private val client = NostrClient(websocketBuilder) + + // Map of relay URLs to their status + private val _relayStatuses = MutableStateFlow>(emptyMap()) + val relayStatuses: StateFlow> = _relayStatuses.asStateFlow() + + // Delegated StateFlows from client + val connectedRelays: StateFlow> = client.connectedRelaysFlow() + val availableRelays: StateFlow> = client.availableRelaysFlow() + + fun addRelay(url: String): NormalizedRelayUrl? { + val normalized = RelayUrlNormalizer.normalizeOrNull(url) ?: return null + updateRelayStatus(normalized) { it.copy(connected = false, error = null) } + return normalized + } + + fun removeRelay(url: NormalizedRelayUrl) { + _relayStatuses.value = _relayStatuses.value - url // Immutable update (remove from map) + } + + private fun updateRelayStatus( + relay: NormalizedRelayUrl, + update: (RelayStatus) -> RelayStatus + ) { + _relayStatuses.value = _relayStatuses.value.toMutableMap().apply { + val current = get(relay) ?: RelayStatus(relay, false) + put(relay, update(current)) + } + } + + // IRelayClientListener implementation + override fun onConnect(relay: NormalizedRelayUrl) { + updateRelayStatus(relay) { it.copy(connected = true, error = null) } + } + + override fun onError(relay: NormalizedRelayUrl, error: String) { + updateRelayStatus(relay) { it.copy(connected = false, error = error) } + } +} +``` + +**Pattern highlights:** +- `Map` as state value for collection tracking +- Immutable map updates (copy with modifications) +- Helper function `updateRelayStatus` for consistent updates +- Delegation pattern (client exposes its own StateFlows) + +--- + +## Flow Composition + +### Multiple StateFlows in UI + +**Pattern:** + +```kotlin +@Composable +fun LoginScreen(accountManager: AccountManager) { + val accountState by accountManager.accountState.collectAsState() + + when (accountState) { + is AccountState.LoggedOut -> { + LoginForm(onLogin = { key -> accountManager.loginWithKey(key) }) + } + is AccountState.LoggedIn -> { + MainApp(account = accountState as AccountState.LoggedIn) + } + } +} +``` + +### Observing Multiple Flows + +**Pattern:** + +```kotlin +@Composable +fun RelayStatusCard(relayManager: RelayConnectionManager) { + val relayStatuses by relayManager.relayStatuses.collectAsState() + val connectedRelays by relayManager.connectedRelays.collectAsState() + + Column { + Text("${connectedRelays.size} of ${relayStatuses.size} relays connected") + + relayStatuses.forEach { (url, status) -> + RelayRow( + url = url, + connected = status.connected, + error = status.error + ) + } + } +} +``` + +--- + +## Common Patterns + +### Pattern: Immutable State Updates + +```kotlin +// Map updates +_relayStatuses.value = _relayStatuses.value + (url to newStatus) // Add +_relayStatuses.value = _relayStatuses.value - url // Remove +_relayStatuses.value = _relayStatuses.value.mapValues { (key, value) -> + if (key == targetUrl) value.copy(connected = true) else value +} + +// List updates +_items.value = _items.value + newItem // Append +_items.value = _items.value.filter { it.id != removedId } // Remove +_items.value = _items.value.map { if (it.id == id) it.copy(name = newName) else it } // Update + +// Object updates +_user.value = _user.value.copy(name = newName) +``` + +### Pattern: Conditional State Transitions + +```kotlin +fun attemptLogin(credentials: Credentials) { + if (_loginState.value is LoginState.LoggingIn) { + return // Already logging in, ignore + } + + _loginState.value = LoginState.LoggingIn + viewModelScope.launch { + try { + val user = repository.login(credentials) + _loginState.value = LoginState.Success(user) + } catch (e: Exception) { + _loginState.value = LoginState.Error(e.message ?: "Login failed") + } + } +} +``` + +### Pattern: Derived State + +```kotlin +class MyViewModel { + private val _items = MutableStateFlow>(emptyList()) + val items: StateFlow> = _items.asStateFlow() + + // Derived state (computed from items) + val itemCount: StateFlow = items.map { it.size } + .stateIn(viewModelScope, SharingStarted.Lazily, 0) + + val hasItems: StateFlow = items.map { it.isNotEmpty() } + .stateIn(viewModelScope, SharingStarted.Lazily, false) +} + +// Usage in Compose +@Composable +fun ItemList(viewModel: MyViewModel) { + val itemCount by viewModel.itemCount.collectAsState() + val hasItems by viewModel.hasItems.collectAsState() + + if (hasItems) { + Text("$itemCount items") + } else { + Text("No items") + } +} +``` + +### Pattern: State with Loading/Error + +```kotlin +sealed class UiState { + data object Loading : UiState() + data class Success(val data: T) : UiState() + data class Error(val message: String) : UiState() +} + +class FeedViewModel { + private val _feedState = MutableStateFlow>>(UiState.Loading) + val feedState: StateFlow>> = _feedState.asStateFlow() + + fun loadFeed() { + viewModelScope.launch { + _feedState.value = UiState.Loading + try { + val events = repository.getEvents() + _feedState.value = UiState.Success(events) + } catch (e: Exception) { + _feedState.value = UiState.Error(e.message ?: "Unknown error") + } + } + } +} + +// UI +@Composable +fun FeedScreen(viewModel: FeedViewModel) { + val state by viewModel.feedState.collectAsState() + + when (state) { + is UiState.Loading -> LoadingSpinner() + is UiState.Success -> EventList((state as UiState.Success).data) + is UiState.Error -> ErrorMessage((state as UiState.Error).message) + } +} +``` + +--- + +## Anti-Patterns + +### ❌ Exposing Mutable State + +```kotlin +// BAD: External code can mutate +class BadViewModel { + val state: MutableStateFlow = MutableStateFlow(State.Initial) +} + +// Caller can do: +viewModel.state.value = State.Hacked // Bypass internal logic! +``` + +### ✅ Expose Immutable + +```kotlin +// GOOD: Only ViewModel can mutate +class GoodViewModel { + private val _state = MutableStateFlow(State.Initial) + val state: StateFlow = _state.asStateFlow() + + fun updateState(newState: State) { + // Controlled mutation with validation + _state.value = newState + } +} +``` + +--- + +### ❌ Not Using Immutable Updates + +```kotlin +// BAD: Mutating collection doesn't trigger StateFlow update +val list = mutableListOf() +list.add(newItem) +_items.value = list // Same reference, no update emitted! +``` + +### ✅ Create New Instance + +```kotlin +// GOOD: New list instance +_items.value = _items.value + newItem // New list created, update emitted +``` + +--- + +### ❌ StateFlow for Events + +```kotlin +// BAD: Events get lost if no collector +class BadViewModel { + val navigationEvent: StateFlow = MutableStateFlow(null) + + fun navigate(event: NavEvent) { + _navigationEvent.value = event // Lost if UI not observing! + } +} +``` + +### ✅ SharedFlow for Events + +```kotlin +// GOOD: Events queued +class GoodViewModel { + private val _navigationEvent = MutableSharedFlow(replay = 0) + val navigationEvent: SharedFlow = _navigationEvent.asSharedFlow() + + fun navigate(event: NavEvent) { + viewModelScope.launch { + _navigationEvent.emit(event) // Queued for collector + } + } +} +``` + +--- + +### ❌ Blocking Operations in State Update + +```kotlin +// BAD: Blocking main thread +fun loadData() { + _state.value = fetchDataFromNetwork() // Blocks! +} +``` + +### ✅ Async Updates + +```kotlin +// GOOD: Use coroutines +fun loadData() { + viewModelScope.launch { + _state.value = UiState.Loading + val data = withContext(Dispatchers.IO) { + fetchDataFromNetwork() + } + _state.value = UiState.Success(data) + } +} +``` + +--- + +## References + +- AccountManager.kt:36-115 +- RelayConnectionManager.kt:44-80 +- [StateFlow and SharedFlow | Android Developers](https://developer.android.com/kotlin/flow/stateflow-and-sharedflow) +- [Hot vs Cold Flows](https://carrion.dev/en/posts/kotlin-flows-hot-cold/) diff --git a/.claude/skills/kotlin-expert/references/immutability-patterns.md b/.claude/skills/kotlin-expert/references/immutability-patterns.md new file mode 100644 index 000000000..f4c076669 --- /dev/null +++ b/.claude/skills/kotlin-expert/references/immutability-patterns.md @@ -0,0 +1,641 @@ +# Immutability Patterns + +@Immutable annotation, data classes, and immutable collections for Compose performance. + +## Table of Contents +- [Why Immutability Matters](#why-immutability-matters) +- [@Immutable Annotation](#immutable-annotation) +- [Data Classes](#data-classes) +- [Immutable Collections](#immutable-collections) +- [Common Patterns](#common-patterns) +- [Performance Impact](#performance-impact) + +--- + +## Why Immutability Matters + +### Compose Recomposition + +**Mental model:** Compose tracks state changes by comparing references. If an `@Immutable` object reference doesn't change, Compose skips recomposition. + +```kotlin +// Without @Immutable - Recomposes on every parent recomposition +data class User(val name: String, val age: Int) + +@Composable +fun UserCard(user: User) { // Recomposes unnecessarily + Text(user.name) +} + +// With @Immutable - Only recomposes when user reference changes +@Immutable +data class User(val name: String, val age: Int) + +@Composable +fun UserCard(user: User) { // Smart recomposition + Text(user.name) +} +``` + +**Performance difference:** +- Without `@Immutable`: 1000 `UserCard` recompositions per screen update +- With `@Immutable`: 10 `UserCard` recompositions (only changed users) + +### Thread Safety + +Immutable objects are inherently thread-safe: + +```kotlin +@Immutable +data class Event( + val id: String, + val content: String, + val createdAt: Long +) + +// Safe to share across coroutines without synchronization +val sharedEvent: Event = fetchEvent() +launch { processEvent(sharedEvent) } // Safe +launch { saveEvent(sharedEvent) } // Safe +``` + +--- + +## @Immutable Annotation + +### Basic Usage + +**Pattern from Amethyst:** + +```kotlin +// TextNoteEvent.kt:51-63 +@Immutable +class TextNoteEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey +) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + // All properties are val (immutable) + // No var properties + // No mutable collections +} +``` + +**Requirements for @Immutable:** +1. All properties must be `val` (no `var`) +2. All property types must be immutable or primitives +3. No mutable collections (`MutableList`, `MutableMap`) +4. Arrays are allowed (treated as immutable by contract) +5. No public mutable state + +### @Immutable vs @Stable + +**@Immutable:** Value never changes after construction + +```kotlin +@Immutable +data class User(val name: String, val age: Int) +// Once created, user.name and user.age never change +``` + +**@Stable:** Value can change, but changes are tracked + +```kotlin +@Stable +class MutableCounter { + var count by mutableStateOf(0) // Changes tracked by Compose +} +``` + +**Amethyst uses @Immutable extensively:** +- 173+ event classes annotated with `@Immutable` +- All Nostr events immutable by design +- Critical for feed performance (thousands of events) + +--- + +## Data Classes + +### Immutable Data Classes + +**Pattern:** + +```kotlin +@Immutable +data class RelayStatus( + val url: NormalizedRelayUrl, + val connected: Boolean, + val error: String? = null, + val messageCount: Int = 0 +) { + // Immutable properties only (val) + // Default values allowed +} +``` + +**Benefits:** +1. **Structural equality:** `equals()` compares values, not references +2. **copy():** Create modified copies without mutation +3. **toString():** Debugging-friendly output +4. **hashCode():** Consistent hashing for collections +5. **componentN():** Destructuring support + +### copy() for Updates + +**Mental model:** Instead of mutating, create modified copies. + +```kotlin +val status = RelayStatus( + url = "wss://relay.damus.io", + connected = false, + error = null +) + +// Immutable update +val updatedStatus = status.copy(connected = true) + +// Original unchanged +assert(status.connected == false) +assert(updatedStatus.connected == true) +``` + +**StateFlow pattern:** + +```kotlin +private val _relayStatuses = MutableStateFlow>(emptyMap()) + +fun updateRelay(url: String, connected: Boolean) { + _relayStatuses.value = _relayStatuses.value.mapValues { (key, status) -> + if (key == url) { + status.copy(connected = connected) // Immutable update + } else { + status + } + } +} +``` + +### All Properties in Constructor + +**Why important for data classes:** + +```kotlin +// BAD: Properties outside constructor not included in equals/hashCode +data class User(val name: String) { + var age: Int = 0 // NOT in equals/hashCode/copy! +} + +val user1 = User("Alice") +val user2 = User("Alice") +user1.age = 25 +user2.age = 30 + +assert(user1 == user2) // TRUE! age not compared +assert(user1.copy() == user1) // TRUE! age not copied + +// GOOD: All properties in constructor +@Immutable +data class User( + val name: String, + val age: Int // Included in equals/hashCode/copy +) +``` + +--- + +## Immutable Collections + +### kotlinx.collections.immutable + +**Installation:** + +```kotlin +// build.gradle.kts +dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7") +} +``` + +**Why use:** +- Structural sharing (efficient copies) +- Explicit immutability (compiler enforced) +- Safe for Compose state + +### ImmutableList + +```kotlin +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +// Create immutable list +val relays: ImmutableList = persistentListOf( + "wss://relay1.com", + "wss://relay2.com" +) + +// Add returns NEW list +val updated = relays.add("wss://relay3.com") +assert(relays.size == 2) // Original unchanged +assert(updated.size == 3) // New list has 3 items + +// Convert from regular list +val mutableList = mutableListOf("a", "b", "c") +val immutable = mutableList.toImmutableList() +``` + +### ImmutableMap + +```kotlin +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.collections.immutable.toImmutableMap + +// Create immutable map +val relayStatuses: ImmutableMap = persistentMapOf( + "wss://relay1.com" to RelayStatus(...), + "wss://relay2.com" to RelayStatus(...) +) + +// Put returns NEW map +val updated = relayStatuses.put("wss://relay3.com", RelayStatus(...)) + +// Remove returns NEW map +val removed = relayStatuses.remove("wss://relay1.com") +``` + +### ImmutableSet + +```kotlin +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentSetOf + +val connectedRelays: ImmutableSet = persistentSetOf( + "wss://relay1.com", + "wss://relay2.com" +) + +val updated = connectedRelays.add("wss://relay3.com") +``` + +### Structural Sharing + +**Mental model:** Immutable collections reuse internal structure for efficiency. + +```kotlin +val list1 = persistentListOf(1, 2, 3, 4, 5) // 5 items +val list2 = list1.add(6) // Shares structure with list1 + +// Internally: +// list1 and list2 share nodes for items 1-5 +// list2 has one additional node for item 6 +// O(1) time, O(1) space for add operation +``` + +--- + +## Common Patterns + +### Pattern: Immutable State Updates + +```kotlin +@Immutable +data class FeedState( + val events: ImmutableList, + val loading: Boolean, + val error: String? +) + +class FeedViewModel { + private val _state = MutableStateFlow( + FeedState( + events = persistentListOf(), + loading = false, + error = null + ) + ) + val state: StateFlow = _state.asStateFlow() + + fun loadEvents() { + _state.value = _state.value.copy(loading = true, error = null) + + viewModelScope.launch { + try { + val events = repository.getEvents() + _state.value = _state.value.copy( + events = events.toImmutableList(), + loading = false + ) + } catch (e: Exception) { + _state.value = _state.value.copy( + loading = false, + error = e.message + ) + } + } + } + + fun addEvent(event: Event) { + _state.value = _state.value.copy( + events = _state.value.events.add(event) // Immutable add + ) + } + + fun removeEvent(eventId: String) { + _state.value = _state.value.copy( + events = _state.value.events.filter { it.id != eventId }.toImmutableList() + ) + } +} +``` + +### Pattern: Deep Immutability + +```kotlin +// Nested immutable structures +@Immutable +data class User( + val name: String, + val profile: Profile // Also immutable +) + +@Immutable +data class Profile( + val bio: String, + val avatar: String, + val relays: ImmutableList // Immutable collection +) + +// Safe deep copy +val user = User( + name = "Alice", + profile = Profile( + bio = "Nostr enthusiast", + avatar = "https://...", + relays = persistentListOf("wss://relay1.com") + ) +) + +val updatedUser = user.copy( + profile = user.profile.copy( + bio = "Bitcoin & Nostr enthusiast" // Deep update + ) +) +``` + +### Pattern: Collection Builder to Immutable + +```kotlin +// Build mutable, convert to immutable +fun processEvents(input: List): ImmutableList { + val processed = mutableListOf() + + for (event in input) { + if (event.isValid()) { + processed.add(event.normalize()) + } + } + + return processed.toImmutableList() // Convert once at end +} +``` + +### Pattern: Immutable Map Updates + +```kotlin +private val _relayStatuses = MutableStateFlow>( + persistentMapOf() +) + +fun updateRelay(url: String, connected: Boolean) { + val currentStatuses = _relayStatuses.value + val currentStatus = currentStatuses[url] ?: RelayStatus(url, false) + + _relayStatuses.value = currentStatuses.put( + url, + currentStatus.copy(connected = connected) + ) +} + +fun removeRelay(url: String) { + _relayStatuses.value = _relayStatuses.value.remove(url) +} +``` + +--- + +## Performance Impact + +### Benchmarks (Approximate) + +**Recomposition cost:** + +```kotlin +// 1000 items in LazyColumn +// Without @Immutable: ~100ms per frame (skipped frames) +// With @Immutable: ~16ms per frame (smooth 60fps) + +@Immutable +data class Item(val id: String, val name: String) + +@Composable +fun ItemList(items: ImmutableList) { + LazyColumn { + items(items, key = { it.id }) { item -> + ItemRow(item) // Only recomposes when item changes + } + } +} +``` + +**Structural sharing efficiency:** + +```kotlin +val list1 = persistentListOf(1..10000) +val list2 = list1.add(10001) // O(log n) time, shares structure + +// Regular list (copy on modification): +val mutableList = (1..10000).toMutableList() +val copy = mutableList.toList() + 10001 // O(n) time, full copy +``` + +### When to Use Immutable Collections + +**Use ImmutableList/Map/Set when:** +- Storing in Compose state (@Immutable class) +- Sharing across coroutines +- Frequent modifications (structural sharing efficient) +- Need compile-time immutability guarantee + +**Use Array when:** +- Fixed size, no modifications +- Nostr protocol (tags are `Array>`) +- Performance-critical (array access is fastest) + +**Use regular List/Map/Set when:** +- Local scope only +- Build once, read many times +- Converting to immutable at boundary + +--- + +## Anti-Patterns + +### ❌ Mutable Properties in @Immutable Class + +```kotlin +@Immutable +data class BadEvent( + val id: String, + var content: String // BAD: var breaks immutability +) +``` + +### ✅ All val Properties + +```kotlin +@Immutable +data class GoodEvent( + val id: String, + val content: String +) +``` + +--- + +### ❌ Mutable Collections in @Immutable Class + +```kotlin +@Immutable +data class BadState( + val items: MutableList // BAD: Can mutate items +) + +// Caller can mutate: +val state = BadState(mutableListOf()) +state.items.add(newItem) // Breaks immutability! +``` + +### ✅ Immutable Collections + +```kotlin +@Immutable +data class GoodState( + val items: ImmutableList +) + +// Caller must create new state: +val updated = state.copy(items = state.items.add(newItem)) +``` + +--- + +### ❌ Direct Mutation + +```kotlin +val status = RelayStatus(url, connected = false) +status.connected = true // Compile error (val) + +// But could happen with mutable nested objects: +@Immutable +data class Config( + val settings: Settings // If Settings is mutable... +) + +class Settings { + var theme: String = "dark" // BAD +} + +val config = Config(Settings()) +config.settings.theme = "light" // Mutates "immutable" config! +``` + +### ✅ Deep Immutability + +```kotlin +@Immutable +data class Config( + val settings: Settings +) + +@Immutable +data class Settings( + val theme: String // val only +) + +val config = Config(Settings("dark")) +val updated = config.copy( + settings = config.settings.copy(theme = "light") +) +``` + +--- + +### ❌ Exposing Mutable Internal State + +```kotlin +@Immutable +class BadViewModel { + private val _items = mutableListOf() + val items: List = _items // BAD: Exposes mutable list + + fun addItem(item: Item) { + _items.add(item) + } +} + +// Caller can cast and mutate: +val vm = BadViewModel() +(vm.items as MutableList).clear() // Breaks encapsulation! +``` + +### ✅ Convert to Immutable at Boundary + +```kotlin +@Immutable +class GoodViewModel { + private val _items = mutableListOf() + val items: ImmutableList + get() = _items.toImmutableList() // GOOD: Copy to immutable + + fun addItem(item: Item) { + _items.add(item) + } +} +``` + +--- + +## Checklist for Immutability + +**For @Immutable classes:** +- [ ] All properties are `val`, never `var` +- [ ] No mutable collections (`MutableList`, `MutableMap`, `MutableSet`) +- [ ] Nested objects are also `@Immutable` or primitives +- [ ] No public mutable state +- [ ] Use `copy()` for updates, never mutation +- [ ] Arrays used only when truly immutable by contract + +**For StateFlow state:** +- [ ] State class is `@Immutable` +- [ ] Use immutable collections (ImmutableList, ImmutableMap) +- [ ] Create new instances for updates (`copy()`, `.add()`, `.put()`) +- [ ] Never mutate state in-place + +**For Compose performance:** +- [ ] All `@Composable` parameters are `@Immutable` or `@Stable` +- [ ] Lists use `ImmutableList` and `key` parameter in `items()` +- [ ] Heavy objects (events, profiles) cached and reused + +--- + +## References + +- TextNoteEvent.kt:51-63 - @Immutable event example +- RelayConnectionManager.kt - Immutable map updates +- [Compose Performance | Android Developers](https://developer.android.com/jetpack/compose/performance/stability) +- [kotlinx.collections.immutable | GitHub](https://github.com/Kotlin/kotlinx.collections.immutable) +- [@Stable and @Immutable | Compose Docs](https://developer.android.com/jetpack/compose/performance/stability/fix) diff --git a/.claude/skills/kotlin-expert/references/sealed-class-catalog.md b/.claude/skills/kotlin-expert/references/sealed-class-catalog.md new file mode 100644 index 000000000..c99cbef3a --- /dev/null +++ b/.claude/skills/kotlin-expert/references/sealed-class-catalog.md @@ -0,0 +1,482 @@ +# Sealed Class Catalog + +Comprehensive list of sealed types in AmethystMultiplatform with usage patterns. + +## Table of Contents +- [State Management](#state-management) +- [Result Types](#result-types) +- [Tag Variants](#tag-variants) +- [Sealed Class vs Sealed Interface](#sealed-class-vs-sealed-interface) +- [Patterns](#patterns) + +--- + +## State Management + +### AccountState (Sealed Class) + +**File:** `commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt:36-46` + +```kotlin +sealed class AccountState { + data object LoggedOut : AccountState() + + data class LoggedIn( + val signer: NostrSigner, + val pubKeyHex: String, + val npub: String, + val nsec: String?, + val isReadOnly: Boolean + ) : AccountState() +} +``` + +**Why sealed class:** +- Two distinct states with different data +- `LoggedIn` holds data, `LoggedOut` doesn't +- No need for generics or multiple inheritance + +**Usage:** + +```kotlin +fun handleAccountState(state: AccountState) { + when (state) { + is AccountState.LoggedOut -> showLogin() + is AccountState.LoggedIn -> { + showFeed( + pubkey = state.pubKeyHex, + canSign = !state.isReadOnly + ) + } + } // Exhaustive - compiler enforces +} +``` + +### VerificationState (Sealed Class) + +**File:** `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/VerificationState.kt` + +```kotlin +sealed class VerificationState { + data object NotStarted : VerificationState() + data object Started : VerificationState() + data class Failed(val reason: String) : VerificationState() + data object Verified : VerificationState() +} +``` + +**Pattern:** +- State machine (NotStarted → Started → Failed/Verified) +- Only `Failed` carries data (reason) +- Rest are singletons (`data object`) + +--- + +## Result Types + +### SignerResult (Sealed Interface with Generics) + +**File:** `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/SignerResult.kt:25-46` + +```kotlin +sealed interface SignerResult { + sealed interface RequestAddressed : SignerResult { + class Successful(val result: T) : RequestAddressed + class Rejected : RequestAddressed + class TimedOut : RequestAddressed + class ReceivedButCouldNotPerform( + val message: String? = null + ) : RequestAddressed + class ReceivedButCouldNotParseEventFromResult( + val eventJson: String + ) : RequestAddressed + class ReceivedButCouldNotVerifyResultingEvent( + val invalidEvent: Event + ) : RequestAddressed + } +} + +interface IResult + +data class SignResult(val event: Event) : IResult +data class EncryptionResult(val ciphertext: String) : IResult +data class DecryptionResult(val plaintext: String) : IResult +``` + +**Why sealed interface:** +- Generic result type `` +- Nested sealed hierarchy (RequestAddressed) +- Need covariance for flexible result types + +**Usage:** + +```kotlin +suspend fun signEvent(event: Event): SignerResult { + return when (val result = remoteSigner.sign(event)) { + is SignerResult.RequestAddressed.Successful -> result + is SignerResult.RequestAddressed.Rejected -> { + logger.warn("Signing rejected") + result + } + is SignerResult.RequestAddressed.TimedOut -> { + logger.error("Signing timed out") + result + } + is SignerResult.RequestAddressed.ReceivedButCouldNotPerform -> { + logger.error("Signer error: ${result.message}") + result + } + } +} +``` + +### CacheResults (Sealed Class with Generics) + +**File:** `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/caches/CacheResults.kt` + +```kotlin +sealed class CacheResults { + data class Found(val value: T) : CacheResults() + class NotFound : CacheResults() +} +``` + +**Pattern:** +- Simple binary result (found/not found) +- `Found` carries data, `NotFound` doesn't +- Generic for reusability + +--- + +## Tag Variants + +### MuteTag (Sealed Class) + +**File:** `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/muteList/tags/MuteTag.kt` + +```kotlin +sealed class MuteTag( + val nameOrNull: String?, + val valueOrNull: String? +) { + class Event(eventId: String) : MuteTag("e", eventId) + class Profile(pubkey: String) : MuteTag("p", pubkey) + class Word(word: String) : MuteTag("word", word) + class Thread(threadId: String) : MuteTag("thread", threadId) + + companion object { + fun parse(tag: Array): MuteTag? { + return when (tag.getOrNull(0)) { + "e" -> tag.getOrNull(1)?.let { Event(it) } + "p" -> tag.getOrNull(1)?.let { Profile(it) } + "word" -> tag.getOrNull(1)?.let { Word(it) } + "thread" -> tag.getOrNull(1)?.let { Thread(it) } + else -> null + } + } + } + + fun toArray(): Array { + return arrayOf(nameOrNull ?: "", valueOrNull ?: "") + } +} +``` + +**Pattern:** +- Common base class with shared properties +- Each variant represents different tag type +- Factory method `parse()` for parsing +- `toArray()` for serialization + +### BookmarkIdTag (Sealed Class) + +**File:** `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/BookmarkIdTag.kt` + +```kotlin +sealed class BookmarkIdTag { + abstract val id: String + abstract val marker: String? + + data class Event(override val id: String, override val marker: String?) : BookmarkIdTag() + data class Profile(override val id: String, override val marker: String?) : BookmarkIdTag() + data class Address(override val id: String, override val marker: String?) : BookmarkIdTag() + + companion object { + fun parse(tag: Array): BookmarkIdTag? { + val marker = tag.getOrNull(3) + return when (tag.getOrNull(0)) { + "e" -> tag.getOrNull(1)?.let { Event(it, marker) } + "p" -> tag.getOrNull(1)?.let { Profile(it, marker) } + "a" -> tag.getOrNull(1)?.let { Address(it, marker) } + else -> null + } + } + } +} +``` + +**Pattern:** +- Abstract properties in sealed class +- Data classes implement abstract properties +- Parse factory returns sealed variant + +--- + +## Exception Hierarchies + +### SignerExceptions (Sealed Class) + +**File:** `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/SignerExceptions.kt` + +```kotlin +sealed class SignerExceptions(message: String) : Exception(message) { + class UnableToSign(message: String) : SignerExceptions(message) + class UnableToDecrypt(message: String) : SignerExceptions(message) + class UnableToEncrypt(message: String) : SignerExceptions(message) + class UnableToGetPublicKey(message: String) : SignerExceptions(message) +} +``` + +**Pattern:** +- Sealed exception hierarchy +- Extends `Exception` base class +- Type-safe error handling + +**Usage:** + +```kotlin +try { + signer.sign(event) +} catch (e: SignerExceptions) { + when (e) { + is SignerExceptions.UnableToSign -> logger.error("Signing failed: ${e.message}") + is SignerExceptions.UnableToDecrypt -> logger.error("Decryption failed: ${e.message}") + is SignerExceptions.UnableToEncrypt -> logger.error("Encryption failed: ${e.message}") + is SignerExceptions.UnableToGetPublicKey -> logger.error("No public key: ${e.message}") + } +} +``` + +--- + +## Sealed Class vs Sealed Interface + +### When to Use Sealed Class + +**Examples from codebase:** + +1. **AccountState** - State variants with different data +2. **VerificationState** - State machine +3. **MuteTag** - Tag variants with common base properties +4. **SignerExceptions** - Exception hierarchy + +**Characteristics:** +- Need common constructor parameters +- Single inheritance only +- State variants +- Exception hierarchies + +### When to Use Sealed Interface + +**Examples from codebase:** + +1. **SignerResult** - Generic result types needing variance +2. **RelayUrlNormalizer.Result** - Binary result with no shared state + +**Characteristics:** +- Need generics with variance (`out`, `in`) +- No common state needed +- Multiple inheritance possible +- Contract/capability representation + +--- + +## Patterns + +### Pattern: State Machine + +```kotlin +sealed class ConnectionState { + data object Disconnected : ConnectionState() + data object Connecting : ConnectionState() + data class Connected(val relay: String) : ConnectionState() + data class Failed(val error: String) : ConnectionState() +} + +// Allowed transitions +fun transition(from: ConnectionState, event: Event): ConnectionState { + return when (from) { + is ConnectionState.Disconnected -> { + when (event) { + is Event.Connect -> ConnectionState.Connecting + else -> from + } + } + is ConnectionState.Connecting -> { + when (event) { + is Event.Success -> ConnectionState.Connected(event.relay) + is Event.Error -> ConnectionState.Failed(event.message) + is Event.Cancel -> ConnectionState.Disconnected + else -> from + } + } + is ConnectionState.Connected -> { + when (event) { + is Event.Disconnect -> ConnectionState.Disconnected + is Event.Error -> ConnectionState.Failed(event.message) + else -> from + } + } + is ConnectionState.Failed -> { + when (event) { + is Event.Retry -> ConnectionState.Connecting + is Event.Cancel -> ConnectionState.Disconnected + else -> from + } + } + } +} +``` + +### Pattern: Result Type + +```kotlin +sealed interface Result { + data class Success(val data: T) : Result + data class Error(val exception: Exception) : Result + data object Loading : Result +} + +// Extension functions +fun Result.getOrNull(): T? = when (this) { + is Result.Success -> data + else -> null +} + +fun Result.getOrThrow(): T = when (this) { + is Result.Success -> data + is Result.Error -> throw exception + is Result.Loading -> error("Still loading") +} + +fun Result.map(transform: (T) -> R): Result = when (this) { + is Result.Success -> Result.Success(transform(data)) + is Result.Error -> this + is Result.Loading -> Result.Loading +} +``` + +### Pattern: Tagged Union (Discriminated Union) + +```kotlin +sealed class Command { + data class SendEvent(val event: Event) : Command() + data class Subscribe(val filters: List) : Command() + data class Unsubscribe(val subId: String) : Command() + data object Close : Command() + + fun toJson(): String = when (this) { + is SendEvent -> """["EVENT",${event.toJson()}]""" + is Subscribe -> """["REQ","sub",${filters.joinToString { it.toJson() }}]""" + is Unsubscribe -> """["CLOSE","$subId"]""" + is Close -> """["CLOSE"]""" + } +} +``` + +### Pattern: Nested Sealed Hierarchies + +```kotlin +sealed interface UiState { + sealed interface Loading : UiState { + data object Initial : Loading + data class Refreshing(val currentData: List) : Loading + } + + sealed interface Content : UiState { + data class Success(val data: List) : Content + data object Empty : Content + } + + sealed interface Error : UiState { + data class Network(val message: String) : Error + data class Server(val code: Int, val message: String) : Error + } +} + +// Usage +fun renderUi(state: UiState) { + when (state) { + is UiState.Loading.Initial -> showFullScreenLoader() + is UiState.Loading.Refreshing -> showRefreshIndicator(state.currentData) + is UiState.Content.Success -> showList(state.data) + is UiState.Content.Empty -> showEmptyState() + is UiState.Error.Network -> showNetworkError(state.message) + is UiState.Error.Server -> showServerError(state.code, state.message) + } +} +``` + +--- + +## All Sealed Types in Quartz + +**Complete list of sealed types found in codebase:** + +### Commons +- AccountState (class) + +### Quartz +- BaseZapSplitSetup (class) +- MuteTag (class) +- BookmarkIdTag (class) +- SignerResult (interface) +- VerificationState (class) +- CacheResults (class) +- SignerExceptions (class) +- RelayUrlNormalizer.Result (interface) + +**Total:** 8 sealed types (7 classes, 1 interface) + +--- + +## Decision Tree + +``` +Need to represent variants of a concept? + YES → Use sealed type + NO → Regular class/interface + +Variants have different data? + YES → sealed class or sealed interface + NO → enum (if simple constants) + +Need generics with variance (out/in)? + YES → sealed interface + NO → sealed class (simpler) + +Need common constructor/properties? + YES → sealed class + NO → sealed interface + +Need multiple inheritance? + YES → sealed interface + NO → Either works + +Representing state machine? + → sealed class (state transitions) + +Representing result/error types? + → sealed interface (if generic, else class) + +Representing tag/command variants? + → sealed class (common structure) +``` + +--- + +## References + +- [Sealed Classes | Kotlin Docs](https://kotlinlang.org/docs/sealed-classes.html) +- [Effective Kotlin: Sealed Classes](https://kt.academy/article/ek-sealed-classes) +- [Complete Guide: Sealed Classes & Interfaces 2025](https://proandroiddev.com/complete-technical-guide-sealed-classes-sealed-interfaces-enums-in-kotlin-28ffc39116df) diff --git a/.claude/skills/kotlin-multiplatform/SKILL.md b/.claude/skills/kotlin-multiplatform/SKILL.md new file mode 100644 index 000000000..13df020fd --- /dev/null +++ b/.claude/skills/kotlin-multiplatform/SKILL.md @@ -0,0 +1,400 @@ +--- +name: kotlin-multiplatform +description: | + Platform abstraction decision-making for Amethyst KMP project. Guides when to abstract vs keep platform-specific, + source set placement (commonMain, jvmAndroid, platform-specific), expect/actual patterns. Covers primary targets + (Android, JVM/Desktop, iOS) with web/wasm future considerations. Integrates with gradle-expert for dependency issues. + Triggers on: abstraction decisions ("should I share this?"), source set placement questions, expect/actual creation, + build.gradle.kts work, incorrect placement detection, KMP dependency suggestions. +--- + +# Kotlin Multiplatform: Platform Abstraction Decisions + +Expert guidance for KMP architecture in Amethyst - deciding what to share vs keep platform-specific. + +## When to Use This Skill + +Making platform abstraction decisions: +- "Should I create expect/actual or keep Android-only?" +- "Can I share this ViewModel logic?" +- "Where does this crypto/JSON/network implementation belong?" +- "This uses Android Context - can it be abstracted?" +- "Is this code in the wrong module?" +- Preparing for iOS/web/wasm targets +- Detecting incorrect placements + +## Abstraction Decision Tree + +**Central question:** "Should this code be reused across platforms?" + +Follow this decision path (< 1 minute): + +``` +Q: Is it used by 2+ platforms? +├─ NO → Keep platform-specific +│ Example: Android-only permission handling +│ +└─ YES → Continue ↓ + +Q: Is it pure Kotlin (no platform APIs)? +├─ YES → commonMain +│ Example: Nostr event parsing, business rules +│ +└─ NO → Continue ↓ + +Q: Does it vary by platform or by JVM vs non-JVM? +├─ By platform (Android ≠ iOS ≠ Desktop) +│ → expect/actual +│ Example: Secp256k1Instance (uses different security APIs) +│ +├─ By JVM (Android = Desktop ≠ iOS/web) +│ → jvmAndroid +│ Example: Jackson JSON parsing (JVM library) +│ +└─ Complex/UI-related + → Keep platform-specific + Example: Navigation (Activity vs Window too different) + +Final check: +Q: Maintenance cost of abstraction < duplication cost? +├─ YES → Proceed with abstraction +└─ NO → Duplicate (simpler) +``` + +### Real Examples from Codebase + +**Crypto → expect/actual:** +```kotlin +// commonMain - expect declaration +expect object Secp256k1Instance { + fun signSchnorr(data: ByteArray, privKey: ByteArray): ByteArray +} + +// androidMain - uses Android Keystore +// jvmMain - uses Desktop JVM crypto +// iosMain - uses iOS Security framework +``` +**Why:** Each platform has different security APIs. + +**JSON parsing → jvmAndroid:** +```kotlin +// quartz/build.gradle.kts +val jvmAndroid = create("jvmAndroid") { + api(libs.jackson.module.kotlin) +} +``` +**Why:** Jackson is JVM-only, works on Android + Desktop, not iOS/web. + +**Navigation → platform-specific:** +- Android: `MainActivity` (Activity + Compose Navigation) +- Desktop: `Window` + sidebar + MenuBar +**Why:** UI paradigms fundamentally different. + +## Mental Model: Source Sets as Dependency Graph + +Think of source sets as a dependency graph, not folders. + +``` +┌─────────────────────────────────────────────┐ +│ commonMain = Contract (pure Kotlin) │ +│ - Business logic, protocol, data models │ +│ - No platform APIs │ +└────────────┬────────────────────────────────┘ + │ + ├──────────────────────┬──────────────────── + │ │ + ▼ ▼ + ┌───────────────────┐ ┌──────────────────┐ + │ jvmAndroid │ │ iosMain │ + │ JVM libs shared │ │ iOS common │ + │ - Jackson │ │ │ + │ - OkHttp │ └────┬─────────────┘ + └───┬───────────┬───┘ │ + │ │ ├─→ iosX64Main + ▼ ▼ ├─→ iosArm64Main + ┌─────────┐ ┌──────────┐ └─→ iosSimulatorArm64Main + │android │ │jvmMain │ + │Main │ │(Desktop) │ + └─────────┘ └──────────┘ + +Future: jsMain, wasmMain +``` + +**Key insight:** jvmAndroid is NOT a platform - it's a shared JVM layer. + +## The jvmAndroid Pattern + +**Unique to Amethyst.** Shares JVM libraries between Android + Desktop. + +### When to Use jvmAndroid + +Use jvmAndroid when: +- ✅ JVM-specific libraries (Jackson, OkHttp, url-detector) +- ✅ Android implementation = Desktop implementation (same JVM) +- ✅ Library doesn't work on iOS/web + +Do NOT use jvmAndroid for: +- ❌ Pure Kotlin code (use commonMain) +- ❌ Platform-specific APIs (use androidMain/jvmMain) +- ❌ Code that should work on all platforms + +### Example from quartz/build.gradle.kts + +```kotlin +// Must be defined BEFORE androidMain and jvmMain +val jvmAndroid = create("jvmAndroid") { + dependsOn(commonMain.get()) + + dependencies { + api(libs.jackson.module.kotlin) // JSON parsing - JVM only + api(libs.url.detector) // URL extraction - JVM only + implementation(libs.okhttp) // HTTP client - JVM only + } +} + +// Both depend on jvmAndroid +jvmMain { dependsOn(jvmAndroid) } +androidMain { dependsOn(jvmAndroid) } +``` + +**Why Jackson in jvmAndroid, not commonMain?** +- Jackson is JVM-specific library +- Works on Android (runs on JVM) +- Works on Desktop (runs on JVM) +- Does NOT work on iOS (not JVM) or web (not JVM) + +**Web/wasm consideration:** For future web support, consider migrating from Jackson → kotlinx.serialization (see Target-Specific Guidance). + +## What to Abstract vs Keep Platform-Specific + +Quick decision guidelines based on codebase patterns: + +### Always Abstract +- **Crypto** (Secp256k1, encryption, signing) +- **Core protocol logic** (Nostr events, NIPs) +- **Why:** Needed everywhere, platform security APIs vary + +### Often Abstract +- **I/O operations** (file reading, caching) +- **Logging** (platform logging systems differ) +- **Serialization** (if using kotlinx.serialization) +- **Why:** Commonly reused, platform implementations available + +### Sometimes Abstract +- **Business logic:** YES - state machines, data processing +- **UI state:** NO - ViewModels platform-specific +- **Why:** Separate concerns, business logic reusable + +### Rarely Abstract +- **UI components** (composables with platform dependencies) +- **Why:** Platform paradigms differ (bottom nav vs sidebar) + +### Never Abstract +- **Navigation** (Activity vs Window fundamentally different) +- **Permissions** (Android vs iOS APIs incompatible) +- **Platform UX patterns** +- **Why:** Too platform-specific, abstraction creates leaky APIs + +### Evidence from shared-ui-analysis.md + +| Component | Shared? | Rationale | +|-----------|---------|-----------| +| PubKeyFormatter, ZapFormatter | ✅ YES | Pure Kotlin, no platform APIs | +| TimeAgoFormatter | ⚠️ ABSTRACTED | Needs StringProvider for localized strings | +| Navigation (INav) | ❌ NO | Activity vs Window too different | +| AccountViewModel | ⚠️ PARTIAL | Business logic → IAccountState (shared), UI state → platform ViewModels | +| Image loading (Coil) | ⚠️ ABSTRACTED | Coil 3.x supports KMP, needs expect/actual wrapper | + +## expect/actual Mechanics + +**When to use:** Code needed by 2+ platforms, varies by platform. + +### Pattern Categories from Codebase + +**Objects (singletons):** +```kotlin +// 24 expect declarations found, common pattern: +expect object Secp256k1Instance { ... } +expect object Log { ... } +expect object LibSodiumInstance { ... } +``` + +**Classes (instantiable):** +```kotlin +expect class AESCBC { ... } +expect class DigestInstance { ... } +``` + +**Functions (utilities):** +```kotlin +expect fun platform(): String +expect fun currentTimeSeconds(): Long +``` + +**See** [references/expect-actual-catalog.md](references/expect-actual-catalog.md) for complete catalog with rationale. + +## Target-Specific Guidance + +### Android, JVM (Desktop), iOS - Current Primary Targets + +**Status:** Mature patterns, stable APIs + +**Android (androidMain):** +- Uses Android framework (Activity, Context, etc.) +- secp256k1-kmp-jni-android for crypto +- AndroidX libraries + +**Desktop JVM (jvmMain):** +- Uses Compose Desktop (Window, MenuBar, etc.) +- secp256k1-kmp-jni-jvm for crypto +- Pure JVM libraries + +**iOS (iosMain):** +- Active development, framework configured +- Architecture targets: iosX64Main, iosArm64Main, iosSimulatorArm64Main +- Platform APIs via platform.posix, Security framework + +### Web, wasm - Future Targets + +**Status:** Not yet implemented, consider for future-proofing + +**Constraints to know:** +- ❌ No platform.posix (file I/O different) +- ❌ No JVM libraries (Jackson, OkHttp won't work) +- ❌ Different async model (JS event loop vs threads) + +**Future-proofing tips:** +1. Prefer pure Kotlin in commonMain +2. Use kotlinx.* libraries: + - kotlinx.serialization instead of Jackson + - ktor instead of OkHttp (ktor supports web) + - kotlinx.datetime instead of custom date handling +3. Avoid platform.posix for file operations +4. Test abstractions work without JVM assumptions + +**Example migration path:** +```kotlin +// Current: jvmAndroid (JVM-only) +api(libs.jackson.module.kotlin) + +// Future: commonMain (all platforms) +api(libs.kotlinx.serialization.json) +``` + +## Integration: When to Invoke Other Skills + +### Invoke gradle-expert + +Trigger gradle-expert skill when encountering: +- Dependency conflicts (e.g., secp256k1-android vs secp256k1-jvm version mismatch) +- Build errors related to source sets +- Version catalog issues (libs.versions.toml) +- "Duplicate class" errors +- Performance/build time issues + +**Example trigger:** +``` +Error: Duplicate class found: fr.acinq.secp256k1.Secp256k1 +``` +→ Invoke gradle-expert for dependency conflict resolution. + +### Flags to Raise + +**Platform code in commonMain:** +```kotlin +// ❌ INCORRECT - Android API in commonMain +expect fun getContext(): Context // Context is Android-only! +``` +→ Flag: "Android API in commonMain won't compile on other platforms" + +**Duplicated business logic:** +```kotlin +// ❌ INCORRECT - Same logic in both +// androidMain/.../CryptoUtils.kt +fun validateSignature(...) { ... } + +// jvmMain/.../CryptoUtils.kt +fun validateSignature(...) { ... } // Duplicated! +``` +→ Flag: "Business logic duplicated, should be in commonMain or expect/actual" + +**Reinventing wheel - suggest KMP alternatives:** +- Custom date/time → kotlinx.datetime +- OkHttp → ktor (supports web) +- Jackson → kotlinx.serialization +- Custom UUID → kotlinx.uuid (when stable) + +## Common Pitfalls + +### 1. Over-Abstraction +**Problem:** Creating expect/actual for UI components +```kotlin +// ❌ BAD +expect fun NavigationComponent(...) +``` +**Why:** Navigation paradigms too different (Activity vs Window) +**Fix:** Keep platform-specific, accept duplication + +### 2. Under-Sharing +**Problem:** Duplicating business logic across platforms +```kotlin +// ❌ BAD - duplicated in androidMain and jvmMain +fun parseNostrEvent(json: String): Event { ... } +``` +**Why:** Bug fixes need to be applied twice, tests duplicated +**Fix:** Move to commonMain (pure Kotlin) or create expect/actual + +### 3. Leaky Abstractions +**Problem:** Platform code in commonMain +```kotlin +// commonMain - ❌ BAD +import android.content.Context // Won't compile on iOS! +``` +**Fix:** Use expect/actual or dependency injection + +### 4. Premature Abstraction +**Problem:** Creating expect/actual before second platform needs it +```kotlin +// ❌ BAD - only used on Android currently +expect fun showNotification(...) +``` +**Why:** Wrong abstraction boundaries, wasted effort +**Fix:** Wait until iOS actually needs it, then abstract + +### 5. Wrong Source Set +**Problem:** JVM libraries in commonMain +```kotlin +// commonMain - ❌ BAD +import com.fasterxml.jackson.databind.ObjectMapper +``` +**Why:** Jackson won't compile on iOS/web +**Fix:** Move to jvmAndroid or migrate to kotlinx.serialization + +## Quick Reference + +| Code Type | Recommended Location | Reason | +|-----------|---------------------|--------| +| Pure Kotlin business logic | commonMain | Works everywhere | +| Nostr protocol, NIPs | commonMain | Core logic, no platform APIs | +| JVM libs (Jackson, OkHttp) | jvmAndroid | Android + Desktop only | +| Crypto (varies by platform) | expect in commonMain, actual in platforms | Different security APIs per platform | +| I/O, logging | expect in commonMain, actual in platforms | Platform implementations differ | +| State (business logic) | commonMain or commons/jvmAndroid | Reusable StateFlow patterns | +| State (UI) | Platform ViewModels | Platform-specific lifecycle | +| UI formatters (pure) | commons/commonMain | Reusable, no dependencies | +| UI components (complex) | Platform-specific | Paradigms differ | +| Navigation | Platform-specific only | Activity vs Window too different | +| Permissions | Platform-specific only | APIs incompatible | +| Platform UX (menus, etc.) | Platform-specific only | Native feel required | + +## See Also + +- [references/abstraction-examples.md](references/abstraction-examples.md) - Good/bad abstraction examples with rationale +- [references/source-set-hierarchy.md](references/source-set-hierarchy.md) - Visual hierarchy with Amethyst examples +- [references/expect-actual-catalog.md](references/expect-actual-catalog.md) - All 24 expect/actual pairs with "why abstracted" +- [references/target-compatibility.md](references/target-compatibility.md) - Platform constraints and future-proofing + +## Scripts + +- `scripts/validate-kmp-structure.sh` - Detect incorrect placements, validate source sets +- `scripts/suggest-kmp-dependency.sh` - Suggest KMP library alternatives (ktor, kotlinx.serialization, etc.) diff --git a/.claude/skills/kotlin-multiplatform/references/abstraction-examples.md b/.claude/skills/kotlin-multiplatform/references/abstraction-examples.md new file mode 100644 index 000000000..e1277dc75 --- /dev/null +++ b/.claude/skills/kotlin-multiplatform/references/abstraction-examples.md @@ -0,0 +1,311 @@ +# Abstraction Examples from Amethyst Codebase + +Real examples of abstraction decisions with rationale. + +## Good Abstractions (Why They Work) + +### 1. Secp256k1Instance - Crypto Signing + +**Location:** expect in commonMain, actual in androidMain/jvmMain/iosMain + +**Code:** +```kotlin +// quartz/src/commonMain/.../Secp256k1Instance.kt +expect object Secp256k1Instance { + fun signSchnorr(data: ByteArray, privKey: ByteArray): ByteArray + fun verifySchnorr(signature: ByteArray, hash: ByteArray, pubKey: ByteArray): Boolean +} +``` + +**Why abstracted:** +- Used by all platforms (Android, Desktop, iOS) +- Security APIs fundamentally different: + - Android: secp256k1-kmp-jni-android (Android Keystore integration) + - Desktop: secp256k1-kmp-jni-jvm (pure JVM crypto) + - iOS: Native Security framework +- Core protocol requirement (Nostr signatures) + +**Decision rationale:** Always abstract crypto - varies by platform security APIs, critical for all platforms. + +--- + +### 2. Log - Platform Logging + +**Location:** expect object in commonMain + +**Code:** +```kotlin +// quartz/src/commonMain/.../Log.kt +expect object Log { + fun d(tag: String, message: String) + fun w(tag: String, message: String, throwable: Throwable?) + fun e(tag: String, message: String, throwable: Throwable?) +} +``` + +**Why abstracted:** +- Used throughout quartz module (protocol library) +- Logging systems differ: + - Android: android.util.Log + - Desktop: println or logging framework + - iOS: NSLog or OSLog +- Simple interface, easy to implement + +**Decision rationale:** Often abstract logging - platform systems differ, widely used, simple interface. + +--- + +### 3. Platform Utils - Time & Platform Name + +**Location:** expect functions in commonMain + +**Code:** +```kotlin +// quartz/src/commonMain/.../Platform.kt +expect fun platform(): String +expect fun currentTimeSeconds(): Long +``` + +**Why abstracted:** +- Used by Nostr event creation (timestamps) +- Platform name for debugging +- Simple utilities, clear platform boundary + +**Decision rationale:** Platform utilities are good abstraction candidates - simple, useful everywhere. + +--- + +### 4. Jackson JSON (jvmAndroid Pattern) + +**Location:** jvmAndroid source set + +**Code:** +```kotlin +// quartz/build.gradle.kts +val jvmAndroid = create("jvmAndroid") { + api(libs.jackson.module.kotlin) // JVM-only library +} +``` + +**Why jvmAndroid (not commonMain):** +- Jackson is JVM-specific library +- Works on Android (JVM) + Desktop (JVM) +- Does NOT work on iOS (not JVM) or web (not JVM) +- Performance-critical JSON parsing + +**Decision rationale:** Use jvmAndroid for JVM libraries shared between Android and Desktop. + +**Future consideration:** For web support, migrate to kotlinx.serialization (works on all platforms). + +--- + +## Bad/Over-Abstractions (Why They Failed) + +### 1. Navigation Abstraction (Avoided) + +**What COULD have been done:** +```kotlin +// ❌ Over-abstraction - DON'T DO THIS +expect interface Navigator { + fun navigate(route: String) + fun popBackStack() +} +``` + +**Why NOT abstracted:** +- Navigation paradigms fundamentally different: + - Android: Activity + Compose Navigation + back stack + - Desktop: Window + screen state + no back stack concept +- Complex APIs don't map well +- Creates leaky abstraction + +**Actual approach:** Keep platform-specific +- Android: `INav` interface + Compose Navigation +- Desktop: Simple screen enum + state + +**Decision rationale:** Never abstract navigation - platforms too different, abstraction would be leaky. + +--- + +### 2. String Resources (Abstraction Planned) + +**Current state:** Platform-specific (over-duplication) + +**Problem:** +```kotlin +// Android uses R.string.* +Text(stringResource(R.string.post_not_found)) + +// Desktop uses hardcoded strings +Text("Post not found") +``` + +**Why NOT yet abstracted:** Waiting for second platform to fully implement UI, then will create StringProvider interface. + +**Planned abstraction:** +```kotlin +// commonMain +interface StringProvider { + fun get(key: String): String +} + +// androidMain +class AndroidStringProvider(context: Context): StringProvider { ... } + +// jvmMain +class DesktopStringProvider: StringProvider { ... } +``` + +**Lesson:** Don't abstract prematurely - wait until second platform needs it, then create proper abstraction. + +--- + +## Platform-Specific Code (Why NOT Abstracted) + +### 1. MainActivity (Android Activity) + +**Location:** amethyst/src/main/.../MainActivity.kt + +**Code:** +```kotlin +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + setContent { + AmethystTheme { + AccountScreen(accountStateViewModel) + } + } + } +} +``` + +**Why platform-specific:** +- AppCompatActivity is Android framework +- Activity lifecycle unique to Android +- enableEdgeToEdge() is Android-specific API +- No equivalent on Desktop (uses Window) + +**Decision rationale:** Android Activity is platform-specific by nature. + +--- + +### 2. Desktop Window & MenuBar + +**Location:** desktopApp/src/jvmMain/.../Main.kt + +**Code:** +```kotlin +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + title = "Amethyst" + ) { + MenuBar { + Menu("File") { + Item("New Note", onClick = { ... }, shortcut = KeyShortcut(Key.N, ctrl = true)) + Item("Quit", onClick = ::exitApplication) + } + } + NavigationRail { ... } // Sidebar navigation + } +} +``` + +**Why platform-specific:** +- Window, MenuBar, NavigationRail are Compose Desktop APIs +- Keyboard shortcuts (Ctrl+N) are desktop paradigm +- Sidebar navigation vs Android bottom nav +- No equivalent on Android + +**Decision rationale:** Desktop UX patterns are platform-specific by nature. + +--- + +### 3. AccountViewModel (Android ViewModel) + +**Location:** amethyst/.../AccountStateViewModel.kt + +**Partially abstracted:** +- Business logic → IAccountState interface (can be shared) +- UI state + lifecycle → AndroidX ViewModel (Android-only) + +**Why not fully abstracted:** +- AndroidX ViewModel lifecycle tied to Android +- Desktop doesn't need ViewModel (simpler state management) +- SavedStateHandle is Android-specific + +**Decision rationale:** Extract business logic to interface, keep UI state platform-specific. + +--- + +## Migration Examples (Android → Shared) + +### Example 1: PubKeyFormatter (Pure Kotlin) + +**Before:** +```kotlin +// amethyst/ui/note/PubKeyFormatter.kt +fun String.toDisplayHexKey(): String { + return "${take(8)}:${takeLast(8)}" +} +``` + +**After:** +```kotlin +// commons/commonMain/formatters/PubKeyFormatter.kt +fun String.toDisplayHexKey(): String { + return "${take(8)}:${takeLast(8)}" +} + +// Both apps use it +import com.vitorpamplona.amethyst.commons.formatters.toDisplayHexKey +``` + +**Why successful:** +- Pure Kotlin, no platform dependencies +- Widely reused +- Simple utility function + +--- + +### Example 2: TimeAgoFormatter (Requires Abstraction) + +**Problem:** +```kotlin +// Uses Android R.string.* +fun timeAgo(timestamp: Long): String { + return context.getString(R.string.x_minutes_ago, minutes) +} +``` + +**Solution:** Abstract string resources +```kotlin +// commonMain +fun timeAgo(timestamp: Long, stringProvider: StringProvider): String { + return stringProvider.get("x_minutes_ago", minutes) +} + +// androidMain +stringProvider = AndroidStringProvider(context) + +// jvmMain +stringProvider = DesktopStringProvider() +``` + +**Why successful:** Clear platform boundary (string resources), useful on both platforms. + +--- + +## Decision Pattern Summary + +| Pattern | Abstract? | Why | +|---------|-----------|-----| +| Pure Kotlin utilities | ✅ YES | No platform dependency, easy | +| Crypto APIs | ✅ YES (expect/actual) | Platform security APIs differ | +| JVM libraries | ⚠️ jvmAndroid | Works on Android+Desktop only | +| UI components (simple) | ✅ YES | Composables work cross-platform | +| UI components (complex) | ❌ NO | Platform dependencies | +| Navigation | ❌ NO | Paradigms too different | +| ViewModels | ⚠️ PARTIAL | Business logic yes, UI state no | +| String resources | ⚠️ PLANNED | Needs abstraction layer | diff --git a/.claude/skills/kotlin-multiplatform/references/expect-actual-catalog.md b/.claude/skills/kotlin-multiplatform/references/expect-actual-catalog.md new file mode 100644 index 000000000..6b6e98c1c --- /dev/null +++ b/.claude/skills/kotlin-multiplatform/references/expect-actual-catalog.md @@ -0,0 +1,163 @@ +# Complete expect/actual Catalog + +All 24 expect declarations in Amethyst quartz module with rationale. + +| # | Name | Type | Purpose | Why Abstracted | Files | +|---|------|------|---------|----------------|-------| +| 1 | AESCBC | class | AES CBC encryption | Platform crypto APIs differ | quartz/.../ciphers/AESCBC.kt | +| 2 | AESGCM | class | AES GCM encryption | Platform crypto APIs differ | quartz/.../ciphers/AESGCM.kt | +| 3 | DigestInstance | class | Hash digests (SHA256) | Platform implementations | quartz/.../diggest/DigestInstance.kt | +| 4 | MacInstance | class | MAC (HMAC) operations | Platform crypto APIs | quartz/.../mac/MacInstance.kt | +| 5 | Sha256 | object | SHA256 hashing | Platform-specific optimizations | quartz/.../sha256/Sha256.kt | +| 6 | LargeCache | object | Large object caching | Platform storage APIs differ | quartz/.../cache/LargeCache.kt | +| 7 | UriParser | object | URI parsing | Platform URL APIs differ | quartz/.../UriParser.kt | +| 8 | UrlEncoder | object | URL encoding | Platform encoding differs | quartz/.../UrlEncoder.kt | +| 9 | Urls | object | URL utilities | Platform URL handling | quartz/.../Urls.kt | +| 10 | Platform | functions | platform(), currentTimeSeconds() | Platform name & time APIs | quartz/.../Platform.kt | +| 11 | Rfc3986 | object | RFC 3986 URL normalization | Used in jvmAndroid | quartz/.../Rfc3986.kt | +| 12 | Secp256k1Instance | object | Bitcoin crypto (secp256k1) | Different libs per platform | quartz/.../Secp256k1Instance.kt | +| 13 | SecureRandom | object | Cryptographically secure random | Platform random APIs differ | quartz/.../SecureRandom.kt | +| 14 | StringExt | functions | String utilities | Platform string handling | quartz/.../StringExt.kt | +| 15 | UnicodeNormalizer | object | Unicode normalization | Platform text APIs | quartz/.../UnicodeNormalizer.kt | +| 16 | GZip | object | GZip compression | Platform compression APIs | quartz/.../GZip.kt | +| 17 | LibSodiumInstance | object | NaCl/libsodium (NIP-44 encryption) | Different libs per platform | quartz/.../LibSodiumInstance.kt | +| 18 | Log | object | Logging | Platform logging systems | quartz/.../Log.kt | +| 19 | BigDecimal | class | Arbitrary precision decimal | Not in Kotlin common stdlib | quartz/.../BigDecimal.kt | +| 20 | BitSet | class | Bit set data structure | Not in Kotlin common stdlib | quartz/.../BitSet.kt | +| 21 | ServerInfoParser | object | Server info parsing (NIP-96) | Platform JSON parsing | quartz/.../nip96.../ServerInfoParser.kt | +| 22 | EventHasherSerializer | object | Event hashing | Platform-specific optimizations | quartz/.../nip01Core.../EventHasherSerializer.kt | +| 23 | OptimizedJsonMapper | object | JSON mapping | Platform JSON libraries | quartz/.../nip01Core.../OptimizedJsonMapper.kt | +| 24 | Address | data class | Address data structure | Platform-specific string handling | quartz/.../nip01Core.../Address.kt | + +## Pattern Analysis + +### Objects (Singletons) - 19 total +Most common pattern for platform-specific singletons: +- Crypto: Secp256k1Instance, LibSodiumInstance, Sha256 +- I/O: UriParser, UrlEncoder, GZip +- Utils: Log, Platform, SecureRandom + +### Classes (Instantiable) - 4 total +For objects that need to maintain state: +- AESCBC, AESGCM (cipher state) +- DigestInstance, MacInstance (hash/MAC state) +- BigDecimal, BitSet (data structures) + +### Functions - 2 total +Simple utilities: +- platform(), currentTimeSeconds() + +## Why Abstracted Categories + +### Crypto (8 items) +**Always abstract:** Security APIs fundamentally different across platforms +- Android: Android Keystore, secp256k1-android +- Desktop: JVM crypto, secp256k1-jvm +- iOS: Security framework, native crypto + +### I/O & Platform Utils (7 items) +**Often abstract:** File systems, URLs, compression differ +- Platform storage APIs +- URL handling varies +- Compression libraries differ + +### Data Structures (2 items) +**Abstract when missing:** Not available in Kotlin common stdlib +- BigDecimal, BitSet not in common + +### JSON/Parsing (3 items) +**Platform-specific optimization:** Uses platform JSON libraries +- Android/Desktop: Jackson (via jvmAndroid) +- iOS: Native parsers + +### Logging (1 item) +**Always abstract:** Platform logging systems differ +- Android: android.util.Log +- Desktop: println or logging framework +- iOS: NSLog or OSLog + +## Actual Implementation Examples + +### Simple Object Pattern + +```kotlin +// commonMain +expect object Log { + fun d(tag: String, message: String) +} + +// androidMain +actual object Log { + actual fun d(tag: String, message: String) { + android.util.Log.d(tag, message) + } +} + +// jvmMain +actual object Log { + actual fun d(tag: String, message: String) { + println("[$tag] $message") + } +} +``` + +### Complex Object with Dependencies + +```kotlin +// commonMain +expect object Secp256k1Instance { + fun signSchnorr(data: ByteArray, privKey: ByteArray): ByteArray +} + +// androidMain - uses JNI bindings +actual object Secp256k1Instance { + actual fun signSchnorr(data: ByteArray, privKey: ByteArray): ByteArray { + return fr.acinq.secp256k1.Secp256k1.signSchnorr(data, privKey, null) + } +} + +// jvmMain - different JNI library +actual object Secp256k1Instance { + actual fun signSchnorr(data: ByteArray, privKey: ByteArray): ByteArray { + return fr.acinq.secp256k1.Secp256k1.signSchnorr(data, privKey, null) + } +} + +// iosMain - native iOS implementation +actual object Secp256k1Instance { + actual fun signSchnorr(data: ByteArray, privKey: ByteArray): ByteArray { + // Uses iOS Security framework or native lib + } +} +``` + +### Class Pattern + +```kotlin +// commonMain +expect class BigDecimal { + constructor(value: String) + fun add(other: BigDecimal): BigDecimal + override fun toString(): String +} + +// jvmAndroid (works on Android + Desktop) +actual typealias BigDecimal = java.math.BigDecimal + +// iosMain +actual class BigDecimal { + private val value: NSDecimalNumber + actual constructor(value: String) { + this.value = NSDecimalNumber(value) + } + // ... implementation +} +``` + +## Decision Patterns + +Ask for each declaration: +1. **Used by 2+ platforms?** → YES (otherwise platform-specific) +2. **Pure Kotlin possible?** → NO (otherwise commonMain) +3. **Varies by platform?** → YES (expect/actual) +4. **JVM-only library?** → NO (otherwise jvmAndroid) diff --git a/.claude/skills/kotlin-multiplatform/references/source-set-hierarchy.md b/.claude/skills/kotlin-multiplatform/references/source-set-hierarchy.md new file mode 100644 index 000000000..ec09e0033 --- /dev/null +++ b/.claude/skills/kotlin-multiplatform/references/source-set-hierarchy.md @@ -0,0 +1,332 @@ +# Source Set Hierarchy in Amethyst + +Visual guide to source set organization with concrete examples from the codebase. + +## Hierarchy Diagram + +``` +┌─────────────────────────────────────────────────────────────┐ +│ commonMain │ +│ Pure Kotlin, no platform APIs │ +│ Examples: │ +│ - Nostr event parsing (TextNoteEvent, MetadataEvent) │ +│ - Business logic (data validation, crypto algorithms) │ +│ - Data models (@Immutable data classes) │ +│ Dependencies: kotlin-stdlib, kotlinx-coroutines │ +└──────────────────────┬──────────────────────────────────────┘ + │ + ┌────────────┴────────────┬───────────────┐ + │ │ │ + ▼ ▼ ▼ +┌──────────────────┐ ┌───────────────────┐ ┌──────────────┐ +│ jvmAndroid │ │ iosMain │ │ Future: │ +│ JVM libraries │ │ iOS common │ │ jsMain │ +│ Examples: │ │ Examples: │ │ wasmMain │ +│ - Jackson JSON │ │ - Platform API │ └──────────────┘ +│ - OkHttp HTTP │ │ - Actuals for │ +│ - url-detector │ │ crypto/I/O │ +│ Dependencies: │ │ Dependencies: │ +│ - Jackson │ │ - Platform libs │ +│ - OkHttp │ └───────┬───────────┘ +└────┬─────────┬───┘ │ + │ │ ├─→ iosX64Main (simulator Intel) + │ │ ├─→ iosArm64Main (device ARM64) + │ │ └─→ iosSimulatorArm64Main (Apple Silicon) + ▼ ▼ +┌──────────┐ ┌───────────┐ +│android │ │ jvmMain │ +│Main │ │ (Desktop) │ +│Examples: │ │ Examples: │ +│- Activity│ │- Window │ +│- ViewModel│ │- MenuBar │ +│- Android │ │- Desktop │ +│ APIs │ │ Compose │ +│Deps: │ │ Deps: │ +│- secp256k│ │- secp256k │ +│ 1-android│ │ 1-jvm │ +│- androidx│ │- Compose │ +│ │ │ Desktop │ +└──────────┘ └───────────┘ +``` + +## Dependency Flow + +``` +Code in commonMain + ↓ can use +Nothing (only Kotlin stdlib) + +Code in jvmAndroid + ↓ can use +commonMain + JVM libraries (Jackson, OkHttp) + +Code in androidMain + ↓ can use +commonMain + jvmAndroid + Android framework + +Code in jvmMain + ↓ can use +commonMain + jvmAndroid + JVM + Compose Desktop + +Code in iosMain + ↓ can use +commonMain + iOS platform APIs +``` + +## Real Examples from Amethyst + +### commonMain - Pure Kotlin + +**File:** `quartz/src/commonMain/.../TextNoteEvent.kt` + +```kotlin +@Immutable +class TextNoteEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseThreadedEvent(...) { + // Pure Kotlin - works everywhere + override fun indexableContent() = "Subject: " + subject() + "\n" + content +} +``` + +**Why commonMain:** +- Pure Kotlin code +- No platform APIs +- Data class with business logic +- Needed by all platforms + +--- + +### jvmAndroid - JVM Libraries + +**File:** `quartz/build.gradle.kts` + +```kotlin +val jvmAndroid = create("jvmAndroid") { + dependsOn(commonMain.get()) + + dependencies { + // Normalizes URLs + api(libs.rfc3986.normalizer) + + // Performant Parser of JSONs into Events + api(libs.jackson.module.kotlin) + + // Parses URLs from Text + api(libs.url.detector) + + // Websockets API + implementation(libs.okhttp) + implementation(libs.okhttpCoroutines) + } +} + +jvmMain { dependsOn(jvmAndroid) } // Desktop gets Jackson, OkHttp +androidMain { dependsOn(jvmAndroid) } // Android gets Jackson, OkHttp +``` + +**Why jvmAndroid:** +- Jackson, OkHttp are JVM-only libraries +- Works on Android (JVM) and Desktop (JVM) +- Does NOT work on iOS (not JVM) or web (not JVM) + +**Usage in code:** +```kotlin +// Can use Jackson in jvmAndroid source set +val mapper = ObjectMapper() +val event = mapper.readValue(json, Event::class.java) +``` + +--- + +### androidMain - Android Platform + +**File:** `amethyst/src/main/.../MainActivity.kt` + +```kotlin +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() // Android API + super.onCreate(savedInstanceState) + + setContent { // Compose for Android + AmethystTheme { + val accountStateViewModel: AccountStateViewModel = viewModel() + AccountScreen(accountStateViewModel) + } + } + } +} +``` + +**Why androidMain:** +- AppCompatActivity is Android framework +- Activity lifecycle Android-specific +- AndroidX libraries (viewModel()) + +**Dependencies:** +```kotlin +androidMain { + dependsOn(jvmAndroid) // Gets Jackson, OkHttp + dependencies { + implementation(libs.androidx.core.ktx) + api(libs.secp256k1.kmp.jni.android) // Android crypto + } +} +``` + +--- + +### jvmMain - Desktop Platform + +**File:** `desktopApp/src/jvmMain/.../Main.kt` + +```kotlin +fun main() = application { + val windowState = rememberWindowState( + width = 1200.dp, + height = 800.dp + ) + + Window( // Compose Desktop API + onCloseRequest = ::exitApplication, + state = windowState, + title = "Amethyst" + ) { + MenuBar { // Desktop-specific + Menu("File") { + Item("New Note", shortcut = KeyShortcut(Key.N, ctrl = true)) + } + } + NavigationRail { ... } // Sidebar + } +} +``` + +**Why jvmMain:** +- Window, MenuBar, NavigationRail are Compose Desktop +- Keyboard shortcuts desktop paradigm +- Different UX from Android (sidebar vs bottom nav) + +**Dependencies:** +```kotlin +jvmMain { + dependsOn(jvmAndroid) // Gets Jackson, OkHttp + dependencies { + implementation(libs.secp256k1.kmp.jni.jvm) // Desktop crypto + implementation(compose.desktop.currentOs) + } +} +``` + +--- + +### iosMain - iOS Platform + +**File:** `quartz/build.gradle.kts` + +```kotlin +iosMain { + dependsOn(commonMain.get()) + dependencies { + // iOS platform dependencies + } +} + +val iosX64Main by getting { dependsOn(iosMain.get()) } +val iosArm64Main by getting { dependsOn(iosMain.get()) } +val iosSimulatorArm64Main by getting { dependsOn(iosMain.get()) } +``` + +**Why iosMain:** +- iOS platform APIs +- Native crypto (Security framework) +- Different from Android/Desktop + +**Architecture targets:** +- iosX64Main: Intel simulator +- iosArm64Main: Device (iPhone, iPad) +- iosSimulatorArm64Main: Apple Silicon simulator + +--- + +## Build Order Matters + +**CRITICAL:** jvmAndroid must be defined BEFORE androidMain and jvmMain: + +```kotlin +// ✅ CORRECT ORDER +val jvmAndroid = create("jvmAndroid") { ... } +jvmMain { dependsOn(jvmAndroid) } +androidMain { dependsOn(jvmAndroid) } + +// ❌ WRONG - Build error +androidMain { dependsOn(jvmAndroid) } // jvmAndroid not defined yet! +val jvmAndroid = create("jvmAndroid") { ... } +``` + +See comment in quartz/build.gradle.kts:131: +```kotlin +// Must be defined before androidMain and jvmMain +val jvmAndroid = create("jvmAndroid") { ... } +``` + +## Choosing the Right Source Set + +Decision flowchart: + +``` +Q: Where should this code go? + +├─ Pure Kotlin? (no platform APIs) +│ └─ commonMain +│ +├─ JVM library? (Jackson, OkHttp) +│ └─ jvmAndroid +│ +├─ Android API? (Activity, Context) +│ └─ androidMain +│ +├─ Desktop API? (Window, MenuBar) +│ └─ jvmMain +│ +└─ iOS API? (platform.posix, Security) + └─ iosMain +``` + +## Future: Web/wasm Source Sets + +**Not yet implemented**, but structure would be: + +``` +commonMain + ├─→ jsMain (JavaScript/Web) + │ └─ JS-specific: DOM APIs, fetch + │ + └─→ wasmMain (WebAssembly) + └─ wasm-specific: limited APIs +``` + +**Constraints:** +- Cannot use jvmAndroid (Jackson, OkHttp) +- Cannot use platform.posix +- Must use pure Kotlin or web-compatible libs (ktor, kotlinx.serialization) + +## Summary Table + +| Source Set | Extends | Can Use | Example Code | +|------------|---------|---------|--------------| +| commonMain | - | Kotlin stdlib only | TextNoteEvent, business logic | +| jvmAndroid | commonMain | JVM libs (Jackson, OkHttp) | JSON parsing, HTTP | +| androidMain | jvmAndroid | Android framework | Activity, ViewModel | +| jvmMain | jvmAndroid | JVM + Compose Desktop | Window, MenuBar | +| iosMain | commonMain | iOS platform | Security framework | +| iosX64Main | iosMain | Simulator (Intel) | Architecture-specific | +| iosArm64Main | iosMain | Device (ARM64) | Architecture-specific | +| jsMain | commonMain | JS/DOM | Web (future) | +| wasmMain | commonMain | wasm APIs | WebAssembly (future) | diff --git a/.claude/skills/kotlin-multiplatform/references/target-compatibility.md b/.claude/skills/kotlin-multiplatform/references/target-compatibility.md new file mode 100644 index 000000000..7ccf720fb --- /dev/null +++ b/.claude/skills/kotlin-multiplatform/references/target-compatibility.md @@ -0,0 +1,345 @@ +# Target Compatibility Guide + +Current targets (Android, JVM/Desktop, iOS) and future targets (web, wasm) with constraints. + +## Current Primary Targets + +### Android (androidMain) + +**Status:** ✅ Mature, production-ready + +**Runtime:** JVM (Dalvik/ART) + +**Available:** +- Android framework (Activity, Context, Intent, etc.) +- AndroidX libraries (ViewModel, Navigation, etc.) +- JVM libraries via jvmAndroid (Jackson, OkHttp) +- Platform-specific crypto: secp256k1-kmp-jni-android + +**Constraints:** +- Mobile UX paradigms (bottom navigation, vertical scroll) +- Touch-first interaction +- Limited screen space +- Battery/performance constraints + +**Example code:** +```kotlin +// androidMain +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + // Android-specific lifecycle + } +} +``` + +--- + +### JVM / Desktop (jvmMain) + +**Status:** ✅ Active development, functional + +**Runtime:** JVM + +**Available:** +- Pure JVM libraries +- JVM libraries via jvmAndroid (Jackson, OkHttp) +- Compose Desktop (Window, MenuBar, etc.) +- Platform-specific crypto: secp256k1-kmp-jni-jvm + +**Constraints:** +- Desktop UX paradigms (sidebar, menus, keyboard shortcuts) +- Keyboard + mouse interaction +- Larger screen space +- Different navigation patterns (no back stack) + +**Example code:** +```kotlin +// jvmMain +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + title = "Amethyst" + ) { + MenuBar { ... } // Desktop-specific + NavigationRail { ... } // Sidebar + } +} +``` + +--- + +### iOS (iosMain + architecture targets) + +**Status:** ⚠️ In development, framework configured + +**Runtime:** Native iOS + +**Source sets:** +- iosMain (common iOS code) +- iosX64Main (Intel simulator) +- iosArm64Main (device - iPhone/iPad) +- iosSimulatorArm64Main (Apple Silicon simulator) + +**Available:** +- iOS platform APIs (platform.posix, Foundation, etc.) +- Native crypto (Security framework) +- SwiftUI integration (via KMP framework) + +**NOT available:** +- JVM libraries (Jackson, OkHttp) +- jvmAndroid source set +- JVM-specific APIs + +**Constraints:** +- Mobile UX (similar to Android) +- Swift/Objective-C interop +- XCFramework distribution +- CocoaPods integration + +**Example code:** +```kotlin +// iosMain +actual object Secp256k1Instance { + actual fun signSchnorr(...): ByteArray { + // Use iOS Security framework + } +} +``` + +**XCFramework setup:** +```kotlin +// quartz/build.gradle.kts +kotlin { + listOf(iosX64(), iosArm64(), iosSimulatorArm64()) + .forEach { target -> + target.binaries.framework { + baseName = "quartz-kmpKit" + isStatic = true + } + } +} +``` + +--- + +## Future Targets + +### Web / JavaScript (jsMain) + +**Status:** ❌ Not implemented, consider for future + +**Runtime:** JavaScript (browser or Node.js) + +**Available:** +- Kotlin/JS stdlib +- JS/DOM APIs +- kotlinx.* libraries (serialization, coroutines, datetime) +- ktor-client (HTTP) + +**NOT available:** +- ❌ JVM libraries (Jackson, OkHttp) +- ❌ jvmAndroid source set +- ❌ platform.posix (no file system access like native) +- ❌ Blocking APIs (different async model - JS event loop) + +**Constraints:** +- Single-threaded event loop +- No blocking calls +- Different async patterns (Promises, async/await) +- Browser security (CORS, no file system) + +**Migration path from current code:** + +| Current (jvmAndroid) | Web-compatible alternative | +|---------------------|---------------------------| +| Jackson JSON | kotlinx.serialization | +| OkHttp HTTP | ktor-client | +| java.math.BigDecimal | Kotlin BigDecimal (coming) | +| Blocking I/O | Suspending functions | + +**Example migration:** +```kotlin +// Current: jvmAndroid +val mapper = ObjectMapper() +val event = mapper.readValue(json, Event::class.java) + +// Future: commonMain (works on web) +val json = Json { ignoreUnknownKeys = true } +val event = json.decodeFromString(jsonString) +``` + +--- + +### WebAssembly (wasmMain) + +**Status:** ❌ Not implemented, experimental Kotlin/Wasm + +**Runtime:** WebAssembly + +**Available:** +- Kotlin/Wasm stdlib +- Limited kotlinx.* libraries +- wasm-specific APIs + +**NOT available:** +- ❌ JVM libraries +- ❌ Full platform.posix +- ❌ Many kotlinx libraries (limited wasm support) + +**Constraints:** +- Even more limited than JS +- Experimental Kotlin support +- Limited library ecosystem + +**Recommendation:** Focus on web (jsMain) first, wasm later. + +--- + +## Cross-Target Compatibility Matrix + +| Feature | Android | JVM/Desktop | iOS | Web (JS) | wasm | +|---------|---------|-------------|-----|----------|------| +| Pure Kotlin | ✅ | ✅ | ✅ | ✅ | ✅ | +| kotlinx.coroutines | ✅ | ✅ | ✅ | ✅ | ⚠️ | +| kotlinx.serialization | ✅ | ✅ | ✅ | ✅ | ⚠️ | +| kotlinx.datetime | ✅ | ✅ | ✅ | ✅ | ⚠️ | +| ktor-client | ✅ | ✅ | ✅ | ✅ | ❌ | +| Jackson JSON | ✅ (jvmAndroid) | ✅ (jvmAndroid) | ❌ | ❌ | ❌ | +| OkHttp | ✅ (jvmAndroid) | ✅ (jvmAndroid) | ❌ | ❌ | ❌ | +| platform.posix | ❌ | ❌ | ✅ | ❌ | ⚠️ | +| Compose Multiplatform | ✅ | ✅ | ⚠️ (experimental) | ⚠️ (experimental) | ❌ | + +Legend: +- ✅ Full support +- ⚠️ Limited/experimental +- ❌ Not available + +--- + +## Future-Proofing Recommendations + +### For Web Compatibility + +**DO:** +- ✅ Use kotlinx.serialization instead of Jackson +- ✅ Use ktor-client instead of OkHttp +- ✅ Use kotlinx.datetime instead of java.time +- ✅ Use suspending functions (non-blocking) +- ✅ Keep business logic in commonMain + +**DON'T:** +- ❌ Put JVM libraries in commonMain +- ❌ Use platform.posix for critical features +- ❌ Use blocking I/O +- ❌ Depend on threading (use coroutines) + +**Example:** +```kotlin +// ❌ NOT web-compatible +// jvmAndroid +fun parseJson(json: String): Event { + val mapper = ObjectMapper() // Jackson - JVM only + return mapper.readValue(json, Event::class.java) +} + +// ✅ Web-compatible +// commonMain +@Serializable +data class Event(...) + +fun parseJson(json: String): Event { + return Json.decodeFromString(json) // Works everywhere +} +``` + +### Current Migration Priorities + +**High priority:** (Needed for web) +1. Migrate Jackson → kotlinx.serialization +2. Migrate OkHttp → ktor-client +3. Move business logic to commonMain + +**Medium priority:** (Nice to have) +1. Abstract date/time handling → kotlinx.datetime +2. Remove platform.posix usage where possible +3. Use suspending functions over blocking + +**Low priority:** (Future optimization) +1. wasm-specific optimizations +2. Platform-specific performance tuning + +--- + +## Platform-Specific Patterns + +### Android vs iOS Differences + +| Aspect | Android | iOS | +|--------|---------|-----| +| **Activity/ViewController** | Activity | UIViewController | +| **Navigation** | Compose Navigation | UINavigationController | +| **Lifecycle** | onCreate, onResume, etc. | viewDidLoad, viewWillAppear | +| **Permissions** | Runtime permissions | Info.plist + runtime | +| **Crypto** | secp256k1-android | Security framework | +| **Storage** | Room, SharedPreferences | Core Data, UserDefaults | + +### Desktop vs Mobile Differences + +| Aspect | Desktop | Mobile | +|--------|---------|--------| +| **Navigation** | Sidebar | Bottom nav | +| **Input** | Keyboard + mouse | Touch | +| **Screen** | Large, landscape | Small, portrait | +| **Windows** | Multi-window | Single app | +| **Shortcuts** | Keyboard shortcuts (Ctrl+N) | None | +| **Menus** | MenuBar | Bottom sheets | + +--- + +## Testing Strategy + +### Per-Target Testing + +**Android:** +- Unit tests: androidTest +- Instrumented: androidInstrumentedTest +- Device/emulator testing + +**Desktop:** +- Unit tests: jvmTest +- Manual desktop app testing + +**iOS:** +- Unit tests: iosTest (iosX64Test, iosArm64Test, etc.) +- Simulator/device testing + +**Web (future):** +- Unit tests: jsTest +- Browser testing (Selenium, Playwright) + +### Shared Testing + +**commonTest:** +- Business logic tests +- Pure Kotlin code +- Works on all platforms + +```kotlin +// commonTest +class EventParsingTest { + @Test + fun parseTextNoteEvent() { + // Tests run on all platforms + } +} +``` + +--- + +## Summary + +**Current Focus:** Android, JVM/Desktop, iOS (active development) + +**Future Considerations:** Web (requires migration from Jackson/OkHttp) + +**Key Decision:** Prefer kotlinx.* libraries over JVM-specific libs for future web compatibility. diff --git a/.claude/skills/kotlin-multiplatform/scripts/suggest-kmp-dependency.sh b/.claude/skills/kotlin-multiplatform/scripts/suggest-kmp-dependency.sh new file mode 100755 index 000000000..5b07cd810 --- /dev/null +++ b/.claude/skills/kotlin-multiplatform/scripts/suggest-kmp-dependency.sh @@ -0,0 +1,166 @@ +#!/bin/bash +# Suggests KMP library alternatives for JVM-specific dependencies + +set -e + +PROJECT_ROOT="${1:-.}" +cd "$PROJECT_ROOT" + +echo "=== KMP Dependency Suggestions ===" +echo + +# Colors +YELLOW='\033[1;33m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +NC='\033[0m' + +SUGGESTIONS_FOUND=0 + +# Check for Jackson (suggest kotlinx.serialization) +echo "📦 Checking for Jackson JSON..." +if grep -r "jackson" */build.gradle.kts 2>/dev/null | grep -q "implementation\|api"; then + echo -e "${YELLOW}⚠ Found Jackson dependency${NC}" + echo " Current: Jackson (JVM-only)" + echo -e " ${GREEN}Suggest: kotlinx.serialization${NC} (works on all platforms)" + echo + echo " Migration:" + echo " // Remove:" + echo " api(libs.jackson.module.kotlin)" + echo + echo " // Add to commonMain:" + echo " implementation(libs.kotlinx.serialization.json)" + echo + echo " // Code change:" + echo " // Before (Jackson):" + echo " val mapper = ObjectMapper()" + echo " val event = mapper.readValue(json, Event::class.java)" + echo + echo " // After (kotlinx.serialization):" + echo " @Serializable" + echo " data class Event(...)" + echo " val event = Json.decodeFromString(json)" + echo + SUGGESTIONS_FOUND=$((SUGGESTIONS_FOUND + 1)) +else + echo -e "${GREEN}✓ Not using Jackson (or already using kotlinx.serialization)${NC}" +fi + +# Check for OkHttp (suggest ktor) +echo +echo "📦 Checking for OkHttp..." +if grep -r "okhttp" */build.gradle.kts 2>/dev/null | grep -q "implementation\|api"; then + echo -e "${YELLOW}⚠ Found OkHttp dependency${NC}" + echo " Current: OkHttp (JVM-only)" + echo -e " ${GREEN}Suggest: ktor-client${NC} (works on all platforms)" + echo + echo " Migration:" + echo " // Remove:" + echo " implementation(libs.okhttp)" + echo + echo " // Add to commonMain:" + echo " implementation(libs.ktor.client.core)" + echo " // Platform-specific engines:" + echo " // androidMain: implementation(libs.ktor.client.android)" + echo " // jvmMain: implementation(libs.ktor.client.cio)" + echo " // iosMain: implementation(libs.ktor.client.darwin)" + echo + echo " // Code change:" + echo " // Before (OkHttp):" + echo " val client = OkHttpClient()" + echo " val request = Request.Builder().url(url).build()" + echo " val response = client.newCall(request).execute()" + echo + echo " // After (ktor):" + echo " val client = HttpClient()" + echo " val response: String = client.get(url)" + echo + SUGGESTIONS_FOUND=$((SUGGESTIONS_FOUND + 1)) +else + echo -e "${GREEN}✓ Not using OkHttp (or already using ktor)${NC}" +fi + +# Check for java.time (suggest kotlinx.datetime) +echo +echo "📦 Checking for java.time usage..." +if find */src -name "*.kt" 2>/dev/null | xargs grep -l "import java.time\." >/dev/null 2>&1; then + echo -e "${YELLOW}⚠ Found java.time imports${NC}" + echo " Current: java.time (JVM-only)" + echo -e " ${GREEN}Suggest: kotlinx.datetime${NC} (works on all platforms)" + echo + echo " Migration:" + echo " // Add to commonMain:" + echo " implementation(libs.kotlinx.datetime)" + echo + echo " // Code change:" + echo " // Before (java.time):" + echo " import java.time.Instant" + echo " val now = Instant.now()" + echo + echo " // After (kotlinx.datetime):" + echo " import kotlinx.datetime.Clock" + echo " val now = Clock.System.now()" + echo + SUGGESTIONS_FOUND=$((SUGGESTIONS_FOUND + 1)) +else + echo -e "${GREEN}✓ Not using java.time (or already using kotlinx.datetime)${NC}" +fi + +# Check for java.math.BigDecimal +echo +echo "📦 Checking for java.math.BigDecimal usage..." +if find */src -name "*.kt" 2>/dev/null | xargs grep -l "import java.math.BigDecimal" >/dev/null 2>&1; then + echo -e "${YELLOW}⚠ Found java.math.BigDecimal imports${NC}" + echo " Current: java.math.BigDecimal (JVM-only)" + echo -e " ${BLUE}Note:${NC} KMP BigDecimal not yet in stable kotlinx" + echo + echo " Options:" + echo " 1. Use expect/actual (current approach in quartz)" + echo " 2. Wait for kotlinx.decimal (proposal stage)" + echo " 3. Use third-party KMP library (e.g., bignum)" + echo + SUGGESTIONS_FOUND=$((SUGGESTIONS_FOUND + 1)) +else + echo -e "${GREEN}✓ Not using java.math.BigDecimal directly${NC}" +fi + +# Check for platform.posix usage +echo +echo "📦 Checking for platform.posix usage..." +if find */src/commonMain -name "*.kt" 2>/dev/null | xargs grep -l "import platform.posix\." >/dev/null 2>&1; then + echo -e "${YELLOW}⚠ Found platform.posix in commonMain${NC}" + echo " Current: platform.posix (native platforms only, not web)" + echo -e " ${GREEN}Suggest:${NC} Abstract file I/O with expect/actual" + echo + echo " For web compatibility:" + echo " - iOS/Native: platform.posix" + echo " - Web: Use kotlinx-io or ktor file APIs" + echo " - Create expect/actual for file operations" + echo + SUGGESTIONS_FOUND=$((SUGGESTIONS_FOUND + 1)) +else + echo -e "${GREEN}✓ Not using platform.posix in commonMain${NC}" +fi + +# Summary +echo +echo "=== Summary ===" +if [ "$SUGGESTIONS_FOUND" -eq 0 ]; then + echo -e "${GREEN}✓ No JVM-specific dependencies found!${NC}" + echo " Your code is ready for web/wasm targets." +else + echo -e "${YELLOW}Found $SUGGESTIONS_FOUND suggestion(s) for KMP alternatives${NC}" + echo + echo "Priority recommendations:" + echo " 1. ${GREEN}High:${NC} Jackson → kotlinx.serialization (enables web support)" + echo " 2. ${GREEN}High:${NC} OkHttp → ktor-client (enables web support)" + echo " 3. ${GREEN}Medium:${NC} java.time → kotlinx.datetime" + echo " 4. ${GREEN}Low:${NC} Consider web compatibility for platform.posix usage" + echo + echo "Resources:" + echo " - kotlinx.serialization: https://github.com/Kotlin/kotlinx.serialization" + echo " - ktor: https://ktor.io/docs/client.html" + echo " - kotlinx.datetime: https://github.com/Kotlin/kotlinx-datetime" +fi + +exit 0 diff --git a/.claude/skills/kotlin-multiplatform/scripts/validate-kmp-structure.sh b/.claude/skills/kotlin-multiplatform/scripts/validate-kmp-structure.sh new file mode 100755 index 000000000..d8713bcd0 --- /dev/null +++ b/.claude/skills/kotlin-multiplatform/scripts/validate-kmp-structure.sh @@ -0,0 +1,126 @@ +#!/bin/bash +# Validates KMP source set structure and detects common issues + +set -e + +PROJECT_ROOT="${1:-.}" +cd "$PROJECT_ROOT" + +echo "=== Validating KMP Structure ===" +echo + +# Colors for output +RED='\033[0;31m' +YELLOW='\033[1;33m' +GREEN='\033[0;32m' +NC='\033[0m' # No Color + +ISSUES_FOUND=0 + +# Check 1: jvmAndroid defined before androidMain/jvmMain +echo "📋 Checking source set definition order..." +if [ -f "quartz/build.gradle.kts" ]; then + jvmandroid_line=$(grep -n "val jvmAndroid = create" quartz/build.gradle.kts | cut -d: -f1) + android_line=$(grep -n "androidMain {" quartz/build.gradle.kts | cut -d: -f1) + jvm_line=$(grep -n "jvmMain {" quartz/build.gradle.kts | cut -d: -f1) + + if [ -n "$jvmandroid_line" ] && [ -n "$android_line" ] && [ -n "$jvm_line" ]; then + if [ "$jvmandroid_line" -lt "$android_line" ] && [ "$jvmandroid_line" -lt "$jvm_line" ]; then + echo -e "${GREEN}✓${NC} jvmAndroid defined before androidMain and jvmMain" + else + echo -e "${RED}✗${NC} jvmAndroid must be defined BEFORE androidMain and jvmMain" + ISSUES_FOUND=$((ISSUES_FOUND + 1)) + fi + fi +fi + +# Check 2: Platform code in commonMain (Android imports) +echo +echo "📋 Checking for platform code in commonMain..." +android_imports_in_common=$(find */src/commonMain -name "*.kt" 2>/dev/null | xargs grep -l "^import android\." || true) +if [ -n "$android_imports_in_common" ]; then + echo -e "${RED}✗${NC} Found Android imports in commonMain:" + echo "$android_imports_in_common" | sed 's/^/ /' + echo " Fix: Move to androidMain or create expect/actual" + ISSUES_FOUND=$((ISSUES_FOUND + 1)) +else + echo -e "${GREEN}✓${NC} No Android imports in commonMain" +fi + +# Check 3: JVM libraries in commonMain (Jackson, OkHttp) +echo +echo "📋 Checking for JVM libraries in commonMain..." +jvm_imports_in_common=$(find */src/commonMain -name "*.kt" 2>/dev/null | xargs grep -l "^import com.fasterxml.jackson\|^import okhttp3\." || true) +if [ -n "$jvm_imports_in_common" ]; then + echo -e "${RED}✗${NC} Found JVM library imports in commonMain:" + echo "$jvm_imports_in_common" | sed 's/^/ /' + echo " Fix: Move to jvmAndroid or migrate to kotlinx.serialization/ktor" + ISSUES_FOUND=$((ISSUES_FOUND + 1)) +else + echo -e "${GREEN}✓${NC} No JVM library imports in commonMain" +fi + +# Check 4: Unmatched expect/actual declarations +echo +echo "📋 Checking expect/actual pairs..." +expect_files=$(find */src/commonMain -name "*.kt" 2>/dev/null | xargs grep -l "^expect " || true) +if [ -n "$expect_files" ]; then + for file in $expect_files; do + # Extract declarations + expects=$(grep "^expect \(class\|object\|fun\|interface\)" "$file" | sed 's/expect //' | awk '{print $2}' | sed 's/[({].*$//') + + # Check for actuals in platform source sets + for expect_name in $expects; do + actual_count=0 + for platform in androidMain jvmMain iosMain; do + platform_dir=$(dirname "$file" | sed "s/commonMain/$platform/") + platform_file="${platform_dir}/$(basename "$file")" + if [ -f "$platform_file" ] && grep -q "actual.*$expect_name" "$platform_file"; then + actual_count=$((actual_count + 1)) + fi + done + + if [ "$actual_count" -eq 0 ]; then + echo -e "${YELLOW}⚠${NC} No actual implementations found for: $expect_name in $file" + echo " Check: androidMain, jvmMain, iosMain" + ISSUES_FOUND=$((ISSUES_FOUND + 1)) + fi + done + done +else + echo -e "${GREEN}✓${NC} No expect declarations to validate" +fi + +# Check 5: Duplicated business logic across platforms +echo +echo "📋 Checking for potential code duplication..." +# This is a heuristic check - look for similar function names in different platform source sets +common_functions=$(find */src/commonMain -name "*.kt" 2>/dev/null | xargs grep -h "^fun " | awk '{print $2}' | sed 's/[({<].*$//' | sort -u || true) +if [ -n "$common_functions" ]; then + for func in $common_functions; do + android_count=$(find */src/androidMain -name "*.kt" 2>/dev/null | xargs grep -l "^fun $func" | wc -l) + jvm_count=$(find */src/jvmMain -name "*.kt" 2>/dev/null | xargs grep -l "^fun $func" | wc -l) + + if [ "$android_count" -gt 0 ] && [ "$jvm_count" -gt 0 ]; then + echo -e "${YELLOW}⚠${NC} Function '$func' found in both androidMain and jvmMain" + echo " Consider: Move to commonMain or jvmAndroid if truly shared" + fi + done +fi + +# Summary +echo +echo "=== Summary ===" +if [ "$ISSUES_FOUND" -eq 0 ]; then + echo -e "${GREEN}✓ All checks passed!${NC}" + exit 0 +else + echo -e "${RED}✗ Found $ISSUES_FOUND issue(s)${NC}" + echo + echo "Common fixes:" + echo " 1. Platform code in commonMain → Move to androidMain or create expect/actual" + echo " 2. JVM libraries in commonMain → Move to jvmAndroid or migrate to kotlinx.*" + echo " 3. Missing actual implementations → Implement in all target platforms" + echo " 4. Duplicated logic → Move to commonMain or jvmAndroid" + exit 1 +fi From f180fd39e1296f85e6d43cb378486bdd9efb1379 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Tue, 30 Dec 2025 15:45:02 +0200 Subject: [PATCH 005/137] finish skills --- .claude/core-skills-plan.md | 118 ++-- .claude/skills/compose-expert/SKILL.md | 577 ++++++++++++++++ .../compose-expert/references/icon-assets.md | 365 ++++++++++ .../references/shared-composables-catalog.md | 281 ++++++++ .../references/state-patterns.md | 334 +++++++++ .../scripts/find-composables.sh | 34 + .claude/skills/gradle-expert/SKILL.md | 548 +++++++++++++++ .../references/build-commands.md | 214 ++++++ .../gradle-expert/references/common-errors.md | 643 ++++++++++++++++++ .../references/dependency-graph.md | 266 ++++++++ .../references/version-catalog-guide.md | 422 ++++++++++++ .../scripts/analyze-build-time.sh | 45 ++ .../scripts/fix-dependency-conflicts.sh | 65 ++ .claude/skills/nostr-expert/SKILL.md | 551 +++++++++++++++ .../references/event-hierarchy.md | 293 ++++++++ .../nostr-expert/references/nip-catalog.md | 179 +++++ .../nostr-expert/references/tag-patterns.md | 336 +++++++++ .../skills/nostr-expert/scripts/nip-lookup.sh | 58 ++ 18 files changed, 5287 insertions(+), 42 deletions(-) create mode 100644 .claude/skills/compose-expert/SKILL.md create mode 100644 .claude/skills/compose-expert/references/icon-assets.md create mode 100644 .claude/skills/compose-expert/references/shared-composables-catalog.md create mode 100644 .claude/skills/compose-expert/references/state-patterns.md create mode 100755 .claude/skills/compose-expert/scripts/find-composables.sh create mode 100644 .claude/skills/gradle-expert/SKILL.md create mode 100644 .claude/skills/gradle-expert/references/build-commands.md create mode 100644 .claude/skills/gradle-expert/references/common-errors.md create mode 100644 .claude/skills/gradle-expert/references/dependency-graph.md create mode 100644 .claude/skills/gradle-expert/references/version-catalog-guide.md create mode 100644 .claude/skills/gradle-expert/scripts/analyze-build-time.sh create mode 100644 .claude/skills/gradle-expert/scripts/fix-dependency-conflicts.sh create mode 100644 .claude/skills/nostr-expert/SKILL.md create mode 100644 .claude/skills/nostr-expert/references/event-hierarchy.md create mode 100644 .claude/skills/nostr-expert/references/nip-catalog.md create mode 100644 .claude/skills/nostr-expert/references/tag-patterns.md create mode 100755 .claude/skills/nostr-expert/scripts/nip-lookup.sh diff --git a/.claude/core-skills-plan.md b/.claude/core-skills-plan.md index ab4dcad6e..6b51cdab0 100644 --- a/.claude/core-skills-plan.md +++ b/.claude/core-skills-plan.md @@ -29,7 +29,7 @@ Create 8 hybrid domain skills combining general expertise with AmethystMultiplat --- -### 2. gradle-expert +### 2. gradle-expert ✅ COMPLETED **Focus:** Build optimization, dependency resolution, multi-module KMP troubleshooting **SKILL.md sections:** @@ -44,11 +44,15 @@ Create 8 hybrid domain skills combining general expertise with AmethystMultiplat **Bundled resources:** - `references/build-commands.md` - Common gradle tasks - `references/dependency-graph.md` - Module visualization +- `references/version-catalog-guide.md` - Version catalog patterns +- `references/common-errors.md` - Troubleshooting guide - `scripts/analyze-build-time.sh` - Performance report - `scripts/fix-dependency-conflicts.sh` - Conflict patterns **Differentiation:** Focus on 4-module structure, KMP + Android + Desktop combo, specific issues (compose conflicts). +**Status:** ✅ SKILL.md (549 lines) + 4 references + 2 scripts created at `.claude/skills/gradle-expert/` + --- ### 3. kotlin-expert ✅ DRAFT COMPLETE @@ -86,25 +90,28 @@ Create 8 hybrid domain skills combining general expertise with AmethystMultiplat --- -### 4. compose-expert +### 4. compose-expert ✅ COMPLETED **Focus:** Shared composables, state management, animations, Material3 **SKILL.md sections:** - Shared composables philosophy (100+ already shared in commons/commonMain) -- State management: remember, derivedStateOf, produceState -- Recomposition optimization: @Stable/@Immutable +- State management: remember, derivedStateOf, produceState (visual patterns) +- Recomposition optimization: @Stable/@Immutable (visual usage) - Material3 conventions: theming -- Custom icons: ImageVector builders (38+ icons) +- Custom icons: ImageVector builders (robohash pattern) - Platform differences: Desktop vs Android UI - Performance: lazy lists, image loading +- Decision framework: share by default in commonMain **Bundled resources:** -- `references/shared-composables-catalog.md` - 100+ composables -- `references/state-patterns.md` - State hoisting examples -- `references/icon-assets.md` - Custom icon catalog -- `scripts/find-composables.sh` - Grep @Composable +- `references/shared-composables-catalog.md` - Complete catalog with patterns +- `references/state-patterns.md` - State hoisting, derivedStateOf examples +- `references/icon-assets.md` - ImageVector patterns, roboBuilder DSL +- `scripts/find-composables.sh` - Grep @Composable utility -**Differentiation:** Multiplatform Compose patterns, shared vs platform UI philosophy, Amethyst conventions (robohash, custom icons). +**Differentiation:** Multiplatform Compose patterns, shared vs platform UI philosophy, Amethyst conventions (robohash, custom icons). Delegates navigation to platform experts, defers Kotlin language details to kotlin-expert. + +**Status:** ✅ SKILL.md (578 lines) + 3 references + 1 script created at `.claude/skills/compose-expert/` --- @@ -206,26 +213,30 @@ Create 8 hybrid domain skills combining general expertise with AmethystMultiplat --- -### 8. nostr-expert +### 8. nostr-expert ✅ COMPLETED **Focus:** Nostr protocol, NIPs, Quartz architecture, event patterns **SKILL.md sections:** - Quartz architecture: package structure by NIP (57 NIPs implemented) -- Event anatomy: IEvent, Event, kinds, tags (verified in TextNoteEvent.kt) -- NIP implementation patterns: Event class, tags, builders -- Common event types: TextNoteEvent, MetadataEvent, ReactionEvent -- Tag patterns: aTag, eTag, pTag builders -- Relay client: WebSocket, subscriptions -- Cryptography: secp256k1, NIP-44 encryption -- Event builders: TagArrayBuilder DSL +- Event anatomy: IEvent, Event, kinds, tags +- EventTemplate & TagArrayBuilder DSL patterns +- Common event types: TextNoteEvent, MetadataEvent, ReactionEvent, Addressable events +- Tag patterns: e-tag, p-tag, a-tag, d-tag with builders +- Threading (NIP-10): reply/root markers +- Cryptography: secp256k1 signing, NIP-44 encryption +- Bech32 encoding: npub, nsec, note, nevent +- Event validation & verification +- Common workflows: publishing, querying, zaps, gift-wrapped DMs **Bundled resources:** -- `references/nip-catalog.md` - All 57 NIPs with code links -- `references/event-hierarchy.md` - Event class diagram -- `references/tag-patterns.md` - Tag builders from nip01Core/tags/ -- `scripts/nip-lookup.sh` - Find NIP implementation files +- `references/nip-catalog.md` - All 57 NIPs with package locations (179 lines) +- `references/event-hierarchy.md` - Event class hierarchy, kind classifications (293 lines) +- `references/tag-patterns.md` - Tag structure, TagArrayBuilder DSL, parsing (251 lines) +- `scripts/nip-lookup.sh` - Find NIP implementations by number or search term -**Differentiation:** nostr-protocol agent = NIP specs. This skill = Quartz implementation (57 NIPs), code patterns. +**Differentiation:** nostr-protocol agent = NIP specs. This skill = Quartz implementation patterns (57 NIPs), concrete code examples from codebase. + +**Status:** ✅ SKILL.md (552 lines) + 3 references + 1 script created at `.claude/skills/nostr-expert/` --- @@ -241,10 +252,13 @@ Using skill-creator 10-step methodology per skill: **Per-Skill Implementation:** - kotlin-multiplatform: ✅ COMPLETED -- android-expert: ✅ DRAFT COMPLETE -- desktop-expert: ✅ DRAFT COMPLETE -- kotlin-expert: ✅ DRAFT COMPLETE -- Remaining 4 skills: Pending (gradle-expert, compose-expert, ios-expert, nostr-expert) +- gradle-expert: ✅ COMPLETED +- kotlin-expert: ✅ COMPLETED +- compose-expert: ✅ COMPLETED +- desktop-expert: ✅ COMPLETED +- android-expert: ✅ COMPLETED +- nostr-expert: ✅ COMPLETED +- ios-expert: ⏸️ DEFERRED (iOS not yet implemented in AmethystMultiplatform) ## Critical Files Referenced @@ -267,22 +281,42 @@ Using skill-creator 10-step methodology per skill: 1. ✅ Save this plan as `.claude/core-skills-plan.md` for reference 2. ✅ Completed kotlin-multiplatform skill -3. ✅ Completed android-expert skill (DRAFT) -4. ✅ Completed desktop-expert skill (DRAFT) -5. ⏳ **Next: Pick one of the remaining 5 skills:** - - gradle-expert - Build optimization, dependency resolution - - kotlin-expert - Coroutines, Flow, sealed classes - - compose-expert - Shared composables, state management - - ios-expert - iOS platform patterns, XCFramework - - nostr-expert - Nostr protocol, NIPs, Quartz +3. ✅ Completed gradle-expert skill +4. ✅ Completed kotlin-expert skill +5. ✅ Completed compose-expert skill +6. ✅ Completed desktop-expert skill +7. ✅ Completed android-expert skill +8. ✅ Completed nostr-expert skill +9. ⏸️ Deferred ios-expert (iOS not yet implemented in codebase) -## Current Focus: 3 skills completed +## Current Status: 7/8 Skills Completed -**Completed:** -- kotlin-multiplatform ✅ -- android-expert ✅ -- desktop-expert ✅ +**Completed Skills (Auto-loaded from `.claude/skills/`):** +1. ✅ kotlin-multiplatform (KMP architecture, jvmAndroid pattern, expect/actual) +2. ✅ gradle-expert (Build system, dependencies, version catalog, troubleshooting) +3. ✅ kotlin-expert (Flow state, sealed classes, @Immutable, DSL builders) +4. ✅ compose-expert (Shared composables, state management, Material3, ImageVector) +5. ✅ desktop-expert (Desktop UX, window management, Compose Desktop APIs) +6. ✅ android-expert (Android platform APIs, navigation, permissions) +7. ✅ nostr-expert (Nostr protocol, Quartz implementation, NIPs, events, tags) -**Remaining:** 5 skills +**Deferred:** +- ⏸️ ios-expert (iOS not implemented yet in AmethystMultiplatform) -Ready to start next skill when requested. +## Skill Loading + +**All completed skills are automatically loaded** when this project opens. Skills are auto-discovered from `.claude/skills/` directory. + +To manually verify skills are loaded: +```bash +ls -1 .claude/skills/ +``` + +Should show: +- android-expert/ +- compose-expert/ +- desktop-expert/ +- gradle-expert/ +- kotlin-expert/ +- kotlin-multiplatform/ +- nostr-expert/ diff --git a/.claude/skills/compose-expert/SKILL.md b/.claude/skills/compose-expert/SKILL.md new file mode 100644 index 000000000..0ba02c013 --- /dev/null +++ b/.claude/skills/compose-expert/SKILL.md @@ -0,0 +1,577 @@ +--- +name: compose-expert +description: Advanced Compose Multiplatform UI patterns for shared composables. Use when working with visual UI components, state management patterns (remember, derivedStateOf, produceState), recomposition optimization (@Stable/@Immutable visual usage), Material3 theming, custom ImageVector icons, or determining whether to share UI in commonMain vs keep platform-specific. Delegates navigation to android-expert/desktop-expert. Complements kotlin-expert (handles Kotlin language aspects of state/annotations). +--- + +# Compose Multiplatform Expert + +Visual UI patterns for sharing composables across Android and Desktop. + +## When to Use This Skill + +- Creating or refactoring shared UI components +- Deciding whether to share UI in `commonMain` or keep platform-specific +- Building custom ImageVector icons (robohash pattern) +- State management: remember, derivedStateOf, produceState +- Recomposition optimization: visual usage of @Stable/@Immutable +- Material3 theming and styling +- Performance: lazy lists, image loading + +**Delegate to other skills:** +- Navigation structure → `android-expert`, `desktop-expert` +- Kotlin state patterns (StateFlow, sealed classes) → `kotlin-expert` +- Build configuration → `gradle-expert` + +## Philosophy: Share by Default + +**Default to `commons/commonMain`** unless platform experts indicate otherwise. + +### Always Share + +- **UI components**: Buttons, cards, lists, dialogs, inputs +- **State visualization**: Loading, empty, error states +- **Custom icons**: ImageVector assets (robohash, custom paths) +- **Theme utilities**: Color calculations, style helpers +- **Material3 components**: Any UI using Material primitives + +### Keep Platform-Specific + +- **Navigation structure**: Bottom nav (Android) vs Sidebar (Desktop) +- **Screen layouts**: Platform-specific scaffolding +- **System integrations**: File pickers, notifications, share sheets +- **Platform UX**: Gestures, keyboard shortcuts, window management + +### Decision Framework + +1. **Uses only Material3 primitives?** → Share in `commonMain` +2. **Requires platform system APIs?** → Platform-specific +3. **Pure visual component without navigation?** → Share in `commonMain` +4. **Needs platform UX patterns?** → Ask `android-expert` or `desktop-expert` + +If uncertain, **default to sharing** - easier to split later than merge. + +## Shared Composable Anatomy + +### Structure + +```kotlin +@Composable +fun SharedComponent( + // State parameters (read-only) + data: DataClass, + isLoading: Boolean, + // Event parameters (write-only) + onAction: () -> Unit, + // Visual parameters + modifier: Modifier = Modifier, + // Optional customization + colors: ComponentColors = ComponentDefaults.colors() +) { + // Implementation +} +``` + +**Pattern**: State down, events up +- Parameters above modifier = required state/events +- `modifier` parameter = layout control +- Parameters below modifier = optional customization + +### Example: AddButton + +```kotlin +@Composable +fun AddButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + text: String = "Add", + enabled: Boolean = true +) { + OutlinedButton( + modifier = modifier, + enabled = enabled, + onClick = onClick, + shape = ActionButtonShape, + contentPadding = ActionButtonPadding + ) { + Text(text = text, textAlign = TextAlign.Center) + } +} + +// Shared constants for consistency +val ActionButtonShape = RoundedCornerShape(20.dp) +val ActionButtonPadding = PaddingValues(vertical = 0.dp, horizontal = 16.dp) +``` + +**Why this works on all platforms:** +- Material3 primitives (OutlinedButton, Text) +- No platform APIs +- Configurable through parameters +- Consistent styling via shared constants + +## State Management Patterns + +### remember - Cache Across Recompositions + +```kotlin +@Composable +fun ExpandableCard() { + var isExpanded by remember { mutableStateOf(false) } + + Column { + IconButton(onClick = { isExpanded = !isExpanded }) { + Icon( + if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore, + contentDescription = if (isExpanded) "Collapse" else "Expand" + ) + } + + if (isExpanded) { + Text("Expanded content...") + } + } +} +``` + +**Visual pattern**: Toggle button → state changes → UI expands/collapses +**Use for**: Simple UI state (toggles, counters, text input) + +### derivedStateOf - Optimize Frequent Changes + +```kotlin +@Composable +fun ScrollToTopButton(listState: LazyListState) { + // Only recomposes when showButton changes, not every scroll pixel + val showButton by remember { + derivedStateOf { + listState.firstVisibleItemIndex > 0 + } + } + + if (showButton) { + FloatingActionButton(onClick = { /* scroll to top */ }) { + Icon(Icons.Default.ArrowUpward, null) + } + } +} +``` + +**Visual pattern**: Scroll position (0, 1, 2...) → boolean (show/hide) → Button visibility +**Use for**: Input changes frequently, derived result changes rarely +**Performance**: Prevents recomposition on every scroll event + +### produceState - Async to Compose State + +```kotlin +@Composable +fun LoadUserProfile(userId: String): State { + return produceState(initialValue = null, userId) { + value = repository.fetchUser(userId) + } +} + +@Composable +fun ProfileScreen(userId: String) { + val user by LoadUserProfile(userId) + + when (user) { + null -> LoadingState("Loading profile...") + else -> ProfileCard(user!!) + } +} +``` + +**Visual pattern**: Async operation → state updates → UI reflects changes +**Use for**: Convert Flow, LiveData, callbacks into Compose state +**Lifecycle**: Coroutine cancelled when composable leaves composition + +For Kotlin-specific state patterns (StateFlow, sealed classes), see `kotlin-expert`. + +## State Hoisting + +Move state up to make composables reusable: + +```kotlin +// ❌ Stateful - hard to test, can't control externally +@Composable +fun BadSearchBar() { + var query by remember { mutableStateOf("") } + TextField(value = query, onValueChange = { query = it }) +} + +// ✅ Stateless - reusable, testable +@Composable +fun GoodSearchBar( + query: String, + onQueryChange: (String) -> Unit, + modifier: Modifier = Modifier +) { + TextField( + value = query, + onValueChange = onQueryChange, + modifier = modifier + ) +} + +@Composable +fun SearchScreen() { + var query by remember { mutableStateOf("") } + + Column { + GoodSearchBar(query = query, onQueryChange = { query = it }) + SearchResults(query = query) + } +} +``` + +**Principle**: State up, events down +- State: `query: String` (read-only parameter) +- Events: `onQueryChange: (String) -> Unit` (callback parameter) + +## Recomposition Optimization + +### Visual Usage of @Immutable + +Use @Immutable on data classes passed to composables: + +```kotlin +@Immutable +data class UserProfile(val name: String, val avatar: String) + +@Composable +fun ProfileCard(profile: UserProfile) { + // Only recomposes when profile instance changes + Row { + RobohashImage(robot = profile.avatar) + Text(profile.name, style = MaterialTheme.typography.titleMedium) + } +} +``` + +**Visual effect**: Prevents recomposition when parent recomposes with same data +**Pattern**: Mark parameter data classes as @Immutable +**Note**: For Kotlin language details on @Immutable, see `kotlin-expert` + +### Stable Parameters + +```kotlin +// ✅ Stable - won't trigger recomposition unless colors instance changes +@Composable +fun ThemedCard( + content: String, + colors: CardColors = CardDefaults.colors(), + modifier: Modifier = Modifier +) { + Card(colors = colors, modifier = modifier) { + Text(content) + } +} +``` + +For @Stable annotation details, see `kotlin-expert`. + +## Material3 Theming + +All shared composables use Material3 for consistency: + +```kotlin +@Composable +fun ThemedComponent() { + val bg = MaterialTheme.colorScheme.background + val fg = MaterialTheme.colorScheme.onBackground + val primary = MaterialTheme.colorScheme.primary + + Column( + modifier = Modifier.background(bg) + ) { + Text( + "Title", + style = MaterialTheme.typography.headlineMedium, + color = fg + ) + Button( + onClick = { /* ... */ }, + colors = ButtonDefaults.buttonColors(containerColor = primary) + ) { + Text("Action") + } + } +} +``` + +**Principles:** +- Colors: `MaterialTheme.colorScheme.*` +- Typography: `MaterialTheme.typography.*` +- Shapes: `MaterialTheme.shapes.*` + +### Theme Detection + +```kotlin +@Composable +private fun isLightTheme(): Boolean { + val background = MaterialTheme.colorScheme.background + return (background.red + background.green + background.blue) / 3 > 0.5f +} + +@Composable +fun ThemedIcon() { + val isDark = !isLightTheme() + val tint = if (isDark) Color.White else Color.Black + Icon(Icons.Default.Face, null, tint = tint) +} +``` + +## Custom Icons: ImageVector Pattern + +Amethyst uses ImageVector for multiplatform icons. + +### roboBuilder DSL + +```kotlin +fun roboBuilder(block: Builder.() -> Unit): ImageVector { + return ImageVector.Builder( + name = "Robohash", + defaultWidth = 300.dp, + defaultHeight = 300.dp, + viewportWidth = 300f, + viewportHeight = 300f + ).apply(block).build() +} +``` + +### Building Icons + +```kotlin +fun customIcon(fgColor: SolidColor, builder: Builder) { + builder.addPath(pathData1, fill = fgColor, stroke = Black, strokeLineWidth = 1.5f) + builder.addPath(pathData2, fill = Black, fillAlpha = 0.4f) + builder.addPath(pathData3, fill = Black, fillAlpha = 0.2f) +} + +private val pathData1 = PathData { + moveTo(144.5f, 87.5f) + reflectiveCurveToRelative(-51.0f, 3.0f, -53.0f, 55.0f) + lineToRelative(16.0f, 16.0f) + close() +} + +@Composable +fun CustomIcon() { + Image( + painter = rememberVectorPainter( + roboBuilder { + customIcon(SolidColor(Color.Blue), this) + } + ), + contentDescription = "Custom icon" + ) +} +``` + +**Why ImageVector?** +- Pure Kotlin, no XML +- Works on Android, Desktop, iOS +- GPU-accelerated +- Type-safe + +### Caching Pattern + +```kotlin +object CustomIcons { + private val cache = mutableMapOf() + + fun get(key: String): ImageVector { + return cache.getOrPut(key) { + buildIcon(key) + } + } +} + +@Composable +fun CachedIcon(key: String) { + Image(imageVector = CustomIcons.get(key), contentDescription = null) +} +``` + +For detailed icon patterns, see `references/icon-assets.md`. + +## Common Visual Patterns + +### State Visualization + +```kotlin +@Composable +fun DataScreen(uiState: UiState) { + when (uiState) { + is UiState.Loading -> LoadingState("Loading...") + is UiState.Empty -> EmptyState( + title = "No data", + onRefresh = { /* refresh */ } + ) + is UiState.Error -> ErrorState( + message = uiState.message, + onRetry = { /* retry */ } + ) + is UiState.Success -> ContentList(uiState.items) + } +} +``` + +**Components** (all in `commons/commonMain`): +- `LoadingState` - Progress indicator + message +- `EmptyState` - Empty message + optional refresh button +- `ErrorState` - Error message + optional retry button + +### Relay Status (Amethyst Pattern) + +```kotlin +@Composable +fun RelayStatusIndicator(connectedCount: Int) { + val statusColor = when { + connectedCount == 0 -> RelayStatusColors.Disconnected + connectedCount < 3 -> RelayStatusColors.Connecting + else -> RelayStatusColors.Connected + } + + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Icon( + imageVector = if (connectedCount > 0) Icons.Default.Check else Icons.Default.Close, + tint = statusColor, + modifier = Modifier.size(16.dp) + ) + Text( + "$connectedCount relay${if (connectedCount != 1) "s" else ""}", + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +} +``` + +**Visual mapping**: +- 0 relays → Red + X icon +- 1-2 relays → Yellow + Check icon +- 3+ relays → Green + Check icon + +### Placeholder Pattern + +```kotlin +@Composable +fun PlaceholderScreen( + title: String, + description: String, + modifier: Modifier = Modifier +) { + Column(modifier = modifier) { + Text(title, style = MaterialTheme.typography.headlineMedium) + Spacer(Modifier.height(16.dp)) + Text(description, color = MaterialTheme.colorScheme.onSurfaceVariant) + } +} + +// Specific implementations +@Composable +fun SearchPlaceholder() = PlaceholderScreen( + title = "Search", + description = "Search for users, notes, and hashtags." +) +``` + +**Pattern**: Generic composable + specific wrappers with preset text + +## Performance + +### Avoid Unnecessary Recomposition + +```kotlin +// ❌ Bad - recomposes on every scroll +@Composable +fun BadButton(scrollState: ScrollState) { + if (scrollState.value > 100) { + Button(onClick = {}) { Text("Top") } + } +} + +// ✅ Good - only recomposes when visibility changes +@Composable +fun GoodButton(scrollState: ScrollState) { + val show by remember { derivedStateOf { scrollState.value > 100 } } + if (show) { + Button(onClick = {}) { Text("Top") } + } +} +``` + +### Lazy Lists + +```kotlin +@Composable +fun FeedList(items: List) { + LazyColumn { + items(items, key = { it.id }) { item -> + FeedItem(item) + } + } +} +``` + +**Key principle**: Use `key` parameter for stable item identity + +## Bundled Resources + +- **references/shared-composables-catalog.md** - Complete catalog of shared UI components +- **references/state-patterns.md** - State management patterns with visual examples +- **references/icon-assets.md** - Custom ImageVector icon patterns +- **scripts/find-composables.sh** - Find all @Composable functions in codebase + +## Quick Reference + +| Task | Pattern | Location | +|------|---------|----------| +| Reusable UI | State hoisting | commons/commonMain | +| Simple state | remember { mutableStateOf() } | Composable scope | +| Derived state | derivedStateOf { } | remember block | +| Async → state | produceState { } | Composable function | +| Custom icons | roboBuilder + PathData | commons/icons | +| Loading/Error | LoadingState, ErrorState | commons/ui/components | +| Theme colors | MaterialTheme.colorScheme | Any @Composable | +| Navigation | Delegate to platform expert | amethyst/, desktopApp/ | + +## Common Workflows + +### Creating a Shared Component + +1. Start in `commons/src/commonMain/kotlin/.../ui/components/` +2. Use Material3 primitives only +3. Hoist state (parameters for data, callbacks for events) +4. Add modifier parameter +5. Use MaterialTheme for colors/typography +6. Test on both Android and Desktop + +### Converting Existing Component + +1. Read current implementation in `amethyst/` or `desktopApp/` +2. Identify pure visual logic (no platform APIs) +3. Create in `commons/commonMain` with hoisted state +4. Replace platform implementations with shared component +5. Keep platform-specific wrappers if needed + +### Custom Icon + +1. Export SVG from design tool +2. Convert to PathData using Android Studio +3. Create icon function with roboBuilder +4. Add caching if generated dynamically +5. Wrap in @Composable for easy use + +### Navigation (Delegate) + +For navigation patterns: +- Android bottom nav → `android-expert` +- Desktop sidebar → `desktop-expert` +- Multi-window → `desktop-expert` + +## Related Skills + +- **kotlin-expert** - Kotlin language aspects (@Immutable details, StateFlow, sealed classes) +- **android-expert** - Android navigation, platform APIs +- **desktop-expert** - Desktop navigation, window management, OS specifics +- **kotlin-coroutines** - Async patterns, Flow integration diff --git a/.claude/skills/compose-expert/references/icon-assets.md b/.claude/skills/compose-expert/references/icon-assets.md new file mode 100644 index 000000000..96b8fea26 --- /dev/null +++ b/.claude/skills/compose-expert/references/icon-assets.md @@ -0,0 +1,365 @@ +# Custom Icon Assets and ImageVector Patterns + +Guide to creating and using custom ImageVector icons in Compose Multiplatform. + +## Why ImageVector? + +ImageVector is the native Compose format for vector graphics: +- **Pure Kotlin**: No XML, no asset files +- **Multiplatform**: Works on Android, Desktop, iOS without conversion +- **Performant**: Lightweight, composable, GPU-accelerated +- **Type-safe**: Compile-time checking, no resource IDs + +## Amethyst Pattern: Robohash + +Amethyst generates deterministic avatars using ImageVector builders. + +### Architecture + +``` +commons/robohash/ +├── RobohashAssembler.kt # Main assembly logic +├── CachedRobohash.kt # Caching layer +└── parts/ + ├── Face0C3po.kt # Face variants (0-9) + ├── Eyes2Single.kt # Eye variants (0-9) + ├── Mouth3Grid.kt # Mouth variants (0-9) + ├── Body2Thinnest.kt # Body variants (0-9) + └── Accessory7Antenna.kt # Accessory variants (0-9) +``` + +**Pattern**: 10 variants per feature × 5 features = 100,000+ unique combinations + +### roboBuilder DSL + +Custom ImageVector builder with sensible defaults: + +```kotlin +fun roboBuilder(block: Builder.() -> Unit): ImageVector { + return ImageVector.Builder( + name = "Robohash", + defaultWidth = 300.dp, + defaultHeight = 300.dp, + viewportWidth = 300f, + viewportHeight = 300f + ).apply(block).build() +} +``` + +**Usage**: +```kotlin +@Composable +fun CustomIcon() { + Image( + painter = rememberVectorPainter( + roboBuilder { + // Add paths here + } + ), + contentDescription = "Custom icon" + ) +} +``` + +### Path Building Pattern + +```kotlin +fun face0C3po(fgColor: SolidColor, builder: Builder) { + builder.addPath(pathData1, fill = fgColor, stroke = Black, strokeLineWidth = 1.5f) + builder.addPath(pathData2, fill = Black, fillAlpha = 0.4f) + builder.addPath(pathData5, fill = Black, fillAlpha = 0.2f) + builder.addPath(pathData6, stroke = Black, strokeLineWidth = 1.0f) + builder.addPath(pathData7, fill = Black, stroke = Black, fillAlpha = 0.2f, strokeLineWidth = 0.75f) +} + +private val pathData1 = PathData { + moveTo(144.5f, 87.5f) + reflectiveCurveToRelative(-51.0f, 3.0f, -53.0f, 55.0f) + curveToRelative(0.0f, 0.0f, 0.0f, 27.0f, 5.0f, 42.0f) + reflectiveCurveToRelative(10.0f, 38.0f, 10.0f, 38.0f) + lineToRelative(16.0f, 16.0f) + // ... + close() +} +``` + +**Key elements**: +- `pathData` variables for path commands +- `addPath()` for each layer +- Parameterized colors (`fgColor`) +- Constant colors (`Black`) +- Alpha for shadows/highlights + +### PathData DSL + +Compose's PathData builder provides SVG-like commands: + +| Command | Description | Example | +|---------|-------------|---------| +| `moveTo(x, y)` | Move pen without drawing | `moveTo(100f, 100f)` | +| `lineTo(x, y)` | Draw line to point | `lineTo(200f, 150f)` | +| `curveToRelative(...)` | Relative cubic Bézier | `curveToRelative(10f, 20f, 30f, 40f, 50f, 60f)` | +| `reflectiveCurveToRelative(...)` | Smooth curve | `reflectiveCurveToRelative(-51f, 3f, -53f, 55f)` | +| `horizontalLineTo(x)` | Horizontal line | `horizontalLineTo(250f)` | +| `verticalLineTo(y)` | Vertical line | `verticalLineTo(300f)` | +| `close()` | Close path | `close()` | + +**Relative vs Absolute**: +- `moveTo` / `lineTo` - Absolute coordinates +- `moveToRelative` / `lineToRelative` - Relative to current position + +## Creating Custom Icons + +### Method 1: From SVG (Recommended) + +1. **Export SVG** from design tool (Figma, Illustrator) +2. **Convert to ImageVector** using Android Studio's Vector Asset tool +3. **Extract path data** and adapt to roboBuilder pattern + +```kotlin +// SVG path: M 10 10 L 20 20 ... +// Becomes: +private val myIconPath = PathData { + moveTo(10f, 10f) + lineTo(20f, 20f) + // ... +} +``` + +### Method 2: Programmatic + +Build paths programmatically for simple shapes: + +```kotlin +fun simpleIcon(): ImageVector = roboBuilder { + addPath( + pathData = PathData { + moveTo(50f, 50f) + lineTo(150f, 50f) + lineTo(150f, 150f) + lineTo(50f, 150f) + close() + }, + fill = SolidColor(Color.Blue), + stroke = SolidColor(Color.Black), + strokeLineWidth = 2f + ) +} +``` + +### Method 3: Material Icons Extensions + +Extend Material Icons when you need platform-consistent icons: + +```kotlin +// For standard icons, use Material Icons +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* + +Icon(Icons.Default.Refresh, contentDescription = "Refresh") +Icon(Icons.Default.Check, contentDescription = "Success") +Icon(Icons.Default.Close, contentDescription = "Error") +``` + +## CachedRobohash Pattern + +Performance optimization for generated icons: + +```kotlin +object CachedRobohash { + private val cache = mutableMapOf, ImageVector>() + + fun get(seed: String, isLight: Boolean): ImageVector { + return cache.getOrPut(seed to isLight) { + RobohashAssembler.assemble(seed, isLight) + } + } +} +``` + +**Pattern**: +- Key: `(seed, theme)` pair +- Value: Assembled ImageVector +- Lifecycle: Application lifetime (never cleared) + +**Usage**: +```kotlin +@Composable +fun RobohashImage(robot: String) { + Image( + imageVector = CachedRobohash.get(robot, isLightTheme()), + contentDescription = "Avatar for $robot" + ) +} +``` + +## Color Management + +### Dynamic Colors + +Pass colors as parameters for theme adaptation: + +```kotlin +fun themedIcon(fgColor: SolidColor, bgColor: SolidColor, builder: Builder) { + builder.addPath(pathData1, fill = bgColor) + builder.addPath(pathData2, fill = fgColor) +} + +@Composable +fun ThemedIcon() { + val fg = MaterialTheme.colorScheme.primary + val bg = MaterialTheme.colorScheme.surface + + Image( + painter = rememberVectorPainter( + roboBuilder { + themedIcon(SolidColor(fg), SolidColor(bg), this) + } + ), + contentDescription = null + ) +} +``` + +### Static Colors + +Define constants for colors that don't change: + +```kotlin +val Black = SolidColor(Color.Black) +val White = SolidColor(Color.White) +val Transparent = SolidColor(Color.Transparent) +``` + +## Advanced Techniques + +### Layering + +Build complex icons with multiple layers: + +```kotlin +fun complexIcon(builder: Builder) { + // Layer 1: Background + builder.addPath(bgPath, fill = SolidColor(Color.White)) + + // Layer 2: Shadow + builder.addPath(shadowPath, fill = SolidColor(Color.Black), fillAlpha = 0.2f) + + // Layer 3: Main shape + builder.addPath(mainPath, fill = SolidColor(Color.Blue)) + + // Layer 4: Highlight + builder.addPath(highlightPath, fill = SolidColor(Color.White), fillAlpha = 0.3f) + + // Layer 5: Stroke + builder.addPath(outlinePath, stroke = SolidColor(Color.Black), strokeLineWidth = 1f) +} +``` + +**Render order**: Bottom to top (first addPath = bottom layer) + +### Alpha for Visual Effects + +```kotlin +// Shadow +builder.addPath(shadowPath, fill = Black, fillAlpha = 0.4f) + +// Highlight +builder.addPath(highlightPath, fill = White, fillAlpha = 0.2f) + +// Glass effect +builder.addPath(glassPath, fill = White, fillAlpha = 0.1f) +``` + +### Stroke Styles + +```kotlin +// Outline only +builder.addPath(path, stroke = Black, strokeLineWidth = 1.5f) + +// Fill + outline +builder.addPath(path, fill = fgColor, stroke = Black, strokeLineWidth = 1f) + +// Dashed (not supported directly, use multiple segments) +``` + +## Composable Icon Pattern + +Wrap ImageVector in a Composable for reusability: + +```kotlin +@Composable +fun MyCustomIcon( + modifier: Modifier = Modifier, + tint: Color = Color.Unspecified +) { + Image( + painter = rememberVectorPainter(myIconVector()), + contentDescription = "My custom icon", + modifier = modifier, + colorFilter = if (tint != Color.Unspecified) { + ColorFilter.tint(tint) + } else null + ) +} +``` + +**Usage**: +```kotlin +MyCustomIcon( + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.primary +) +``` + +## Best Practices + +### DO +✅ Cache generated ImageVectors for performance +✅ Use PathData DSL for readability +✅ Parameterize colors for theme support +✅ Use Material Icons for standard icons +✅ Keep viewport size consistent (e.g., 300×300) +✅ Layer paths from back to front +✅ Use alpha for shadows and highlights + +### DON'T +❌ Generate ImageVectors in @Composable without caching +❌ Hardcode theme-specific colors +❌ Create custom icons for standard Material icons +❌ Use extreme viewport sizes (stay 24-1000dp) +❌ Mix absolute and relative coordinates unnecessarily +❌ Forget to close() paths + +## Icon Organization + +### Structure +``` +commons/icons/ +├── CustomIcons.kt # Icon collection object +├── icons/ +│ ├── Zap.kt # Lightning bolt +│ ├── Relay.kt # Relay indicator +│ └── Bitcoin.kt # Bitcoin symbol +└── builders/ + └── IconBuilder.kt # Shared builder utilities +``` + +### Collection Object +```kotlin +object CustomIcons { + val Zap: ImageVector by lazy { ZapIcon.create() } + val Relay: ImageVector by lazy { RelayIcon.create() } + val Bitcoin: ImageVector by lazy { BitcoinIcon.create() } +} + +// Usage +Icon(CustomIcons.Zap, contentDescription = "Zap") +``` + +## Resources + +- [Compose ImageVector API](https://developer.android.com/reference/kotlin/androidx/compose/ui/graphics/vector/ImageVector) +- [SVG Path Commands](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths) +- [Material Icons](https://fonts.google.com/icons) +- Robohash implementation: `commons/robohash/` in AmethystMultiplatform diff --git a/.claude/skills/compose-expert/references/shared-composables-catalog.md b/.claude/skills/compose-expert/references/shared-composables-catalog.md new file mode 100644 index 000000000..19f7938fd --- /dev/null +++ b/.claude/skills/compose-expert/references/shared-composables-catalog.md @@ -0,0 +1,281 @@ +# Shared Composables Catalog + +This catalog documents shared UI components in `commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/`. + +## Directory Structure + +``` +commons/src/commonMain/kotlin/.../commons/ui/ +├── components/ # Reusable UI components +├── screens/ # Screen-level composables +├── theme/ # Theming and styling +└── feed/ # Feed-specific components +``` + +## Components (`ui/components/`) + +### State Visualization + +**LoadingState** - Centered loading indicator with message +```kotlin +@Composable +fun LoadingState(message: String, modifier: Modifier = Modifier) +``` +- Use for: Async operations, data fetching +- Pattern: fillMaxSize, centered Column, CircularProgressIndicator +- Works on: Android, Desktop + +**EmptyState** - Centered empty state with optional refresh +```kotlin +@Composable +fun EmptyState( + title: String, + modifier: Modifier = Modifier, + description: String? = null, + onRefresh: (() -> Unit)? = null, + refreshLabel: String = "Refresh" +) +``` +- Use for: Empty lists, no data scenarios +- Pattern: Centered Column, optional OutlinedButton +- Works on: Android, Desktop + +**ErrorState** - Centered error message with retry +```kotlin +@Composable +fun ErrorState( + message: String, + modifier: Modifier = Modifier, + onRetry: (() -> Unit)? = null, + retryLabel: String = "Try Again" +) +``` +- Use for: Error handling, failed operations +- Pattern: error color, optional Button +- Works on: Android, Desktop + +### Feed-Specific States + +**FeedEmptyState** - Pre-configured empty state for feeds +```kotlin +@Composable +fun FeedEmptyState( + modifier: Modifier = Modifier, + title: String = "Feed is empty", + onRefresh: (() -> Unit)? = null +) +``` + +**FeedErrorState** - Pre-configured error state for feeds +```kotlin +@Composable +fun FeedErrorState( + errorMessage: String, + modifier: Modifier = Modifier, + onRetry: (() -> Unit)? = null +) +``` + +### Action Buttons + +**Shared Constants**: +```kotlin +val ActionButtonShape = RoundedCornerShape(20.dp) +val ActionButtonPadding = PaddingValues(vertical = 0.dp, horizontal = 16.dp) +``` + +**AddButton** - Consistent "Add" action button +```kotlin +@Composable +fun AddButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + text: String = "Add", + enabled: Boolean = true +) +``` +- Pattern: OutlinedButton with consistent shape/padding +- Works on: Android, Desktop + +**RemoveButton** - Consistent "Remove" action button +```kotlin +@Composable +fun RemoveButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + text: String = "Remove", + enabled: Boolean = true +) +``` + +### Custom Images + +**RobohashImage** - Deterministic avatar generation +```kotlin +@Composable +fun RobohashImage( + robot: String, // Seed (e.g., pubkey) + modifier: Modifier = Modifier, + contentDescription: String? = null, + loadRobohash: Boolean = true +) + +// Overload with more options +@Composable +fun RobohashImage( + robot: String, + modifier: Modifier = Modifier, + contentDescription: String? = null, + alignment: Alignment = Alignment.Center, + contentScale: ContentScale = ContentScale.Fit, + colorFilter: ColorFilter? = null, + loadRobohash: Boolean = true +) +``` +- Use for: User avatars, deterministic graphics +- Pattern: Uses CachedRobohash.get(), isLightTheme() detection +- Fallback: Icons.Default.Face +- Works on: Android, Desktop (pure ImageVector) + +**Theme Detection Helper**: +```kotlin +@Composable +private fun isLightTheme(): Boolean { + val background = MaterialTheme.colorScheme.background + return (background.red + background.green + background.blue) / 3 > 0.5f +} +``` + +## Feed Components (`ui/feed/`) + +### FeedHeader + +**FeedHeader** - Screen header with title and relay status +```kotlin +@Composable +fun FeedHeader( + title: String, + connectedRelayCount: Int, + onRefresh: () -> Unit, + modifier: Modifier = Modifier +) +``` +- Pattern: Row with SpaceBetween, title + RelayStatusIndicator +- Works on: Android, Desktop + +**RelayStatusIndicator** - Compact relay connection indicator +```kotlin +@Composable +fun RelayStatusIndicator( + connectedCount: Int, + onRefresh: () -> Unit, + modifier: Modifier = Modifier +) +``` +- Pattern: Status icon + count text + refresh button +- Colors: RelayStatusColors.{Disconnected, Connecting, Connected} +- Visual cues: Check icon (connected), Close icon (disconnected) + +## Screens (`ui/screens/`) + +### Placeholder Pattern + +**PlaceholderScreen** - Generic placeholder +```kotlin +@Composable +fun PlaceholderScreen( + title: String, + description: String, + modifier: Modifier = Modifier +) +``` +- Pattern: Column with title (headlineMedium) + description +- Use for: Unimplemented screens, coming soon features + +**Specific Placeholders**: +- `SearchPlaceholder()` - Search screen +- `MessagesPlaceholder()` - DMs screen +- `NotificationsPlaceholder()` - Notifications screen + +Pattern: Specific implementations wrap PlaceholderScreen with preset text. + +## Custom Icons (`robohash/parts/`) + +### ImageVector Builder Pattern + +Amethyst uses a custom DSL for building ImageVector assets: + +```kotlin +@Composable +fun Face0C3po() { + Image( + painter = rememberVectorPainter( + roboBuilder { + face0C3po(SolidColor(Color.Blue), this) + } + ), + contentDescription = "" + ) +} + +fun face0C3po(fgColor: SolidColor, builder: Builder) { + builder.addPath(pathData1, fill = fgColor, stroke = Black, strokeLineWidth = 1.5f) + builder.addPath(pathData2, fill = Black, fillAlpha = 0.4f) + // ... +} + +private val pathData1 = PathData { + moveTo(144.5f, 87.5f) + reflectiveCurveToRelative(-51.0f, 3.0f, -53.0f, 55.0f) + // ... path commands +} +``` + +**roboBuilder** - Custom ImageVector.Builder DSL +- Located in: `commons/robohash/` +- Pattern: Builder-based, composable paths +- Parts: Face, Eyes, Mouth, Body, Accessory (0-9 variants each) +- Colors: Dynamic (fgColor parameter) + Black constants + +### CachedRobohash + +```kotlin +CachedRobohash.get(seed: String, isLight: Boolean): ImageVector +``` +- Deterministic: Same seed → same avatar +- Theme-aware: Different colors for light/dark +- Cached: Performance optimization +- Pure ImageVector: Works on all platforms + +## Sharing Guidelines + +### Always Share +- State visualization (Loading, Empty, Error) +- Action buttons with consistent styling +- Generic placeholders +- Custom ImageVector icons +- Material3 themed components +- Theme utilities (isLightTheme) + +### Platform-Specific (Delegate to Experts) +- Navigation structure (android-expert, desktop-expert) +- Screen layouts and scaffolds +- Platform system integrations +- Gesture handling specifics + +### Decision Framework +1. **Can it use Material3 primitives?** → Share +2. **Does it need platform system APIs?** → Platform-specific +3. **Is it a visual component without navigation?** → Share +4. **Does it require platform UX patterns?** → Ask platform expert + +## Material3 Usage + +All shared composables use Material3: +- `MaterialTheme.colorScheme.*` for colors +- `MaterialTheme.typography.*` for text styles +- `OutlinedButton`, `Button`, `IconButton` for actions +- `CircularProgressIndicator` for loading +- `Icon`, `Image` for visuals + +This ensures consistent theming across Android and Desktop. diff --git a/.claude/skills/compose-expert/references/state-patterns.md b/.claude/skills/compose-expert/references/state-patterns.md new file mode 100644 index 000000000..3015f766b --- /dev/null +++ b/.claude/skills/compose-expert/references/state-patterns.md @@ -0,0 +1,334 @@ +# Compose State Management Patterns + +Visual guide to state management in Compose Multiplatform. For Kotlin-specific patterns (StateFlow, sealed classes), see `kotlin-expert` skill. + +## Core State Functions + +### remember + +Cache values across recompositions: + +```kotlin +@Composable +fun Counter() { + var count by remember { mutableStateOf(0) } + + Button(onClick = { count++ }) { + Text("Clicked $count times") + } +} +``` + +**When to use**: Simple UI state (toggles, counters, text input) +**Visual pattern**: Button press → state changes → UI updates + +### derivedStateOf + +Compute state from other state, recompose only when result changes: + +```kotlin +@Composable +fun ScrollToTopButton(listState: LazyListState) { + // Only recomposes when showButton value changes (not every scroll pixel) + val showButton by remember { + derivedStateOf { + listState.firstVisibleItemIndex > 0 + } + } + + if (showButton) { + FloatingActionButton(onClick = { /* scroll to top */ }) { + Icon(Icons.Default.ArrowUpward, null) + } + } +} +``` + +**When to use**: Input state changes frequently, but derived result changes rarely +**Visual pattern**: Scroll position (0, 1, 2...) → boolean (show/hide) → FAB visibility +**Performance**: Prevents recomposition on every scroll event + +### produceState + +Convert non-Compose state into Compose state: + +```kotlin +@Composable +fun LoadUserProfile(userId: String): State { + return produceState(initialValue = null, userId) { + value = repository.fetchUser(userId) + } +} + +@Composable +fun ProfileScreen(userId: String) { + val user by LoadUserProfile(userId) + + when (user) { + null -> LoadingState("Loading profile...") + else -> ProfileCard(user!!) + } +} +``` + +**When to use**: Convert Flow, LiveData, callbacks into Compose state +**Visual pattern**: Async operation → state updates → UI reflects changes +**Lifecycle**: Coroutine cancelled when composable leaves composition + +## State Hoisting Pattern + +Move state up to make composables reusable and testable: + +### Before (Stateful) +```kotlin +@Composable +fun SearchBar() { + var query by remember { mutableStateOf("") } + + TextField( + value = query, + onValueChange = { query = it }, + placeholder = { Text("Search...") } + ) +} +``` +❌ Hard to test, can't control state externally + +### After (Stateless) +```kotlin +@Composable +fun SearchBar( + query: String, + onQueryChange: (String) -> Unit, + modifier: Modifier = Modifier +) { + TextField( + value = query, + onValueChange = onQueryChange, + placeholder = { Text("Search...") }, + modifier = modifier + ) +} + +@Composable +fun SearchScreen() { + var query by remember { mutableStateOf("") } + + Column { + SearchBar(query = query, onQueryChange = { query = it }) + SearchResults(query = query) + } +} +``` +✅ Reusable, testable, state controlled by parent + +**Hoisting principle**: State goes up, events go down +- State: `query: String` (read-only) +- Events: `onQueryChange: (String) -> Unit` (write-only) + +## Amethyst State Patterns + +### Theme-Aware State + +```kotlin +@Composable +private fun isLightTheme(): Boolean { + val background = MaterialTheme.colorScheme.background + return (background.red + background.green + background.blue) / 3 > 0.5f +} + +@Composable +fun ThemedContent() { + val isDark = !isLightTheme() + // Adjust visuals based on theme + val iconTint = if (isDark) Color.White else Color.Black +} +``` + +**Pattern**: Derive state from MaterialTheme +**Visual**: Component adapts to light/dark theme automatically + +### Relay Status State + +```kotlin +@Composable +fun RelayStatusIndicator( + connectedCount: Int, + onRefresh: () -> Unit, + modifier: Modifier = Modifier +) { + val statusColor = when { + connectedCount == 0 -> RelayStatusColors.Disconnected + connectedCount < 3 -> RelayStatusColors.Connecting + else -> RelayStatusColors.Connected + } + + Icon( + imageVector = if (connectedCount > 0) Icons.Default.Check else Icons.Default.Close, + tint = statusColor + ) +} +``` + +**Pattern**: Visual state derived from domain state +**Visual mapping**: +- 0 relays → Red + X icon +- 1-2 relays → Yellow + Check icon +- 3+ relays → Green + Check icon + +### Loading/Empty/Error States + +```kotlin +@Composable +fun FeedScreen(viewModel: FeedViewModel) { + val uiState by viewModel.uiState.collectAsState() + + when (uiState) { + is UiState.Loading -> LoadingState("Loading feed...") + is UiState.Empty -> FeedEmptyState(onRefresh = { viewModel.refresh() }) + is UiState.Error -> FeedErrorState( + errorMessage = uiState.message, + onRetry = { viewModel.retry() } + ) + is UiState.Success -> LazyColumn { + items(uiState.items) { FeedItem(it) } + } + } +} +``` + +**Pattern**: Sealed class → visual state component +**Components**: +- `LoadingState` - Progress indicator +- `EmptyState` - Empty message + refresh +- `ErrorState` - Error message + retry +- Success - Actual content + +## Common Patterns + +### Toggle State +```kotlin +var isExpanded by remember { mutableStateOf(false) } + +IconButton(onClick = { isExpanded = !isExpanded }) { + Icon( + if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore, + contentDescription = if (isExpanded) "Collapse" else "Expand" + ) +} + +if (isExpanded) { + Text("Expanded content...") +} +``` + +### List State with Actions +```kotlin +var items by remember { mutableStateOf(listOf("Item 1", "Item 2")) } + +Column { + AddButton(onClick = { + items = items + "Item ${items.size + 1}" + }) + + items.forEachIndexed { index, item -> + Row { + Text(item) + RemoveButton(onClick = { + items = items.filterIndexed { i, _ -> i != index } + }) + } + } +} +``` + +### TextField State +```kotlin +var text by remember { mutableStateOf("") } + +TextField( + value = text, + onValueChange = { text = it }, + label = { Text("Enter text") } +) +``` + +## Performance Patterns + +### Avoid Unnecessary Recomposition +```kotlin +// ❌ Bad: Recomposes on every scroll position change +@Composable +fun BadScrollButton(scrollState: ScrollState) { + if (scrollState.value > 100) { // scrollState.value changes constantly + Button(onClick = { /* ... */ }) { Text("Scroll to Top") } + } +} + +// ✅ Good: Only recomposes when visibility changes +@Composable +fun GoodScrollButton(scrollState: ScrollState) { + val showButton by remember { + derivedStateOf { scrollState.value > 100 } + } + + if (showButton) { + Button(onClick = { /* ... */ }) { Text("Scroll to Top") } + } +} +``` + +### Stable Parameters +Use `@Immutable` data classes (see `kotlin-expert`) to prevent recomposition: + +```kotlin +@Immutable +data class UserProfile(val name: String, val avatar: String) + +@Composable +fun ProfileCard(profile: UserProfile) { + // Only recomposes when profile instance changes + Row { + RobohashImage(robot = profile.avatar) + Text(profile.name) + } +} +``` + +## Integration with Kotlin State + +For ViewModel state, Flow, StateFlow → See `kotlin-expert` skill + +Common integration pattern: +```kotlin +// ViewModel (Kotlin state) +class FeedViewModel { + private val _uiState = MutableStateFlow(UiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() +} + +// Composable (Compose state) +@Composable +fun FeedScreen(viewModel: FeedViewModel) { + val uiState by viewModel.uiState.collectAsState() + // Use uiState to render UI +} +``` + +## Quick Reference + +| Function | Use Case | Recomposes When | +|----------|----------|----------------| +| `remember { mutableStateOf() }` | Local UI state | State value changes | +| `derivedStateOf { }` | Computed state | Derived result changes | +| `produceState { }` | Async/Flow → State | Async operation updates value | +| `collectAsState()` | Flow → State | Flow emits new value | +| State hoisting | Reusable components | Parent passes new state | + +## Sources + +State management patterns based on: +- [State and Jetpack Compose - Android Developers](https://developer.android.com/develop/ui/compose/state) +- [When should I use derivedStateOf?](https://medium.com/androiddevelopers/jetpack-compose-when-should-i-use-derivedstateof-63ce7954c11b) +- [Advanced State and Side Effects](https://developer.android.com/codelabs/jetpack-compose-advanced-state-side-effects) +- AmethystMultiplatform codebase patterns (2025) diff --git a/.claude/skills/compose-expert/scripts/find-composables.sh b/.claude/skills/compose-expert/scripts/find-composables.sh new file mode 100755 index 000000000..436a3cefe --- /dev/null +++ b/.claude/skills/compose-expert/scripts/find-composables.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Find all @Composable functions in the codebase + +set -e + +# Default to current directory if no path provided +SEARCH_PATH="${1:-.}" + +echo "Searching for @Composable functions in: $SEARCH_PATH" +echo "================================================" +echo "" + +# Find all @Composable functions with file paths and line numbers +grep -r -n "@Composable" "$SEARCH_PATH" \ + --include="*.kt" \ + --exclude-dir=build \ + --exclude-dir=.gradle \ + | while IFS=: read -r file line content; do + # Extract function name if possible + if [[ $content =~ fun[[:space:]]+([a-zA-Z0-9_]+) ]]; then + func_name="${BASH_REMATCH[1]}" + echo "$file:$line - $func_name" + else + echo "$file:$line" + fi + done + +echo "" +echo "Total @Composable functions found:" +grep -r "@Composable" "$SEARCH_PATH" \ + --include="*.kt" \ + --exclude-dir=build \ + --exclude-dir=.gradle \ + | wc -l diff --git a/.claude/skills/gradle-expert/SKILL.md b/.claude/skills/gradle-expert/SKILL.md new file mode 100644 index 000000000..208caa11c --- /dev/null +++ b/.claude/skills/gradle-expert/SKILL.md @@ -0,0 +1,548 @@ +--- +name: gradle-expert +description: Build optimization, dependency resolution, and multi-module KMP troubleshooting for AmethystMultiplatform. Use when working with: (1) Gradle build files (build.gradle.kts, settings.gradle), (2) Version catalog (libs.versions.toml), (3) Build errors and dependency conflicts, (4) Module dependencies and source sets, (5) Desktop packaging (DMG/MSI/DEB), (6) Build performance optimization, (7) Proguard/R8 configuration, (8) Common KMP + Android Gradle issues (Compose conflicts, secp256k1 JNI variants, source set problems). +--- + +# Gradle Expert + +Build system expertise for AmethystMultiplatform's 4-module KMP architecture. Focus: practical troubleshooting, dependency resolution, and project-specific optimizations. + +## Build Architecture Mental Model + +Think of this project as **4 layers**: + +``` +┌─────────────┬─────────────┐ +│ :amethyst │ :desktopApp │ ← Platform apps (navigation, layouts) +│ (Android) │ (JVM) │ +└──────┬──────┴──────┬──────┘ + │ │ + └──────┬──────┘ + ▼ + ┌─────────────┐ + │ :commons │ ← Shared UI (KMP with jvmAndroid) + │ (KMP UI) │ + └──────┬──────┘ + ▼ + ┌─────────────┐ + │ :quartz │ ← Core library (KMP: Android/JVM/iOS) + │(KMP Library)│ + └─────────────┘ +``` + +**Key insight:** Dependencies flow DOWN. Lower modules never depend on upper modules. This enables code sharing without circular dependencies. + +**The jvmAndroid pattern:** Unique to this project. A custom source set between commonMain and {androidMain, jvmMain} for JVM-specific code shared by Android and Desktop. Not standard KMP, but critical for this architecture. + +## Version Catalog Philosophy + +All dependencies centralized in `gradle/libs.versions.toml`. Think "single source of truth." + +**Pattern:** +```toml +[versions] +kotlin = "2.3.0" + +[libraries] +okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } + +[plugins] +kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +``` + +**Usage:** +```kotlin +dependencies { + implementation(libs.okhttp) // Type-safe, IDE-autocompleted +} +``` + +**Critical alignments:** +- **Kotlin ecosystem:** All Kotlin plugins MUST share same version +- **Compose ecosystem:** Compose Multiplatform version → Kotlin version (check compatibility matrix) +- **secp256k1 variants:** All three variants (common, jni-android, jni-jvm) MUST share same version + +See [references/version-catalog-guide.md](references/version-catalog-guide.md) for comprehensive patterns. + +## Common Build Tasks + +### Quick Reference + +```bash +# Full builds +./gradlew build # All modules +./gradlew clean build # Clean build + +# Desktop +./gradlew :desktopApp:run # Run desktop app +./gradlew :desktopApp:packageDmg # macOS package + +# Module-specific +./gradlew :quartz:build # KMP library only +./gradlew :commons:build # Shared UI only + +# Analysis +./gradlew dependencies # Dependency tree +./gradlew build --scan # Online diagnostics +``` + +See [references/build-commands.md](references/build-commands.md) for comprehensive command reference. + +## Module Structure & Dependencies + +### Dependency Flow + +**Desktop build chain:** +``` +:desktopApp → :commons (jvmMain) → :quartz (jvmMain → jvmAndroid → commonMain) +``` + +**Android build chain:** +``` +:amethyst → :commons (androidMain) → :quartz (androidMain → jvmAndroid → commonMain) +``` + +**Key source set pattern (quartz & commons):** +``` +commonMain # Truly cross-platform code + │ + ├─ jvmAndroid # JVM-specific, shared by Android + Desktop + │ ├─ androidMain + │ └─ jvmMain + │ + └─ iosMain # iOS-specific (quartz only) +``` + +**Dependency config types:** +- Use `api` when types appear in module's public API or expect/actual declarations +- Use `implementation` for internal implementation details +- Example: quartz exposes secp256k1 (`api`), but hides okhttp (`implementation`) + +See [references/dependency-graph.md](references/dependency-graph.md) for module visualization and transitive dependency flow. + +## Critical Dependency Patterns + +### 1. secp256k1 (Crypto Library) + +**The problem:** KMP library with platform-specific JNI bindings. Wrong variant = runtime crash. + +**Pattern:** +```kotlin +// commonMain - API only +api(libs.secp256k1.kmp.common) + +// androidMain - Android JNI +api(libs.secp256k1.kmp.jni.android) + +// jvmMain - Desktop JVM JNI +implementation(libs.secp256k1.kmp.jni.jvm) +``` + +**Why api in androidMain?** Types leak to consumers (:amethyst). + +**Common error:** Desktop using jni-android variant → `UnsatisfiedLinkError: no secp256k1jni in java.library.path` + +**Fix:** Check source set dependencies. jvmMain must use jni-jvm, never jni-android. + +### 2. JNA (for LibSodium Encryption) + +**The problem:** Android needs AAR packaging, JVM needs JAR. Same library, different artifact types. + +**Pattern:** +```kotlin +// androidMain +implementation("com.goterl:lazysodium-android:5.2.0@aar") // @aar explicit +implementation("net.java.dev.jna:jna:5.18.1@aar") + +// jvmMain +implementation(libs.lazysodium.java) // JAR implicit +implementation(libs.jna) +``` + +**Critical:** Never put JNA in jvmAndroid or commonMain. Platform-specific packaging only. + +### 3. Compose Versions + +**The problem:** Two Compose ecosystems (Multiplatform + AndroidX) must align, or duplicate classes. + +**Current project config:** +```toml +composeMultiplatform = "1.9.3" # Plugin + runtime +composeBom = "2025.12.01" # AndroidX Compose BOM +kotlin = "2.3.0" +``` + +**Rule:** Compose Multiplatform version must be compatible with Kotlin version. Check: https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-compatibility-and-versioning.html + +**In KMP modules (quartz, commons):** +```kotlin +// ✅ Use Compose Multiplatform +implementation(compose.ui) +implementation(compose.material3) + +// ❌ DON'T use AndroidX BOM in KMP modules +// implementation(libs.androidx.compose.bom) +``` + +**In Android-only modules (amethyst):** +```kotlin +// Can use AndroidX BOM +val composeBom = platform(libs.androidx.compose.bom) +implementation(composeBom) +``` + +## Desktop Packaging Basics + +**TargetFormat options:** +```kotlin +// In desktopApp/build.gradle.kts +nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + + packageName = "Amethyst" + packageVersion = "1.0.0" + + macOS { + bundleID = "com.vitorpamplona.amethyst.desktop" + iconFile.set(project.file("src/jvmMain/resources/icon.icns")) + } +} +``` + +**Package tasks:** +```bash +./gradlew :desktopApp:packageDmg # macOS +./gradlew :desktopApp:packageMsi # Windows +./gradlew :desktopApp:packageDeb # Linux +``` + +**Output locations:** +- macOS: `desktopApp/build/compose/binaries/main/dmg/` +- Windows: `desktopApp/build/compose/binaries/main/msi/` +- Linux: `desktopApp/build/compose/binaries/main/deb/` + +**Icon requirements:** +- macOS: `.icns` (multi-resolution: 512, 256, 128, 32) +- Windows: `.ico` (256, 128, 64, 32, 16) +- Linux: `.png` (512x512) + +**Common issues:** +- Main class not found → Verify `mainClass = "...MainKt"` (Kotlin adds `Kt` suffix) +- Native libs missing → Ensure secp256k1-kmp-jni-jvm in dependencies +- Icon not found → Check file exists at path, use absolute path if needed + +## Build Performance Optimization + +**Add to `gradle.properties`:** +```properties +# Daemon (faster subsequent builds) +org.gradle.daemon=true +org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g + +# Parallel execution (multi-module speedup) +org.gradle.parallel=true +org.gradle.workers.max=8 + +# Caching (incremental builds) +org.gradle.caching=true +org.gradle.configuration-cache=true + +# Kotlin daemon +kotlin.incremental=true +kotlin.daemon.jvmargs=-Xmx2g +``` + +**Impact:** Typically 30-50% faster builds after first run. + +**Measure impact:** +```bash +./gradlew clean build --profile +# Report: build/reports/profile/profile-.html +``` + +**When to clean build:** +- After changing version catalog +- After adding/removing source sets +- When seeing unexplained errors + +**When NOT to clean:** +- Regular development iteration +- Small code changes +- Incremental compilation works fine + +Use script: `scripts/analyze-build-time.sh` for automated profiling. + +## Troubleshooting: Practical Patterns + +### Pattern 1: Version Conflict + +**Symptom:** `Duplicate class` or `NoSuchMethodError` + +**Diagnosis:** +```bash +./gradlew dependencyInsight --dependency +``` + +**Fix options:** +1. Align versions in libs.versions.toml (preferred) +2. Force resolution: +```kotlin +configurations.all { + resolutionStrategy { + force(libs.okhttp.get().toString()) + } +} +``` + +### Pattern 2: Source Set Issues + +**Symptom:** `Unresolved reference` to JVM library in shared code + +**Diagnosis:** Check source set hierarchy. JVM-only libs (jackson, okhttp) can't be in commonMain. + +**Fix:** Move to jvmAndroid or platform-specific source set. + +```kotlin +// ❌ Wrong +commonMain { + dependencies { + implementation(libs.jackson.module.kotlin) // JVM-only! + } +} + +// ✅ Correct +val jvmAndroid = create("jvmAndroid") { + dependsOn(commonMain.get()) + dependencies { + api(libs.jackson.module.kotlin) // JVM code, shared by Android + Desktop + } +} +``` + +### Pattern 3: Proguard Stripping Native Libs + +**Symptom:** `NoClassDefFoundError` for secp256k1, JNA, or LibSodium in release builds + +**Fix:** Update proguard rules in `quartz/proguard-rules.pro`: +```proguard +# Native libraries +-keep class fr.acinq.secp256k1.** { *; } +-keep class com.goterl.lazysodium.** { *; } +-keep class com.sun.jna.** { *; } + +# Jackson (reflection-based) +-keep class com.vitorpamplona.quartz.** { *; } +-keepattributes *Annotation* +-keepattributes Signature +``` + +### Pattern 4: Compose Compiler Mismatch + +**Symptom:** `IllegalStateException: Version mismatch: runtime 1.10.0 but compiler 1.9.0` + +**Fix:** Update Compose Multiplatform version in libs.versions.toml to match Kotlin version compatibility. + +Check: https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-compatibility-and-versioning.html + +### Pattern 5: Wrong JVM Target + +**Symptom:** `Unsupported class file major version 65` + +**Fix:** Ensure Java 21 everywhere: +```bash +# Check current Java +java -version # Should show 21 + +# Set JAVA_HOME +export JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home + +# Stop Gradle daemon to pick up new Java +./gradlew --stop +``` + +Verify all build files use JVM 21: +```kotlin +kotlin { + jvm { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_21) + } + } +} + +android { + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } +} +``` + +## Comprehensive Error Guide + +For detailed troubleshooting of specific errors, see [references/common-errors.md](references/common-errors.md). Covers: +- Compose version conflicts +- secp256k1 JNI errors +- Source set dependency issues +- Proguard/R8 problems +- Desktop packaging errors +- Kotlin compilation errors +- Dependency resolution failures +- JVM/JDK version issues + +Each error includes: symptom, cause, solution, verification steps. + +## Quick Diagnostic Commands + +```bash +# Check dependencies for specific module +./gradlew :quartz:dependencies + +# Find specific library in dependency tree +./gradlew dependencyInsight --dependency okhttp + +# Build with detailed logging +./gradlew build --info + +# Generate interactive build scan (best diagnostics) +./gradlew build --scan + +# Profile build performance +./gradlew clean build --profile + +# Stop all Gradle daemons (fresh start) +./gradlew --stop + +# Check Gradle version +./gradlew --version +``` + +## Scripts & References + +### Diagnostic Scripts +- `scripts/analyze-build-time.sh` - Profile build performance, generate optimization report +- `scripts/fix-dependency-conflicts.sh` - Diagnose common dependency conflicts, suggest fixes + +### Reference Docs +- `references/build-commands.md` - Comprehensive command reference for all tasks +- `references/dependency-graph.md` - Module dependencies, source set hierarchy, transitive deps +- `references/version-catalog-guide.md` - Version catalog patterns, usage, best practices +- `references/common-errors.md` - Troubleshooting guide for frequent build issues + +## Workflow Examples + +### Example 1: Adding New Dependency + +**Task:** Add kotlinx.datetime to quartz + +**Steps:** +1. **Update version catalog** (gradle/libs.versions.toml): +```toml +[versions] +kotlinxDatetime = "0.6.0" + +[libraries] +kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinxDatetime" } +``` + +2. **Add to build file** (quartz/build.gradle.kts): +```kotlin +sourceSets { + commonMain { + dependencies { + implementation(libs.kotlinx.datetime) // KMP library, goes in commonMain + } + } +} +``` + +3. **Sync & verify:** +```bash +./gradlew :quartz:dependencies | grep datetime +``` + +### Example 2: Fixing secp256k1 Error on Desktop + +**Error:** `UnsatisfiedLinkError: no secp256k1jni in java.library.path` when running desktop app + +**Diagnosis:** +```bash +./gradlew :desktopApp:dependencies --configuration runtimeClasspath | grep secp256k1 +# Shows: secp256k1-kmp-jni-android ← WRONG! +``` + +**Fix:** +```kotlin +// In quartz/build.gradle.kts +jvmMain { + dependencies { + // Change from: + // implementation(libs.secp256k1.kmp.jni.android) ❌ + + // To: + implementation(libs.secp256k1.kmp.jni.jvm) // ✅ + } +} +``` + +**Verify:** +```bash +./gradlew :desktopApp:dependencies --configuration runtimeClasspath | grep secp256k1 +# Now shows: secp256k1-kmp-jni-jvm ✅ + +./gradlew :desktopApp:run # Should work +``` + +### Example 3: Optimizing Build Time + +**Current:** Clean build takes 5 minutes + +**Steps:** +1. **Baseline measurement:** +```bash +./gradlew clean build --profile +# Check: build/reports/profile/profile-*.html +``` + +2. **Add optimizations** to gradle.properties: +```properties +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configuration-cache=true +org.gradle.jvmargs=-Xmx4g +kotlin.incremental=true +``` + +3. **Re-measure:** +```bash +./gradlew clean build --profile +``` + +**Expected improvement:** 30-50% faster on subsequent builds (incremental builds much faster). + +## Delegation Patterns + +**When to delegate to other skills:** +- **Source set architecture** (jvmAndroid pattern, expect/actual) → Use `kotlin-multiplatform` skill +- **Compose UI issues** (composables, state management) → Use `compose-expert` skill (when available) +- **Kotlin language issues** (Flow, sealed classes, DSLs) → Use `kotlin-expert` skill +- **Desktop-specific features** (Window management, MenuBar, tray) → Use `desktop-expert` skill + +**This skill handles:** Build system, dependencies, versioning, module structure, packaging, performance. + +## Core Principles for This Build System + +1. **Centralize versions:** Never hardcode versions in build.gradle.kts. Always use libs.versions.toml. + +2. **Respect source set hierarchy:** Dependencies flow downward. jvmAndroid depends on commonMain, never the reverse. + +3. **Platform-specific variants matter:** secp256k1, JNA must use correct variant per platform. Check when errors occur. + +4. **Clean builds are expensive:** Use incremental compilation. Only clean when truly needed (source set changes, version updates). + +5. **Compose alignment is critical:** Compose Multiplatform version must match Kotlin version. Check compatibility matrix. + +6. **Proguard for native libs:** All JNI libraries need explicit `-keep` rules in release builds. + +7. **Java 21 everywhere:** All modules, all targets, consistent JVM version. diff --git a/.claude/skills/gradle-expert/references/build-commands.md b/.claude/skills/gradle-expert/references/build-commands.md new file mode 100644 index 000000000..63cde3dfb --- /dev/null +++ b/.claude/skills/gradle-expert/references/build-commands.md @@ -0,0 +1,214 @@ +# Build Commands Reference + +## Table of Contents +- [Core Build Tasks](#core-build-tasks) +- [Module-Specific Builds](#module-specific-builds) +- [Desktop Tasks](#desktop-tasks) +- [Android Tasks](#android-tasks) +- [Testing](#testing) +- [Analysis & Diagnostics](#analysis--diagnostics) +- [Performance Optimization](#performance-optimization) + +## Core Build Tasks + +### Full Project Build +```bash +./gradlew build # Build all modules +./gradlew clean build # Clean build +./gradlew assemble # Build without tests +``` + +### Incremental Builds +```bash +./gradlew :quartz:build # Build only quartz module +./gradlew :commons:build # Build only commons module +./gradlew :desktopApp:build # Build only desktop app +``` + +## Module-Specific Builds + +### Quartz (KMP Library) +```bash +./gradlew :quartz:build # All targets +./gradlew :quartz:compileKotlinJvm # JVM target only +./gradlew :quartz:compileDebugKotlinAndroid # Android target only +./gradlew :quartz:linkDebugFrameworkIosArm64 # iOS framework +./gradlew :quartz:publishToMavenLocal # Publish locally +``` + +### Commons (Shared UI) +```bash +./gradlew :commons:build # All targets +./gradlew :commons:compileKotlinJvm # Desktop target +./gradlew :commons:compileDebugKotlinAndroid # Android target +``` + +## Desktop Tasks + +### Run Desktop App +```bash +./gradlew :desktopApp:run # Run desktop app +./gradlew :desktopApp:runDistributable # Run packaged version +``` + +### Package Desktop App +```bash +./gradlew :desktopApp:createDistributable # Create runnable package +./gradlew :desktopApp:packageDmg # macOS DMG +./gradlew :desktopApp:packageMsi # Windows MSI +./gradlew :desktopApp:packageDeb # Linux DEB +``` + +### Distribution Location +- macOS: `desktopApp/build/compose/binaries/main/dmg/` +- Windows: `desktopApp/build/compose/binaries/main/msi/` +- Linux: `desktopApp/build/compose/binaries/main/deb/` + +## Android Tasks + +### Compile & Assemble +```bash +./gradlew :amethyst:assembleDebug # Debug APK +./gradlew :amethyst:assembleRelease # Release APK +./gradlew :amethyst:bundleRelease # Release AAB +``` + +### Install & Run +```bash +./gradlew :amethyst:installDebug # Install debug on device +adb shell am start -n com.vitorpamplona.amethyst/.MainActivity +``` + +### Proguard/R8 +```bash +./gradlew :quartz:minifyReleaseWithR8 # Test R8 minification +``` + +## Testing + +### Unit Tests +```bash +./gradlew test # All unit tests +./gradlew :quartz:jvmTest # JVM unit tests +./gradlew :quartz:testDebugUnitTest # Android unit tests +./gradlew :commons:test # Commons tests +``` + +### Android Instrumented Tests +```bash +./gradlew :quartz:connectedAndroidTest # Requires device/emulator +``` + +### Test Reports +```bash +# Reports location: /build/reports/tests/ +open quartz/build/reports/tests/jvmTest/index.html +``` + +## Analysis & Diagnostics + +### Dependency Analysis +```bash +./gradlew dependencies # All dependencies +./gradlew :quartz:dependencies # Quartz dependencies +./gradlew dependencyInsight --dependency okhttp # Specific dependency +``` + +### Build Scan +```bash +./gradlew build --scan # Upload to scans.gradle.com +``` + +### Performance Profiling +```bash +./gradlew build --profile # Generate profile report +# Report: build/reports/profile/profile-.html +``` + +### Task Dependencies +```bash +./gradlew :desktopApp:run --dry-run # Show task graph +./gradlew :desktopApp:dependencies --scan # Visualize dependencies +``` + +## Performance Optimization + +### Configuration Cache +```bash +./gradlew build --configuration-cache # Enable config cache +./gradlew build --configuration-cache-problems=warn +``` + +### Build Cache +```bash +./gradlew build --build-cache # Enable build cache +./gradlew cleanBuildCache # Clear build cache +``` + +### Parallel Execution +```bash +./gradlew build --parallel --max-workers=8 # Parallel with 8 workers +``` + +### Daemon Management +```bash +./gradlew --stop # Stop Gradle daemon +./gradlew --status # Daemon status +``` + +### Incremental Compilation +```bash +# Already enabled by default in Kotlin, but can verify: +./gradlew :quartz:compileKotlinJvm --info | grep "Incremental" +``` + +## gradle.properties Optimizations + +Add to `gradle.properties` for faster builds: + +```properties +# Daemon +org.gradle.daemon=true +org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g + +# Parallel +org.gradle.parallel=true +org.gradle.workers.max=8 + +# Caching +org.gradle.caching=true +org.gradle.configuration-cache=true + +# Kotlin +kotlin.incremental=true +kotlin.daemon.jvmargs=-Xmx2g +``` + +## Common Workflows + +### Full Desktop Build & Run +```bash +./gradlew :desktopApp:clean :desktopApp:run +``` + +### Quick Desktop Iteration +```bash +# No clean - incremental compilation +./gradlew :desktopApp:run +``` + +### Android Release Build +```bash +./gradlew :amethyst:clean :amethyst:bundleRelease +``` + +### Test All KMP Targets +```bash +./gradlew :quartz:test :quartz:testDebugUnitTest +``` + +### Publish Quartz Locally for Testing +```bash +./gradlew :quartz:publishToMavenLocal +# Then update version in consumer project to test +``` diff --git a/.claude/skills/gradle-expert/references/common-errors.md b/.claude/skills/gradle-expert/references/common-errors.md new file mode 100644 index 000000000..a97f59d64 --- /dev/null +++ b/.claude/skills/gradle-expert/references/common-errors.md @@ -0,0 +1,643 @@ +# Common Build Errors & Solutions + +## Table of Contents +- [Compose Version Conflicts](#compose-version-conflicts) +- [secp256k1 JNI Errors](#secp256k1-jni-errors) +- [Source Set Dependency Issues](#source-set-dependency-issues) +- [Proguard/R8 Issues](#proguardr8-issues) +- [Desktop Packaging Errors](#desktop-packaging-errors) +- [Kotlin Compilation Errors](#kotlin-compilation-errors) +- [Dependency Resolution Failures](#dependency-resolution-failures) +- [JVM/JDK Version Issues](#jvmjdk-version-issues) + +--- + +## Compose Version Conflicts + +### Error 1: Compose Runtime Mismatch + +``` +java.lang.IllegalStateException: Version mismatch: Compose runtime is 1.10.0 but compiler is 1.9.0 +``` + +**Cause:** Compose Compiler plugin version doesn't match Compose Runtime + +**Solution:** +```kotlin +// In gradle/libs.versions.toml +composeMultiplatform = "1.9.3" // Must align with Kotlin version +kotlin = "2.3.0" + +// Check compatibility matrix: +// https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-compatibility-and-versioning.html +``` + +**Verification:** +```bash +./gradlew :commons:dependencies | grep compose +``` + +### Error 2: AndroidX Compose BOM Conflict + +``` +Duplicate class androidx.compose.ui.platform.AndroidCompositionLocalMap found in modules... +``` + +**Cause:** Both Compose Multiplatform and AndroidX Compose BOM providing same classes + +**Solution:** +```kotlin +// In commons/build.gradle.kts (KMP module) +// Use Compose Multiplatform, NOT AndroidX BOM +dependencies { + implementation(compose.ui) // ✅ Compose Multiplatform + implementation(compose.material3) + + // Don't use in KMP modules: + // implementation(libs.androidx.compose.bom) // ❌ Android-only +} + +// In amethyst/build.gradle.kts (Android-only module) +// Can use AndroidX BOM +dependencies { + val composeBom = platform(libs.androidx.compose.bom) + implementation(composeBom) + implementation(libs.androidx.ui) +} +``` + +### Error 3: Material3 WindowSizeClass Not Found + +``` +Unresolved reference: WindowSizeClass +``` + +**Cause:** Using Android's WindowSizeClass in shared KMP code + +**Solution:** +```kotlin +// Don't use in commonMain or jvmAndroid: +// import androidx.compose.material3.windowsizeclass.WindowSizeClass // ❌ + +// Use in androidMain only, or create expect/actual: +// commonMain +expect class WindowSizeClassAdapter + +// androidMain +actual typealias WindowSizeClassAdapter = androidx.compose.material3.windowsizeclass.WindowSizeClass + +// jvmMain (desktop) +actual class WindowSizeClassAdapter { /* Custom impl */ } +``` + +--- + +## secp256k1 JNI Errors + +### Error 1: JNI Library Not Found (Desktop) + +``` +java.lang.UnsatisfiedLinkError: no secp256k1jni in java.library.path +``` + +**Cause:** Desktop using wrong secp256k1 variant (Android JNI instead of JVM JNI) + +**Solution:** +```kotlin +// In quartz/build.gradle.kts +sourceSets { + jvmMain { + dependencies { + // ✅ Correct - JVM variant + implementation(libs.secp256k1.kmp.jni.jvm) + + // ❌ Wrong - Android variant + // implementation(libs.secp256k1.kmp.jni.android) + } + } +} +``` + +**Verification:** +```bash +./gradlew :quartz:dependencies --configuration jvmRuntimeClasspath | grep secp256k1 +# Should show: secp256k1-kmp-jni-jvm, NOT jni-android +``` + +### Error 2: Version Mismatch Between Variants + +``` +java.lang.NoSuchMethodError: fr.acinq.secp256k1.Secp256k1.sign +``` + +**Cause:** Common, Android, and JVM variants have different versions + +**Solution:** +```toml +# In gradle/libs.versions.toml +# All three MUST use same version +secp256k1KmpJniAndroid = "0.22.0" + +[libraries] +secp256k1-kmp-common = { ..., version.ref = "secp256k1KmpJniAndroid" } +secp256k1-kmp-jni-android = { ..., version.ref = "secp256k1KmpJniAndroid" } +secp256k1-kmp-jni-jvm = { ..., version.ref = "secp256k1KmpJniAndroid" } +``` + +### Error 3: Android JNI Not Loaded + +``` +java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader couldn't find "libsecp256k1jni.so" +``` + +**Cause:** Proguard stripping JNI classes + +**Solution:** +```proguard +# In quartz/proguard-rules.pro +-keep class fr.acinq.secp256k1.** { *; } +``` + +--- + +## Source Set Dependency Issues + +### Error 1: jvmAndroid Defined After androidMain + +``` +Could not get unknown property 'jvmAndroid' for source set container +``` + +**Cause:** Source sets must be defined in dependency order + +**Solution:** +```kotlin +// ✅ Correct order +sourceSets { + commonMain { } + + // Define jvmAndroid BEFORE androidMain and jvmMain + val jvmAndroid = create("jvmAndroid") { + dependsOn(commonMain.get()) + } + + androidMain { + dependsOn(jvmAndroid) // Now jvmAndroid exists + } + + jvmMain { + dependsOn(jvmAndroid) + } +} +``` + +### Error 2: Dependency in Wrong Source Set + +``` +Unresolved reference: ObjectMapper (Jackson) +``` + +**Cause:** JVM-only library in commonMain + +**Solution:** +```kotlin +sourceSets { + commonMain { + // ❌ Jackson is JVM-only, can't use here + // implementation(libs.jackson.module.kotlin) + } + + val jvmAndroid = create("jvmAndroid") { + dependsOn(commonMain.get()) + // ✅ Jackson in jvmAndroid (shared JVM code) + api(libs.jackson.module.kotlin) + } +} +``` + +### Error 3: Platform-Specific Code in Shared Source Set + +``` +java.lang.NoClassDefFoundError: android.content.Context +``` + +**Cause:** Android-specific API in jvmAndroid or commonMain + +**Solution:** +```kotlin +// Use expect/actual pattern + +// commonMain +expect class PlatformContext + +// androidMain +actual typealias PlatformContext = android.content.Context + +// jvmMain +actual class PlatformContext { + // Custom desktop implementation +} +``` + +--- + +## Proguard/R8 Issues + +### Error 1: Native Library Classes Stripped + +``` +java.lang.NoClassDefFoundError: com.goterl.lazysodium.Sodium +``` + +**Cause:** R8/Proguard removing JNA/LibSodium classes + +**Solution:** +```proguard +# In quartz/proguard-rules.pro +-keep class com.goterl.lazysodium.** { *; } +-keep class com.sun.jna.** { *; } +-keep class fr.acinq.secp256k1.** { *; } +``` + +### Error 2: Reflection-Based Libraries Broken + +``` +com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of ... +``` + +**Cause:** Jackson uses reflection, R8 strips class metadata + +**Solution:** +```proguard +# Preserve reflection metadata +-keepattributes *Annotation* +-keepattributes Signature +-keepattributes InnerClasses + +# Keep all Quartz event classes +-keep class com.vitorpamplona.quartz.** { *; } +``` + +### Error 3: Enum Values Missing + +``` +java.lang.IllegalArgumentException: No enum constant ... +``` + +**Cause:** R8 obfuscating enum names + +**Solution:** +```proguard +# Keep all enums +-keep enum ** { *; } +-keepnames class ** { *; } +``` + +--- + +## Desktop Packaging Errors + +### Error 1: Icon Not Found + +``` +FAILURE: Build failed with an exception. +* What went wrong: Cannot find icon file: src/jvmMain/resources/icon.icns +``` + +**Cause:** Icon file missing or wrong path + +**Solution:** +```kotlin +// In desktopApp/build.gradle.kts +nativeDistributions { + macOS { + // Ensure file exists at this path + iconFile.set(project.file("src/jvmMain/resources/icon.icns")) + } + + // Check file exists: + // ls -la desktopApp/src/jvmMain/resources/ +} +``` + +**Icon Requirements:** +- macOS: `.icns` (512x512, 256x256, 128x128, 32x32) +- Windows: `.ico` (256x256, 128x128, 64x64, 32x32, 16x16) +- Linux: `.png` (512x512 recommended) + +### Error 2: Main Class Not Found + +``` +Error: Could not find or load main class com.vitorpamplona.amethyst.desktop.MainKt +``` + +**Cause:** Wrong mainClass path or Main.kt doesn't have main() + +**Solution:** +```kotlin +// In desktopApp/build.gradle.kts +compose.desktop { + application { + mainClass = "com.vitorpamplona.amethyst.desktop.MainKt" + // ^^^^ + // Kotlin compiler adds "Kt" suffix + } +} + +// In src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt +fun main() = application { + // ... +} +``` + +### Error 3: Native Library Missing in Package + +``` +java.lang.UnsatisfiedLinkError: no secp256k1jni in java.library.path +``` + +**Cause:** Native libraries not bundled in distribution + +**Solution:** +```kotlin +// Native libs are automatically included via dependencies +// Verify secp256k1-kmp-jni-jvm is in dependencies: +dependencies { + implementation(libs.secp256k1.kmp.jni.jvm) // ✅ Includes native libs +} + +// Test packaged app: +./gradlew :desktopApp:createDistributable +# Run from: desktopApp/build/compose/binaries/main/app/ +``` + +--- + +## Kotlin Compilation Errors + +### Error 1: Expect/Actual Mismatch + +``` +'actual' declaration has no corresponding expected declaration +``` + +**Cause:** Signature mismatch or missing expect + +**Solution:** +```kotlin +// commonMain - expect declaration +expect class CryptoProvider { + fun sign(message: ByteArray, privateKey: ByteArray): ByteArray +} + +// androidMain & jvmMain - actual must match EXACTLY +actual class CryptoProvider { + actual fun sign(message: ByteArray, privateKey: ByteArray): ByteArray { + // Implementation + } +} + +// Common mistakes: +// - Different parameter names ❌ +// - Different return types ❌ +// - Missing 'actual' modifier ❌ +``` + +### Error 2: Target JVM Version Mismatch + +``` +Compilation failed: module was compiled with an incompatible version of Kotlin +``` + +**Cause:** Different JVM targets across modules + +**Solution:** +```kotlin +// Ensure ALL modules use same JVM target + +// In quartz/build.gradle.kts +kotlin { + jvm { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_21) // ✅ Java 21 + } + } +} + +// In android {} block +compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} +``` + +### Error 3: Compose Compiler Plugin Missing + +``` +This declaration needs opt-in. Please use @OptIn(ComposeApi::class) or @Composable +``` + +**Cause:** Compose compiler plugin not applied + +**Solution:** +```kotlin +// In build.gradle.kts +plugins { + alias(libs.plugins.jetbrainsComposeCompiler) // ✅ Add this + alias(libs.plugins.composeMultiplatform) +} +``` + +--- + +## Dependency Resolution Failures + +### Error 1: Repository Not Found + +``` +Could not find com.github.vitorpamplona.compose-richtext:richtext-ui:f92ef49c9d +``` + +**Cause:** Jitpack or custom Maven repository not configured + +**Solution:** +```kotlin +// In settings.gradle +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + maven { url = "https://jitpack.io" } // ✅ Add Jitpack + } +} +``` + +### Error 2: Gradle Version Too Old + +``` +Version catalogs are not supported in this version of Gradle +``` + +**Cause:** Gradle < 7.0 + +**Solution:** +```properties +# In gradle/wrapper/gradle-wrapper.properties +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +``` + +Then: `./gradlew wrapper --gradle-version=8.9` + +### Error 3: Dependency Variant Not Found + +``` +No matching variant of fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.22.0 was found +``` + +**Cause:** Wrong dependency configuration for target + +**Solution:** +```kotlin +// In androidMain (Android library module) +dependencies { + // For AAR packaging + implementation("net.java.dev.jna:jna:5.18.1@aar") // ✅ Specify @aar + + // secp256k1 works without @aar (auto-detects) + api(libs.secp256k1.kmp.jni.android) +} +``` + +--- + +## JVM/JDK Version Issues + +### Error 1: Unsupported Class File Version + +``` +Unsupported class file major version 65 +``` + +**Cause:** Compiled with Java 21, running with older Java + +**Solution:** +```bash +# Check Java version +java -version # Should show 21 + +# Set JAVA_HOME if needed +export JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home + +# Or in gradle.properties +org.gradle.java.home=/path/to/jdk-21 +``` + +### Error 2: JVM Toolchain Not Found + +``` +No matching toolchain found for requested JvmVersion +``` + +**Cause:** Java 21 not installed or not detected + +**Solution:** +```bash +# macOS (Homebrew) +brew install openjdk@21 + +# Ubuntu +sudo apt install openjdk-21-jdk + +# Set JAVA_HOME +export JAVA_HOME=$(/usr/libexec/java_home -v 21) # macOS +export JAVA_HOME=/usr/lib/jvm/java-21-openjdk # Linux + +# Verify +./gradlew -version +``` + +### Error 3: Gradle Daemon Using Wrong Java + +``` +Daemon will be stopped at the end of the build because JVM version has changed +``` + +**Cause:** Daemon started with different Java version + +**Solution:** +```bash +# Stop all daemons +./gradlew --stop + +# Start with correct JAVA_HOME +export JAVA_HOME=/path/to/jdk-21 +./gradlew build + +# Or set in gradle.properties permanently +org.gradle.java.home=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home +``` + +--- + +## General Troubleshooting Steps + +### Step 1: Clean Build +```bash +./gradlew clean +./gradlew --stop # Stop daemon +./gradlew build +``` + +### Step 2: Check Dependencies +```bash +./gradlew :moduleName:dependencies +./gradlew dependencyInsight --dependency libraryName +``` + +### Step 3: Enable Debug Logging +```bash +./gradlew build --info # Info logging +./gradlew build --debug # Debug logging (verbose) +./gradlew build --stacktrace +``` + +### Step 4: Invalidate Caches +```bash +# Clear Gradle cache +rm -rf ~/.gradle/caches/ + +# Clear build outputs +./gradlew clean + +# Clear Gradle wrapper cache +rm -rf ~/.gradle/wrapper/ +``` + +### Step 5: Build Scan +```bash +./gradlew build --scan +# Opens interactive diagnostics in browser +``` + +## Quick Reference: Error Keywords → Solution + +| Error Keyword | Likely Cause | Quick Fix | +|---------------|--------------|-----------| +| `UnsatisfiedLinkError` | Wrong JNI variant | Check secp256k1/JNA variants by platform | +| `IllegalStateException` (Compose) | Version mismatch | Align Compose Multiplatform + Kotlin versions | +| `NoClassDefFoundError` | Proguard stripping | Add `-keep` rule for class | +| `Unresolved reference` | Wrong source set | Move to appropriate source set (jvmAndroid) | +| `Duplicate class` | BOM conflict | Remove AndroidX BOM from KMP modules | +| `Version mismatch` | Plugin/runtime version mismatch | Update libs.versions.toml | +| `No matching variant` | Repository or packaging issue | Add repository or @aar suffix | +| `Could not find` (dependency) | Missing repository | Add maven/jitpack to repositories | +| `Unsupported class file` | Java version mismatch | Update JAVA_HOME to Java 21 | + +--- + +## Getting Help + +1. **Check Build Scan**: `./gradlew build --scan` for detailed diagnostics +2. **Gradle Forums**: https://discuss.gradle.org/ +3. **Kotlin Slack**: #multiplatform channel +4. **Stack Overflow**: Tags `gradle`, `kotlin-multiplatform`, `compose-multiplatform` diff --git a/.claude/skills/gradle-expert/references/dependency-graph.md b/.claude/skills/gradle-expert/references/dependency-graph.md new file mode 100644 index 000000000..3f3820398 --- /dev/null +++ b/.claude/skills/gradle-expert/references/dependency-graph.md @@ -0,0 +1,266 @@ +# Module Dependency Graph + +## Visual Hierarchy + +``` +┌─────────────────────────────────────────────────────────┐ +│ Root Project │ +│ (Amethyst) │ +└─────────────────────────────────────────────────────────┘ + │ + ┌────────────────┼────────────────┬────────────┐ + │ │ │ │ + ▼ ▼ ▼ ▼ +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌──────────┐ +│ :amethyst │ │ :desktopApp │ │ :benchmark │ │:ammolite │ +│ (Android) │ │ (JVM) │ │ (Android) │ │ (Support)│ +└─────────────┘ └─────────────┘ └─────────────┘ └──────────┘ + │ │ │ + │ │ │ + └────────────────┼────────────────┘ + │ + ▼ + ┌─────────────┐ + │ :commons │ + │ (KMP UI) │ + │ │ + │ jvmAndroid │ + │ / \ │ + │ jvm android│ + └─────────────┘ + │ + │ + ▼ + ┌─────────────┐ + │ :quartz │ + │(KMP Library)│ + │ │ + │ commonMain │ + │ │ │ + │ jvmAndroid │ + │ / | \ │ + │jvm and ios │ + └─────────────┘ +``` + +## Module Details + +### :quartz (KMP Nostr Library) +**Type:** Kotlin Multiplatform Library +**Targets:** JVM, Android, iOS (iosX64, iosArm64, iosSimulatorArm64) +**Dependencies:** +- External: secp256k1, jackson, okhttp, kotlinx.coroutines, kotlinx.collections.immutable +- Source sets: commonMain → jvmAndroid → {androidMain, jvmMain}, iosMain + +**Role:** Core Nostr protocol implementation, shared across all platforms + +### :commons (Shared UI Components) +**Type:** Kotlin Multiplatform Library +**Targets:** JVM, Android +**Dependencies:** +- Module: `:quartz` +- External: Compose Multiplatform, Material3, kotlinx.collections.immutable +- Source sets: commonMain → jvmAndroid → {androidMain, jvmMain} + +**Role:** Shared Compose UI components for Desktop and Android + +### :desktopApp (Desktop Application) +**Type:** JVM Application +**Targets:** JVM (Desktop) +**Dependencies:** +- Modules: `:commons`, `:quartz` +- External: Compose Desktop, kotlinx.coroutines.swing + +**Role:** Desktop-specific navigation, layouts, and entry point + +### :amethyst (Android Application) +**Type:** Android Application +**Targets:** Android +**Dependencies:** +- Modules: `:commons`, `:quartz`, `:ammolite` +- External: Android SDK, AndroidX, Firebase, Tor + +**Role:** Android-specific navigation, layouts, and entry point + +### :benchmark (Android Benchmark) +**Type:** Android Library +**Targets:** Android +**Dependencies:** +- Modules: `:commons`, `:quartz` +- External: AndroidX Benchmark + +**Role:** Performance benchmarking for Android builds + +### :ammolite (Support Module) +**Type:** Android Library +**Targets:** Android +**Dependencies:** Android-specific utilities + +**Role:** Android support utilities for amethyst + +## Dependency Flow Patterns + +### Desktop Build Chain +``` +:desktopApp → :commons (jvmMain) → :quartz (jvmMain) + ↓ + jvmAndroid + ↓ + commonMain +``` + +### Android Build Chain +``` +:amethyst → :commons (androidMain) → :quartz (androidMain) + ↓ ↓ +:ammolite jvmAndroid + ↓ + commonMain +``` + +## Source Set Dependencies + +### :quartz Source Sets +``` +commonMain (base) + ├─ jvmAndroid (shared JVM code) + │ ├─ androidMain (Android platform) + │ └─ jvmMain (Desktop platform) + └─ iosMain (iOS platform) + ├─ iosX64Main + ├─ iosArm64Main + └─ iosSimulatorArm64Main +``` + +**Key Dependencies per Source Set:** +- **commonMain**: secp256k1-kmp, kotlinx.coroutines, collection, immutable collections +- **jvmAndroid**: jackson, okhttp, url-detector, rfc3986 +- **androidMain**: secp256k1-kmp-jni-android, lazysodium-android, jna (aar) +- **jvmMain**: secp256k1-kmp-jni-jvm, lazysodium-java, jna (jar) + +### :commons Source Sets +``` +commonMain (base UI) + └─ jvmAndroid (shared JVM UI) + ├─ androidMain (Android UI utilities) + └─ jvmMain (Desktop UI utilities) +``` + +**Key Dependencies per Source Set:** +- **commonMain**: Compose Multiplatform, Material3, :quartz +- **jvmAndroid**: url-detector +- **androidMain**: AndroidX Compose tooling +- **jvmMain**: Compose Desktop + +## Critical Dependency Patterns + +### 1. secp256k1 Variants +```kotlin +// commonMain - API only +api(libs.secp256k1.kmp.common) + +// androidMain - JNI Android +api(libs.secp256k1.kmp.jni.android) + +// jvmMain - JNI JVM +implementation(libs.secp256k1.kmp.jni.jvm) +``` +**Why:** Different JNI bindings for Android vs Desktop JVM + +### 2. JNA Variants (for LibSodium) +```kotlin +// androidMain +implementation("com.goterl:lazysodium-android:5.2.0@aar") +implementation("net.java.dev.jna:jna:5.18.1@aar") + +// jvmMain +implementation(libs.lazysodium.java) +implementation(libs.jna) // JAR variant +``` +**Why:** Android needs AAR packaging, JVM needs JAR + +### 3. Compose Alignment +```kotlin +// commons/build.gradle.kts +implementation(compose.ui) // Compose Multiplatform BOM +implementation(compose.material3) + +// Version catalog alignment +composeMultiplatform = "1.9.3" +composeBom = "2025.12.01" // AndroidX Compose +``` +**Why:** Two Compose ecosystems (Multiplatform + AndroidX) must align + +## Dependency Configuration Types + +### API vs Implementation + +**Use `api` when:** +- Dependency types appear in module's public API +- Used in expect/actual declarations visible to consumers +- Example: `secp256k1-kmp-common` in quartz (public types) + +**Use `implementation` when:** +- Internal implementation detail +- Not exposed to module consumers +- Example: `okhttp` in quartz (internal network client) + +### Example from quartz +```kotlin +// Public API - exposed to consumers +api(libs.secp256k1.kmp.common) +api(libs.jackson.module.kotlin) // Event serialization public + +// Internal implementation +implementation(libs.okhttp) +implementation(libs.kotlinx.coroutines.core) +``` + +## Transitive Dependency Impact + +### When :desktopApp depends on :commons +- Gets `:quartz` transitively (via :commons) +- Gets `secp256k1-kmp-jvm` transitively (via :quartz jvmMain) +- Does NOT get Android-specific dependencies (scoped to androidMain) + +### When :amethyst depends on :commons +- Gets `:quartz` transitively (via :commons) +- Gets `secp256k1-kmp-jni-android` transitively (via :quartz androidMain) +- Does NOT get JVM/Desktop-specific dependencies (scoped to jvmMain) + +## Verifying Dependencies + +### Check Module Dependencies +```bash +./gradlew :desktopApp:dependencies +./gradlew :amethyst:dependencies +``` + +### Check Specific Library +```bash +./gradlew dependencyInsight --dependency secp256k1 +./gradlew dependencyInsight --dependency compose-ui +``` + +### Visualize with Build Scan +```bash +./gradlew :desktopApp:dependencies --scan +# Opens interactive dependency graph in browser +``` + +## Common Dependency Issues + +### Issue 1: Wrong secp256k1 Variant in Desktop +**Symptom:** `UnsatisfiedLinkError: no secp256k1jni in java.library.path` +**Cause:** Desktop using Android JNI variant +**Fix:** Ensure jvmMain uses `secp256k1-kmp-jni-jvm` + +### Issue 2: Compose Version Mismatch +**Symptom:** `IllegalStateException: Version mismatch` +**Cause:** Compose Multiplatform plugin vs runtime version mismatch +**Fix:** Align `composeMultiplatform` version in libs.versions.toml with Kotlin plugin + +### Issue 3: Duplicate JNA Classes +**Symptom:** `DuplicateClassException: com.sun.jna.Native` +**Cause:** Both JAR and AAR JNA variants in classpath +**Fix:** Use AAR (@aar) in androidMain, JAR in jvmMain (never in shared source sets) diff --git a/.claude/skills/gradle-expert/references/version-catalog-guide.md b/.claude/skills/gradle-expert/references/version-catalog-guide.md new file mode 100644 index 000000000..0c50df357 --- /dev/null +++ b/.claude/skills/gradle-expert/references/version-catalog-guide.md @@ -0,0 +1,422 @@ +# Version Catalog Guide + +## Overview + +AmethystMultiplatform uses Gradle's version catalog (`gradle/libs.versions.toml`) to centralize dependency management. This ensures version consistency across all modules and simplifies updates. + +## Structure + +### sections +```toml +[versions] # Version numbers (referenced by libraries and plugins) +[libraries] # Library dependencies +[plugins] # Gradle plugins +``` + +## Version References + +### Defining Versions +```toml +[versions] +kotlin = "2.3.0" +composeMultiplatform = "1.9.3" +okhttp = "5.3.2" +``` + +### Special Patterns + +#### Android SDK Versions +```toml +android-compileSdk = "36" +android-minSdk = "26" +android-targetSdk = "36" +``` +**Access in build.gradle.kts:** +```kotlin +compileSdk = libs.versions.android.compileSdk.get().toInt() +minSdk = libs.versions.android.minSdk.get().toInt() +``` + +#### Version Suffixes (Git Commits) +```toml +androidKotlinGeohash = "b481c6a64e" # Jitpack commit hash +markdown = "f92ef49c9d" +``` +**Why:** For GitHub dependencies via Jitpack that don't have semantic versions + +## Library Declarations + +### Basic Pattern +```toml +[libraries] +library-name = { group = "...", name = "...", version.ref = "..." } +``` + +### Examples + +#### Version Reference +```toml +okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } +``` + +#### Module Reference (for multi-artifact libs) +```toml +androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "androidxCamera" } +``` + +#### Without Group (shorthand) +```toml +androidx-ui = { group = "androidx.compose.ui", name = "ui" } +``` +**Note:** Inherits version from BOM (compose-bom) + +### BOMs (Bill of Materials) + +#### AndroidX Compose BOM +```toml +[versions] +composeBom = "2025.12.01" + +[libraries] +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +androidx-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +``` + +**Usage in build.gradle.kts:** +```kotlin +val composeBom = platform(libs.androidx.compose.bom) +implementation(composeBom) +implementation(libs.androidx.ui) // Version from BOM +implementation(libs.androidx.material3) // Version from BOM +``` + +**Benefits:** +- All AndroidX Compose artifacts use compatible versions +- Update single BOM version, not individual libraries +- Prevents version conflicts + +### Platform-Specific Variants + +#### secp256k1 (KMP crypto library) +```toml +secp256k1KmpJniAndroid = "0.22.0" + +[libraries] +secp256k1-kmp-common = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp", version.ref = "secp256k1KmpJniAndroid" } +secp256k1-kmp-jni-android = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-android", version.ref = "secp256k1KmpJniAndroid" } +secp256k1-kmp-jni-jvm = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-jvm", version.ref = "secp256k1KmpJniAndroid" } +``` + +**Critical:** All three variants MUST share the same version + +#### JNA (for LibSodium) +```toml +jna = "5.18.1" + +[libraries] +jna = { group = "net.java.dev.jna", name = "jna", version.ref = "jna" } +``` + +**Usage in build.gradle.kts:** +```kotlin +// androidMain - AAR packaging +implementation("net.java.dev.jna:jna:5.18.1@aar") + +// jvmMain - JAR packaging +implementation(libs.jna) +``` + +**Why:** Android needs AAR, JVM needs JAR (different artifact types) + +## Plugin Declarations + +### Basic Pattern +```toml +[plugins] +plugin-id = { id = "...", version.ref = "..." } +``` + +### Examples + +#### Kotlin Plugins +```toml +[versions] +kotlin = "2.3.0" + +[plugins] +jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +jetbrainsKotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +jetbrainsComposeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +serialization = { id = 'org.jetbrains.kotlin.plugin.serialization', version.ref = 'kotlinxSerializationPlugin' } +``` + +**Critical:** All Kotlin plugins MUST use the same Kotlin version + +#### Android Gradle Plugin +```toml +[versions] +agp = "8.13.2" + +[plugins] +androidApplication = { id = "com.android.application", version.ref = "agp" } +androidLibrary = { id = "com.android.library", version.ref = "agp" } +androidKotlinMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" } +``` + +#### Compose Multiplatform +```toml +[versions] +composeMultiplatform = "1.9.3" + +[plugins] +composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" } +``` + +### Plugin Application + +```kotlin +// In build.gradle.kts +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidLibrary) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.jetbrainsComposeCompiler) +} +``` + +## Usage in Build Files + +### Accessing Versions +```kotlin +// Direct version access +val kotlinVersion = libs.versions.kotlin.get() +val minSdk = libs.versions.android.minSdk.get().toInt() +``` + +### Accessing Libraries +```kotlin +dependencies { + implementation(libs.kotlinx.coroutines.core) + api(libs.secp256k1.kmp.common) + implementation(libs.okhttp) +} +``` + +### Accessing Plugins +```kotlin +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidLibrary) +} +``` + +## Version Catalog Benefits + +### 1. Centralized Version Management +Update once, applies everywhere: +```toml +# Change one line +kotlin = "2.3.0" → "2.4.0" + +# Affects all usages +- kotlinMultiplatform plugin +- jetbrainsKotlinAndroid plugin +- kotlin-stdlib +- All Kotlin-related dependencies +``` + +### 2. Type-Safe Accessors +```kotlin +// Compile-time checked +implementation(libs.okhttp) // ✅ IDE autocomplete + +// vs string-based (error-prone) +implementation("com.squareup.okhttp3:okhttp:5.3.2") // ❌ No autocomplete +``` + +### 3. Dependency Consistency +```kotlin +// All modules reference same catalog +:quartz → libs.okhttp +:commons → libs.okhttp +:desktopApp → libs.okhttp + +// Same version everywhere +``` + +### 4. Gradle Sync Improvements +- Faster IDE sync (pre-parsed catalog) +- Better dependency resolution +- Clearer error messages + +## Common Patterns + +### GitHub Dependencies (Jitpack) +```toml +[versions] +markdown = "f92ef49c9d" # Git commit hash + +[libraries] +markdown-ui = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-ui", version.ref = "markdown" } +``` + +**Repository config** (in settings.gradle): +```kotlin +repositories { + maven { url = "https://jitpack.io" } +} +``` + +### Multi-Artifact Libraries +```toml +[versions] +media3 = "1.9.0" + +[libraries] +androidx-media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" } +androidx-media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3" } +androidx-media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "media3" } +``` + +**Why:** All media3 artifacts share same version for compatibility + +### Test Dependencies +```toml +[libraries] +junit = { group = "junit", name = "junit", version.ref = "junit" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidxJunit" } +mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } +kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-test"} +``` + +## Version Update Strategy + +### Check for Updates +```bash +# Using Gradle Versions Plugin (if installed) +./gradlew dependencyUpdates + +# Manual check +# Browse to Maven Central for specific library +``` + +### Update Process +1. **Update version in catalog** + ```toml + okhttp = "5.3.2" → "5.4.0" + ``` + +2. **Test locally** + ```bash + ./gradlew clean build + ``` + +3. **Check for breaking changes** + - Review library changelog + - Run full test suite + +4. **Commit with clear message** + ``` + chore: update okhttp 5.3.2 → 5.4.0 + ``` + +### Critical Version Alignments + +#### Kotlin Ecosystem +```toml +kotlin = "2.3.0" +kotlinxCoroutinesCore = "1.10.2" +kotlinxSerialization = "1.9.0" +``` +**Rule:** Kotlin version must be compatible with kotlinx libraries + +#### Compose Ecosystem +```toml +composeMultiplatform = "1.9.3" +composeBom = "2025.12.01" +kotlin = "2.3.0" +``` +**Rule:** Compose Multiplatform → Kotlin version (see compatibility matrix) + +#### AGP & Gradle +```toml +agp = "8.13.2" +# Requires Gradle 8.9+ +``` +**Rule:** AGP version dictates minimum Gradle version + +## Troubleshooting + +### Issue 1: Unresolved Reference +**Error:** `Unresolved reference: libs` + +**Cause:** Gradle version < 7.0 (version catalogs not supported) + +**Fix:** Upgrade Gradle in `gradle/wrapper/gradle-wrapper.properties` + +### Issue 2: Library Not Found +**Error:** `Could not find com.example:library:1.0.0` + +**Cause:** Repository not configured or typo in catalog + +**Fix:** +1. Check repository in settings.gradle +2. Verify group/name/version in libs.versions.toml + +### Issue 3: Version Conflict +**Error:** `Conflict with dependency ... and ...` + +**Cause:** Different versions of same library via transitive dependencies + +**Fix:** +```kotlin +configurations.all { + resolutionStrategy { + force(libs.okhttp.get().toString()) + } +} +``` + +## Best Practices + +### 1. Naming Conventions +```toml +# Hyphen-separated, hierarchical +androidx-compose-ui +androidx-compose-material3 +kotlinx-coroutines-core + +# Platform suffixes +secp256k1-kmp-jni-android +secp256k1-kmp-jni-jvm +``` + +### 2. Group Related Dependencies +```toml +# Camera APIs together +androidx-camera-core +androidx-camera-camera2 +androidx-camera-view +``` + +### 3. Document Special Cases +```toml +# JNA requires @aar for Android (see build.gradle.kts) +jna = { group = "net.java.dev.jna", name = "jna", version.ref = "jna" } +``` + +### 4. Keep BOMs Updated +```toml +# Update BOM, individual libs follow +composeBom = "2025.12.01" # Latest stable +``` + +### 5. Test Version Updates +```bash +# Before committing +./gradlew :quartz:test +./gradlew :commons:test +./gradlew :desktopApp:run +``` diff --git a/.claude/skills/gradle-expert/scripts/analyze-build-time.sh b/.claude/skills/gradle-expert/scripts/analyze-build-time.sh new file mode 100644 index 000000000..49dad60f4 --- /dev/null +++ b/.claude/skills/gradle-expert/scripts/analyze-build-time.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# Analyze Gradle build performance and generate report + +set -e + +PROJECT_ROOT="${1:-.}" +cd "$PROJECT_ROOT" + +echo "🔍 Analyzing Gradle build performance..." +echo "========================================" +echo "" + +# Clean build for accurate timing +echo "Running clean build with --profile..." +./gradlew clean build --profile --scan + +# Find the latest profile report +PROFILE_REPORT=$(find build/reports/profile -name "*.html" -type f -printf '%T@ %p\n' | sort -n | tail -1 | cut -f2- -d" ") + +if [ -n "$PROFILE_REPORT" ]; then + echo "" + echo "✅ Profile report generated: $PROFILE_REPORT" + echo "" + echo "📊 Build Performance Summary:" + echo "----------------------------" + + # Extract key metrics if available + if command -v jq &> /dev/null && [ -f "build/reports/profile/profile.json" ]; then + jq -r '.buildTime, .taskExecutionTime' build/reports/profile/profile.json + else + echo "Open the HTML report for detailed analysis:" + echo "file://$PWD/$PROFILE_REPORT" + fi +else + echo "⚠️ Profile report not found" +fi + +echo "" +echo "💡 Build optimization tips:" +echo "- Enable Gradle daemon: org.gradle.daemon=true" +echo "- Parallel execution: org.gradle.parallel=true" +echo "- Configuration cache: org.gradle.configuration-cache=true" +echo "- Build cache: org.gradle.caching=true" +echo "" +echo "Add these to gradle.properties for faster builds" diff --git a/.claude/skills/gradle-expert/scripts/fix-dependency-conflicts.sh b/.claude/skills/gradle-expert/scripts/fix-dependency-conflicts.sh new file mode 100644 index 000000000..17951ace4 --- /dev/null +++ b/.claude/skills/gradle-expert/scripts/fix-dependency-conflicts.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# Diagnose and suggest fixes for common dependency conflicts + +set -e + +PROJECT_ROOT="${1:-.}" +cd "$PROJECT_ROOT" + +echo "🔍 Analyzing dependency conflicts..." +echo "====================================" +echo "" + +# Run dependency report +echo "Generating dependency insight report..." +./gradlew dependencies --configuration runtimeClasspath > /tmp/gradle-dependencies.txt 2>&1 || true + +# Check for common conflict patterns +echo "" +echo "🔎 Checking for common issues:" +echo "------------------------------" + +# Check 1: Compose version conflicts +if grep -q "compose" /tmp/gradle-dependencies.txt; then + echo "✓ Compose dependencies found" + echo " Tip: Ensure Compose Multiplatform and AndroidX Compose versions align" + echo " Current project uses:" + echo " - Compose Multiplatform BOM" + echo " - AndroidX Compose BOM" +fi + +# Check 2: secp256k1 variants +if grep -q "secp256k1" /tmp/gradle-dependencies.txt; then + echo "✓ secp256k1 dependencies found" + echo " Ensure correct variant:" + echo " - Android: secp256k1-kmp-jni-android" + echo " - JVM/Desktop: secp256k1-kmp-jni-jvm" + echo " - Common: secp256k1-kmp (transitive)" +fi + +# Check 3: Kotlin version alignment +KOTLIN_VERSION=$(grep "kotlin =" gradle/libs.versions.toml | cut -d'"' -f2) +echo "✓ Kotlin version: $KOTLIN_VERSION" +echo " All Kotlin plugins should use the same version" + +# Check 4: Multiple versions of same library +echo "" +echo "🔍 Checking for version conflicts..." +./gradlew dependencyInsight --configuration runtimeClasspath --dependency okhttp || true + +echo "" +echo "💡 Common fixes:" +echo "---------------" +echo "1. Compose conflicts:" +echo " - Align compose-multiplatform plugin version with runtime" +echo " - Use BOM for AndroidX Compose to enforce consistency" +echo "" +echo "2. secp256k1 conflicts:" +echo " - Use 'api' instead of 'implementation' in source sets" +echo " - Ensure androidMain uses jni-android, jvmMain uses jni-jvm" +echo "" +echo "3. Kotlin version conflicts:" +echo " - Update all kotlin plugins to same version in libs.versions.toml" +echo " - Check for transitive Kotlin dependencies" +echo "" +echo "Run './gradlew dependencyInsight --dependency ' for specific conflicts" diff --git a/.claude/skills/nostr-expert/SKILL.md b/.claude/skills/nostr-expert/SKILL.md new file mode 100644 index 000000000..692f2fe76 --- /dev/null +++ b/.claude/skills/nostr-expert/SKILL.md @@ -0,0 +1,551 @@ +--- +name: nostr-expert +description: Nostr protocol implementation patterns in Quartz (AmethystMultiplatform's KMP Nostr library). Use when working with: (1) Nostr events (creating, parsing, signing), (2) Event kinds and tags, (3) NIP implementations (57 NIPs in quartz/), (4) Event builders and TagArrayBuilder DSL, (5) Nostr cryptography (secp256k1, NIP-44 encryption), (6) Relay communication patterns, (7) Bech32 encoding (npub, nsec, note, nevent). Complements nostr-protocol agent (NIP specs) - this skill provides Quartz codebase patterns and implementation details. +--- + +# Nostr Protocol Expert (Quartz Implementation) + +Practical patterns for working with Nostr in Quartz, AmethystMultiplatform's KMP Nostr library. + +## When to Use This Skill + +- Implementing Nostr event types (TextNote, Reaction, Zap, etc.) +- Creating/parsing events with TagArrayBuilder DSL +- Working with event kinds and tags +- Finding NIP implementations in quartz/ codebase +- Nostr cryptography (secp256k1 signing, NIP-44 encryption) +- Bech32 encoding/decoding (npub, nsec, note formats) +- Event validation and verification + +**For NIP specifications** → Use `nostr-protocol` agent +**For Quartz implementation** → Use this skill + +## Quartz Architecture + +Quartz organizes code by NIP number: + +``` +quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/ +├── nip01Core/ # Core protocol (Event, Kind, Tags) +├── nip04Dm/ # Legacy DMs (deprecated) +├── nip10Notes/ # Text notes with threading +├── nip17Dm/ # Private DMs (gift wrap) +├── nip19Bech32/ # Bech32 encoding +├── nip44Encryption/ # Modern encryption (ChaCha20) +├── nip57Zaps/ # Lightning zaps +├── ... (57 NIPs total) +└── experimental/ # Draft NIPs +``` + +**Pattern**: `nip##/` directories contain event classes, tags, and utilities for that NIP. + +**Find implementations**: Use `scripts/nip-lookup.sh ` or see `references/nip-catalog.md`. + +## Event Anatomy + +### Core Structure + +```kotlin +@Immutable +open class Event( + val id: HexKey, // SHA-256 hash of serialized event + val pubKey: HexKey, // Author's public key (32 bytes hex) + val createdAt: Long, // Unix timestamp + val kind: Kind, // Event kind (Int typealias) + val tags: TagArray, // Array of tag arrays + val content: String, // Event content + val sig: HexKey, // Schnorr signature (64 bytes hex) +) : IEvent +``` + +**Key insight**: `Event` is the base class. Specific event types (TextNoteEvent, ReactionEvent) extend it and add parsing/helper methods. + +### Kind Classification + +```kotlin +typealias Kind = Int + +fun Kind.isEphemeral() = this in 20000..29999 // Not stored +fun Kind.isReplaceable() = this == 0 || this == 3 || this in 10000..19999 +fun Kind.isAddressable() = this in 30000..39999 // Replaceable + has d-tag +fun Kind.isRegular() = this in 1000..9999 // Stored, not replaced +``` + +**Pattern**: Kind determines event lifecycle and replaceability. + +## Creating Events + +### EventTemplate Pattern + +```kotlin +fun eventTemplate( + kind: Kind, + content: String, + tags: TagArray = emptyArray() +): EventTemplate +``` + +**Usage**: +```kotlin +val template = eventTemplate( + kind = 1, // Text note + content = "Hello Nostr!", + tags = tagArray { + add(arrayOf("subject", "Greeting")) + } +) + +// Sign with a signer +val signedEvent = signer.sign(template) +``` + +**Why templates?** Separates event data from signing. Templates can be signed by different signers (local keys, remote signers, hardware wallets). + +### TagArrayBuilder DSL + +```kotlin +fun tagArray( + initializer: TagArrayBuilder.() -> Unit +): TagArray +``` + +**Methods**: +- `add(tag)` - Append tag +- `addFirst(tag)` - Prepend tag (for ordering) +- `addUnique(tag)` - Replace all tags with this name +- `remove(tagName)` - Remove by name +- `addAll(tags)` - Bulk add + +**Example**: +```kotlin +val tags = tagArray { + add(arrayOf("e", replyToEventId, "", "reply")) + add(arrayOf("p", authorPubkey)) + addUnique(arrayOf("subject", "Re: Hello")) + add(arrayOf("content-warning", "spoilers")) +} +``` + +**Pattern**: Fluent DSL for building tag arrays with validation and deduplication. + +## Common Event Types + +### TextNoteEvent (kind 1) + +```kotlin +class TextNoteEvent : BaseThreadedEvent +``` + +**Creating**: +```kotlin +val note = eventTemplate( + kind = 1, + content = "Hello world!", + tags = tagArray { + add(arrayOf("subject", "First post")) + } +) +``` + +**Parsing**: +```kotlin +val event: TextNoteEvent = ... +val subject = event.subject() // Extension from nip14Subject +val mentions = event.mentions() // List of p-tags +val quotedEvents = event.quotes() // List of q-tags +``` + +### ReactionEvent (kind 7) + +```kotlin +fun createReaction( + targetEvent: Event, + emoji: String = "+" +): EventTemplate { + return eventTemplate( + kind = 7, + content = emoji, + tags = tagArray { + add(arrayOf("e", targetEvent.id)) + add(arrayOf("p", targetEvent.pubKey)) + } + ) +} +``` + +### MetadataEvent (kind 0) + +```kotlin +data class UserMetadata( + val name: String?, + val displayName: String?, + val picture: String?, + val banner: String?, + val about: String?, + // ... more fields +) + +fun createMetadata(metadata: UserMetadata): EventTemplate { + return eventTemplate( + kind = 0, + content = metadata.toJson() // Serialize to JSON + ) +} +``` + +### Addressable Events (kinds 30000-40000) + +```kotlin +fun createArticle( + slug: String, + title: String, + content: String +): EventTemplate { + return eventTemplate( + kind = 30023, + content = content, + tags = tagArray { + addUnique(arrayOf("d", slug)) // Unique identifier + add(arrayOf("title", title)) + add(arrayOf("published_at", "${TimeUtils.now()}")) + } + ) +} +``` + +**Key**: `d-tag` makes it addressable. Events with same kind + pubkey + d-tag replace each other. + +## Tag Patterns + +Tags are `Array` with pattern `[name, value, ...optionalParams]`. + +### Core Tags + +**e-tag** (event reference): +```kotlin +add(arrayOf("e", eventId, relayHint, marker)) +// marker: "reply", "root", "mention" +``` + +**p-tag** (pubkey reference): +```kotlin +add(arrayOf("p", pubkey, relayHint)) +``` + +**a-tag** (addressable event): +```kotlin +add(arrayOf("a", "$kind:$pubkey:$dtag", relayHint)) +``` + +**d-tag** (identifier for addressable events): +```kotlin +addUnique(arrayOf("d", "unique-slug")) +``` + +### Tag Extensions + +```kotlin +// Find tags +event.tags.tagValue("subject") // First subject tag value +event.tags.allTags("p") // All p-tags +event.tags.tagValues("e") // All e-tag values + +// Parse structured tags +event.tags.mapNotNull(ETag::parse) // Parse as ETag objects +``` + +For comprehensive tag patterns, see `references/tag-patterns.md`. + +## Threading (NIP-10) + +```kotlin +fun createReply( + original: TextNoteEvent, + content: String +): EventTemplate { + return eventTemplate( + kind = 1, + content = content, + tags = tagArray { + // Reply marker + add(arrayOf("e", original.id, "", "reply")) + + // Root marker (original's root, or original itself) + original.rootEvent()?.let { + add(arrayOf("e", it.id, "", "root")) + } ?: add(arrayOf("e", original.id, "", "root")) + + // Tag author + add(arrayOf("p", original.pubKey)) + + // Tag all mentioned users + original.mentions().forEach { + add(arrayOf("p", it)) + } + } + ) +} +``` + +**Pattern**: `reply` and `root` markers establish thread hierarchy. + +## Cryptography + +### Signing (secp256k1) + +```kotlin +interface ISigner { + suspend fun sign(template: EventTemplate): Event +} + +// Local key signing +class LocalSigner(private val privateKey: ByteArray) : ISigner { + override suspend fun sign(template: EventTemplate): Event { + val id = template.generateId() + val sig = Secp256k1.sign(id, privateKey) + return Event(id, pubKey, createdAt, kind, tags, content, sig) + } +} +``` + +**Pattern**: Signers abstract key management. Can be local, remote (NIP-46), or hardware. + +### Encryption (NIP-44) + +```kotlin +// Modern encryption (ChaCha20-Poly1305) +object Nip44v2 { + fun encrypt(plaintext: String, privateKey: ByteArray, pubKey: HexKey): String + fun decrypt(ciphertext: String, privateKey: ByteArray, pubKey: HexKey): String +} + +// Usage +val encrypted = Nip44v2.encrypt( + plaintext = "Secret message", + privateKey = myPrivateKey, + pubKey = recipientPubKey +) + +val decrypted = Nip44v2.decrypt( + ciphertext = encrypted, + privateKey = myPrivateKey, + pubKey = senderPubKey +) +``` + +**Pattern**: Elliptic curve Diffie-Hellman + ChaCha20-Poly1305 AEAD. + +### NIP-04 (Deprecated) + +```kotlin +// Legacy encryption (NIP-04, deprecated for NIP-44) +object Nip04 { + fun encrypt(msg: String, privateKey: ByteArray, pubKey: HexKey): String + fun decrypt(msg: String, privateKey: ByteArray, pubKey: HexKey): String +} +``` + +**Note**: Use NIP-44 (Nip44v2) for new implementations. NIP-04 has security issues. + +## Bech32 Encoding (NIP-19) + +```kotlin +object Nip19 { + // Encode + fun npubEncode(pubkey: HexKey): String // npub1... + fun nsecEncode(privateKey: ByteArray): String // nsec1... + fun noteEncode(eventId: HexKey): String // note1... + fun neventEncode(eventId: HexKey, relays: List = emptyList()): String + fun nprofileEncode(pubkey: HexKey, relays: List = emptyList()): String + fun naddrEncode(kind: Int, pubkey: HexKey, dTag: String, relays: List = emptyList()): String + + // Decode + fun decode(bech32: String): Nip19Result +} + +sealed class Nip19Result { + data class NPub(val hex: HexKey) : Nip19Result() + data class NSec(val hex: HexKey) : Nip19Result() + data class Note(val hex: HexKey) : Nip19Result() + data class NEvent(val hex: HexKey, val relays: List) : Nip19Result() + data class NProfile(val hex: HexKey, val relays: List) : Nip19Result() + data class NAddr(val kind: Int, val pubkey: HexKey, val dTag: String, val relays: List) : Nip19Result() +} +``` + +**Usage**: +```kotlin +// Encode +val npub = Nip19.npubEncode(pubkeyHex) +// Output: "npub1..." + +// Decode +when (val result = Nip19.decode(npub)) { + is Nip19Result.NPub -> println("Pubkey: ${result.hex}") + is Nip19Result.NEvent -> println("Event: ${result.hex}, relays: ${result.relays}") + else -> println("Other type") +} +``` + +## Event Validation + +```kotlin +fun Event.verify(): Boolean { + // 1. Verify ID matches content hash + val computedId = generateId() + if (id != computedId) return false + + // 2. Verify signature + return Secp256k1.verify(id, sig, pubKey) +} + +fun Event.generateId(): HexKey { + val serialized = serializeForId() // JSON array format + return sha256(serialized) +} +``` + +**Pattern**: Always verify events from untrusted sources (relays). + +## Common Workflows + +### Publishing an Event + +```kotlin +suspend fun publishNote(content: String, signer: ISigner, relays: List) { + // 1. Create template + val template = eventTemplate(kind = 1, content = content) + + // 2. Sign + val event = signer.sign(template) + + // 3. Verify (optional but recommended) + require(event.verify()) { "Signature verification failed" } + + // 4. Publish to relays + relays.forEach { relay -> + relayClient.send(relay, event) + } +} +``` + +### Querying Events + +```kotlin +// Subscription filter +data class Filter( + val ids: List? = null, + val authors: List? = null, + val kinds: List? = null, + val since: Long? = null, + val until: Long? = null, + val limit: Int? = null, + val tags: Map>? = null // e.g., {"#e": [eventId], "#p": [pubkey]} +) + +// Usage +val filter = Filter( + authors = listOf(userPubkey), + kinds = listOf(1), // Text notes only + limit = 50 +) + +relayClient.subscribe(relay, filter) { event -> + // Handle incoming events +} +``` + +### Creating a Zap (NIP-57) + +```kotlin +fun createZapRequest( + targetEvent: Event, + amountSats: Long, + comment: String = "" +): EventTemplate { + return eventTemplate( + kind = 9734, // Zap request + content = comment, + tags = tagArray { + add(arrayOf("e", targetEvent.id)) + add(arrayOf("p", targetEvent.pubKey)) + add(arrayOf("amount", "${amountSats * 1000}")) // millisats + add(arrayOf("relays", "wss://relay1.com", "wss://relay2.com")) + } + ) +} +``` + +### Gift-Wrapped DMs (NIP-17) + +```kotlin +fun createGiftWrappedDM( + recipientPubkey: HexKey, + message: String, + signer: ISigner +): Event { + // 1. Create sealed gossip (kind 14) + val sealedGossip = createSealedGossip(message, recipientPubkey, signer) + + // 2. Wrap in gift wrap (kind 1059) + return createGiftWrap(sealedGossip, recipientPubkey, signer) +} +``` + +**Pattern**: Double encryption + random ephemeral keys for metadata protection. + +## Finding NIPs + +Use the bundled script: + +```bash +# Find by NIP number +scripts/nip-lookup.sh 44 + +# Search by term +scripts/nip-lookup.sh encryption +scripts/nip-lookup.sh "gift wrap" +``` + +Or see `references/nip-catalog.md` for complete catalog. + +## Bundled Resources + +- **references/nip-catalog.md** - All 57 NIPs with package locations and key files +- **references/event-hierarchy.md** - Event class hierarchy, kind classifications, common types +- **references/tag-patterns.md** - Tag structure, TagArrayBuilder DSL, common tag types, parsing patterns +- **scripts/nip-lookup.sh** - Find NIP implementations by number or search term + +## Quick Reference + +| Task | Pattern | Location | +|------|---------|----------| +| Create event | `eventTemplate(kind, content, tags)` | nip01Core/signers/ | +| Build tags | `tagArray { add(...) }` | nip01Core/core/ | +| Sign event | `signer.sign(template)` | nip01Core/signers/ | +| Verify signature | `event.verify()` | nip01Core/core/ | +| Encrypt (NIP-44) | `Nip44v2.encrypt(...)` | nip44Encryption/ | +| Bech32 encode | `Nip19.npubEncode(...)` | nip19Bech32/ | +| Find NIP | `scripts/nip-lookup.sh ` | - | + +## Common Event Kinds + +| Kind | Type | NIP | Package | +|------|------|-----|---------| +| 0 | Metadata | 01 | nip01Core/ | +| 1 | Text note | 01, 10 | nip10Notes/ | +| 3 | Contact list | 02 | nip02FollowList/ | +| 5 | Deletion | 09 | nip09Deletions/ | +| 7 | Reaction | 25 | nip25Reactions/ | +| 1059 | Gift wrap | 59 | nip59Giftwrap/ | +| 9734 | Zap request | 57 | nip57Zaps/ | +| 9735 | Zap receipt | 57 | nip57Zaps/ | +| 10002 | Relay list | 65 | nip65RelayList/ | +| 30023 | Long-form content | 23 | nip23LongContent/ | + +## Related Skills + +- **nostr-protocol** - NIP specifications and protocol details +- **kotlin-expert** - Kotlin patterns (@Immutable, sealed classes, DSLs) +- **kotlin-coroutines** - Async patterns for relay communication +- **kotlin-multiplatform** - KMP patterns, expect/actual in Quartz diff --git a/.claude/skills/nostr-expert/references/event-hierarchy.md b/.claude/skills/nostr-expert/references/event-hierarchy.md new file mode 100644 index 000000000..1ca0c0fa1 --- /dev/null +++ b/.claude/skills/nostr-expert/references/event-hierarchy.md @@ -0,0 +1,293 @@ +# Event Hierarchy & Structure + +## Core Hierarchy + +``` +IEvent (empty interface) + └── Event (@Immutable base class) + ├── BaseAddressableEvent (replaceable + addressable, has d-tag) + │ ├── BaseReplaceableEvent (kinds 10000-20000, FIXED_D_TAG = "") + │ └── [Specific addressable events - 30000-40000] + └── [Specific event implementations - all other kinds] +``` + +## Event Base Class + +**Location**: `/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Event.kt` + +```kotlin +@Immutable +open class Event( + val id: HexKey, // SHA-256 hash of serialized event + val pubKey: HexKey, // Author's public key (32 bytes hex) + val createdAt: Long, // Unix timestamp + val kind: Kind, // Event kind (Int typealias) + val tags: TagArray, // Array of tag arrays + val content: String, // Event content + val sig: HexKey, // schnorr signature (64 bytes hex) +) : IEvent, OptimizedSerializable +``` + +## Kind Classification + +```kotlin +typealias Kind = Int + +fun Kind.isEphemeral() = this in 20000..29999 +fun Kind.isReplaceable() = this == 0 || this == 3 || this in 10000..19999 +fun Kind.isAddressable() = this in 30000..39999 +fun Kind.isRegular() = this in 1000..9999 +``` + +## Common Event Types + +### Text Note (kind 1) +```kotlin +class TextNoteEvent(...) : BaseThreadedEvent(...), + EventHintProvider, AddressHintProvider, PubKeyHintProvider, SearchableEvent + +// Threading support via markers: reply, root, mention +fun replyTo(): List // Direct reply targets +fun root(): Note? // Root of thread +``` + +### Metadata (kind 0) +```kotlin +class MetadataEvent(...) : BaseAddressableEvent(...) + +// Replaceable: newest version overwrites old +// d-tag automatically set to "" for kind 0 +fun name(): String? +fun displayName(): String? +fun picture(): String? +fun about(): String? +fun lnAddress(): String? +``` + +### Reaction (kind 7) +```kotlin +class ReactionEvent(...) : Event(...) + +companion object { + const val LIKE = "+" + const val DISLIKE = "-" + + fun like(reactedTo: EventHintBundle, ...) + fun dislike(reactedTo: EventHintBundle, ...) +} +``` + +### Zap Request/Receipt (kinds 9734, 9735) +```kotlin +class LnZapRequestEvent(...) : Event(...) + // Created by client, sent to Lightning Address + +class LnZapEvent(...) : Event(...) + // Receipt from LSP, contains bolt11 + embedded zap request + val zapRequest: LnZapRequestEvent? by lazy { containedPost() } + val amount: BigDecimal? by lazy { /* parse from bolt11 */ } +``` + +### Long-Form Content (kind 30023) +```kotlin +class LongTextNoteEvent(...) : BaseAddressableEvent(...) + // Blog posts, articles + // Addressable via kind:pubkey:d-tag +``` + +### Lists (kinds 10000-30004) +```kotlin +sealed class PeopleListEvent : BaseAddressableEvent { + object MuteList : PeopleListEvent(10000) + object PinList : PeopleListEvent(10001) + object BookmarkList : PeopleListEvent(10003) + // ... 18 list types total +} +``` + +## Event Interfaces + +### Hint Providers +Events can implement interfaces to optimize relay queries: + +```kotlin +interface EventHintProvider { + fun taggedEventIds(): Set + fun taggedEventRelays(): Map> +} + +interface PubKeyHintProvider { + fun taggedPubKeys(): Set + fun taggedPubKeyRelays(): Map> +} + +interface AddressHintProvider { + fun taggedAddresses(): Set
+ fun taggedAddressRelays(): Map> +} + +interface SearchableEvent { + fun subject(): String? + fun isContentEncoded(): Boolean +} +``` + +## Event Building Pattern + +### DSL Builder +```kotlin +TextNoteEvent.build( + note = "Hello Nostr", + replyingTo = eventBundle, + createdAt = TimeUtils.now() +) { + pTag(pubKey, relayHint) // Tag person + eTag(eventId, relayHint, "reply") // Tag event with marker + hashtag("nostr") // Add hashtag + alt("A short note") // Alt text +} +``` + +### Event Template (Low-level) +```kotlin +suspend fun eventTemplate( + kind: Kind, + content: String, + createdAt: Long, + initializer: TagArrayBuilder.() -> Unit +): EventTemplate { + val tags = TagArrayBuilder().apply(initializer).build() + return EventTemplate(kind, tags, content, createdAt) +} + +// Sign with signer +val template = eventTemplate(1, "Hello", now()) { pTag(pubkey) } +val signedEvent = signer.sign(template) +``` + +## Addressable vs Regular Events + +| Feature | Regular Event | Addressable Event | +|---------|---------------|-------------------| +| **Identifier** | Event ID (SHA-256 hash) | Address (kind:pubkey:d-tag) | +| **Replaceability** | Immutable | Newest replaces old | +| **d-tag** | Optional | Required | +| **Lookup** | By event ID | By address | +| **Example** | Text note (kind 1) | Metadata (kind 0), Long-form (kind 30023) | + +```kotlin +// Regular event address +note = LocalCache.getNoteIfExists(eventId) + +// Addressable event address +address = Address(kind = 30023, pubkey = authorHex, dTag = "my-article") +note = LocalCache.getAddressableNoteIfExists(address) +``` + +## Event Validation + +```kotlin +// Verify event ID matches computed hash +fun Event.verifyId(): Boolean = + EventHasher.hashIdCheck(id, pubKey, createdAt, kind, tags, content) + +// Verify signature +fun Event.verifySignature(): Boolean = + Nip01.verify(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey)) + +// Complete verification +fun Event.checkSignature() { + if (!verifyId()) throw Exception("ID mismatch") + if (!verifySignature()) throw Exception("Bad signature!") +} +``` + +## Event Serialization + +```kotlin +// To JSON (for transmission/signing) +fun Event.toJson(): String = OptimizedJsonMapper.toJson(this) + +// From JSON +fun Event.fromJson(json: String): Event = OptimizedJsonMapper.fromJson(json) + +// Event ID generation (SHA-256 of canonical JSON) +fun EventHasher.hashId( + pubKey: HexKey, + createdAt: Long, + kind: Kind, + tags: TagArray, + content: String +): HexKey { + val serialized = """[0,"$pubKey",$createdAt,$kind,${tags.toJson()},"$content"]""" + return sha256(serialized.encodeToByteArray()).toHexKey() +} +``` + +## Event Lifecycle in LocalCache + +``` +Event received from relay + ↓ +LocalCache.consume(event, relay, wasVerified) + ↓ +getOrCreateNote(event.id) or getOrCreateAddressableNote(address) + ↓ +justVerify(event) → checkSignature() + ↓ +note.loadEvent(event, author, replyTo) + ↓ +Update indices (replies, reactions, boosts) + ↓ +refreshNewNoteObservers(note) → emit to SharedFlow + ↓ +UI updates +``` + +## Common Event Patterns + +### Reply Threading +```kotlin +// Root event (top of thread) +val rootEvent = TextNoteEvent.build("Thread root") { } + +// Reply to root +val reply1 = TextNoteEvent.build("First reply", replyingTo = rootEvent) { + // Automatically adds: + // ["e", , , "root"] + // ["e", , , "reply"] +} + +// Reply to reply (nested) +val reply2 = TextNoteEvent.build("Nested reply", replyingTo = reply1) { + // Automatically adds: + // ["e", , , "root"] + // ["e", , , "reply"] +} +``` + +### Replaceable Events +```kotlin +// Metadata update (kind 0) - newest wins +val metadata1 = MetadataEvent.createNew(name = "Alice", picture = "url1") +Thread.sleep(1000) +val metadata2 = MetadataEvent.createNew(name = "Alice Updated", picture = "url2") + +// LocalCache keeps only metadata2 (higher createdAt) +``` + +### Event Deletion +```kotlin +// Delete events +val deletion = DeletionEvent.create( + deleteEvents = listOf(eventId1, eventId2), + reason = "Spam", + signer = signer +) + +// LocalCache marks events as deleted, but doesn't remove (for verification) +``` + +## 63+ Event Classes + +Full list at `/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip*/` - one class per event type across 60+ NIP implementations. diff --git a/.claude/skills/nostr-expert/references/nip-catalog.md b/.claude/skills/nostr-expert/references/nip-catalog.md new file mode 100644 index 000000000..950f2cb1a --- /dev/null +++ b/.claude/skills/nostr-expert/references/nip-catalog.md @@ -0,0 +1,179 @@ +# NIP Catalog: 60 Standard + 8 Experimental NIPs in Quartz + +## Standard NIPs by Category + +### Core/Basic Protocol +| NIP | Directory | Key Files | Description | +|-----|-----------|-----------|-------------| +| 01 | `nip01Core/` | Event.kt, Kind.kt, Tag.kt | Core protocol, event structure, kinds, tags | +| 02 | `nip02FollowList/` | ContactListEvent.kt | Follow/contact lists (kind 3) | +| 03 | `nip03Timestamp/` | OpenTimestampsAttestation.kt | Timestamps | +| 04 | `nip04Dm/` | EncryptedDmEvent.kt | Legacy encrypted DMs (deprecated for NIP-17) | +| 05 | `nip05DnsIdentifiers/` | Nip05Verifier.kt | DNS-based verification | +| 06 | `nip06KeyDerivation/` | Mnemonic-related | BIP-39 key derivation | +| 09 | `nip09Deletions/` | DeletionEvent.kt | Event deletion requests (kind 5) | +| 11 | `nip11RelayInfo/` | RelayInformation.kt | Relay metadata | +| 13 | `nip13Pow/` | ProofOfWork.kt | Proof of work | +| 14 | `nip14Subject/` | Subject tags | Subject tags for text notes | +| 17 | `nip17Dm/` | GiftWrapEvent.kt, SealedGossipEvent.kt | Private DMs (replacem + +ent for NIP-04) | +| 21 | `nip21UriScheme/` | URI scheme (`nostr:`) | URI scheme parsing | +| 42 | `nip42RelayAuth/` | RelayAuthEvent.kt | Relay authentication (kind 22242) | +| 44 | `nip44Encryption/` | Nip44.kt, Nip44v2.kt | Modern encryption (ChaCha20) | +| 49 | `nip49PrivKeyEnc/` | NIP-49Ncryptsec.kt | Private key encryption format | + +### Content Types +| NIP | Directory | Key Files | Description | +|-----|-----------|-----------|-------------| +| 10 | `nip10Notes/` | TextNoteEvent.kt | Text notes with threading (kind 1) | +| 18 | `nip18Reposts/` | RepostEvent.kt, GenericRepostEvent.kt | Reposts (kind 6, 16) | +| 22 | `nip22Comments/` | CommentEvent.kt | Comments (kind 1111) | +| 23 | `nip23LongContent/` | LongTextNoteEvent.kt | Long-form content (kind 30023) | +| 25 | `nip25Reactions/` | ReactionEvent.kt | Reactions (kind 7) | +| 31 | `nip31Alts/` | Alt tags | Alt description tags | +| 36 | `nip36SensitiveContent/` | Content warnings | Content warning tags | +| 37 | `nip37Drafts/` | DraftEvent.kt | Drafts (kind 31234) | +| 50 | `nip50Search/` | Search filters | Full-text search | + +### Encoding & Standards +| NIP | Directory | Key Files | Description | +|-----|-----------|-----------|-------------| +| 19 | `nip19Bech32/` | Nip19.kt | Bech32 encoding (npub, nsec, note, nevent, nprofile, naddr) | +| 40 | `nip40Expiration/` | Expiration tags | Event expiration | +| 48 | `nip48ProxyTags/` | Proxy tags | Proxy tags for delegation | +| 62 | `nip62RequestToVanish/` | RequestToVanishEvent.kt | Request to vanish (kind 12) | +| 98 | `nip98HttpAuth/` | HTTP authorization | HTTP auth header | + +### Lists & Management +| NIP | Directory | Key Files | Description | +|-----|-----------|-----------|-------------| +| 51 | `nip51Lists/` | 18 list types | Named lists (mute, bookmarks, pins, communities, etc.) (kinds 10000-30004) | +| 65 | `nip65RelayList/` | AdvertisedRelayListEvent.kt | Relay lists (kind 10002) | + +### Social & Identity +| NIP | Directory | Key Files | Description | +|-----|-----------|-----------|-------------| +| 39 | `nip39ExtIdentities/` | External identities | External identity claims | +| 46 | `nip46RemoteSigner/` | NostrConnectEvent.kt | Remote signer protocol (bunker) | +| 47 | `nip47WalletConnect/` | Nostr Wallet Connect | Wallet connection protocol | +| 56 | `nip56Reports/` | ReportEvent.kt | Reports (kind 1984) | +| 57 | `nip57Zaps/` | LnZapEvent.kt, LnZapRequestEvent.kt | Lightning zaps (kinds 9734, 9735) | +| 58 | `nip58Badges/` | Badge events | Badge definitions & awards (kinds 30009, 8) | +| 59 | `nip59Giftwrap/` | GiftWrapEvent.kt | Gift-wrapped events for privacy | +| 75 | `nip75ZapGoals/` | ZapGoalEvent.kt | Zap goals (kind 9041) | + +### Specialized Content +| NIP | Directory | Key Files | Description | +|-----|-----------|-----------|-------------| +| 28 | `nip28PublicChat/` | ChannelCreateEvent.kt, ChannelMessageEvent.kt | Public chat channels (kinds 40-44) | +| 30 | `nip30CustomEmoji/` | EmojiUrl.kt | Custom emoji | +| 34 | `nip34Git/` | Git patch/issue events | Git repository tracking (kinds 30617, 30618, 1617, 1621, 1622, 1630, 1633) | +| 35 | `nip35Torrents/` | Torrent events | Torrent tracking | +| 52 | `nip52Calendar/` | Calendar events | Calendar time-based/date-based (kinds 31922-31925) | +| 53 | `nip53LiveActivities/` | LiveActivitiesEvent.kt | Live events/streaming (kind 30311) | +| 54 | `nip54Wiki/` | WikiNoteEvent.kt | Wiki pages (kind 30818) | +| 68 | `nip68Picture/` | Picture metadata | Picture metadata | +| 71 | `nip71Video/` | 7 video event types | Video events (kinds 34235, 35235, 1234, 1235) | +| 72 | `nip72ModCommunities/` | Community events | Moderated communities (kinds 34550, 34551, 9041) | +| 84 | `nip84Highlights/` | HighlightEvent.kt | Highlights (kind 9802) | +| 89 | `nip89AppHandlers/` | AppDefinitionEvent.kt | App recommendations (kinds 31990, 31989) | +| 90 | `nip90Dvms/` | DVM job events | Data Vending Machines (DVMs) (kinds 5000-7000) | +| 92 | `nip92IMeta/` | IMeta tags | Image metadata tags | +| 94 | `nip94FileMetadata/` | FileHeaderEvent.kt, FileStorageEvent.kt | File metadata (kind 1063) | +| 96 | `nip96FileStorage/` | HTTP file storage | HTTP-based file storage | +| 99 | `nip99Classifieds/` | ClassifiedsEvent.kt | Classifieds/marketplace (kind 30402) | +| A0 | `nipA0VoiceMessages/` | Voice messages | Voice message events | +| B7 | `nipB7Blossom/` | Blossom server URLs | Blossom file storage | + +### Web/Storage/Other +| NIP | Directory | Key Files | Description | +|-----|-----------|-----------|-------------| +| 38 | `nip38UserStatus/` | StatusEvent.kt | User status (kind 30315) | +| 60 | `nip60Payment/` | Wallet events | Wallet info (kind 13194) | +| 61 | `nip61PaymentRequest/` | Nut zaps | Cashu payment requests | +| 64 | `nip64Chess/` | Chess moves | Chess move events | +| 66 | `nip66Monitoring/` | Relay monitor events | Relay monitoring | +| 67 | `nip67Invoices/` | Invoice tags | Lightning invoice tags | +| 69 | `nip69Offers/` | BOLT-12 offers | BOLT-12 offer tags | +| 70 | `nip70ProtectedEvts/` | Protected events | Protected event types | +| 73 | `nip73ExternalIds/` | External content IDs | External content identifiers | +| 78 | `nip78AppData/` | AppDataEvent.kt | Application data (kind 30078) | +| 79 | `nip79Labels/` | Label events | Labeling (kinds 1985, 1986) | +| 80-88 | Various | Various protocols | Relationship, preferences, polls, surveys, social graphs, etc. | +| 91 | `nip91Feed/` | Feed display events | Feed definitions | +| 93 | `nip93Gallery/` | Gallery events | Gallery collections | +| 95 | `nip95Storage/` | Storage event tags | Storage events | +| 97 | `nip97Nests/` | Audio rooms | Audio room events | + +## Experimental NIPs (18 packages) + +Located at `/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/`: + +| Package | Description | +|---------|-------------| +| `audio/` | Audio content, track events | +| `bounties/` | Bounty/funding events | +| `decoupling/` | Decoupling setup | +| `edits/` | Event edit tracking | +| `ephemChat/` | Ephemeral encrypted chat | +| `forks/` | Fork tracking | +| `inlineMetadata/` | Inline metadata | +| `interactiveStories/` | Interactive story events | +| `limits/` | Limit enforcement | +| `medical/` | Medical data | +| `nip95/` | File storage support | +| `nipA3/` | A3 protocol extension | +| `nns/` | Nostr Name System | +| `profileGallery/` | Profile gallery lists | +| `publicMessages/` | Public message lists | +| `relationshipStatus/` | Relationship status events | +| `trustedAssertions/` | Trust/assertion events | +| `zapPolls/` | Zap-based polling | + +## Quick Lookup by Kind + +| Kind | Event Type | NIP | +|------|------------|-----| +| 0 | Metadata | 01 | +| 1 | Text Note | 01, 10 | +| 3 | Follow List | 02 | +| 4 | Encrypted DM (legacy) | 04 | +| 5 | Deletion | 09 | +| 6 | Repost | 18 | +| 7 | Reaction | 25 | +| 8 | Badge Award | 58 | +| 16 | Generic Repost | 18 | +| 40-44 | Channel Events | 28 | +| 1063 | File Metadata | 94 | +| 1111 | Comment | 22 | +| 1617, 1621, 1622, 1630, 1633 | Git | 34 | +| 1984 | Report | 56 | +| 1985, 1986 | Label | 79 | +| 9734 | Zap Request | 57 | +| 9735 | Zap Receipt | 57 | +| 9802 | Highlight | 84 | +| 10000-20000 | Replaceable Lists | 51 | +| 10002 | Relay List | 65 | +| 13194 | Wallet Info | 60 | +| 22242 | Relay Auth | 42 | +| 23194, 23195 | NWC Payment | 47 | +| 30000-40000 | Addressable Events | Various | +| 30009 | Badge Definition | 58 | +| 30023 | Long-Form Content | 23 | +| 30078 | App Data | 78 | +| 30311 | Live Event | 53 | +| 30315 | User Status | 38 | +| 30402 | Classifieds | 99 | +| 30818 | Wiki | 54 | +| 31234 | Draft | 37 | +| 31922-31925 | Calendar | 52 | +| 31989, 31990 | App Handlers | 89 | +| 34235, 34550-34551 | Video/Communities | 71, 72 | +| 5000-7000 | DVM Jobs | 90 | + +## File Location Pattern + +All NIPs located at: `/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip/` + +Example: NIP-57 → `/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/` diff --git a/.claude/skills/nostr-expert/references/tag-patterns.md b/.claude/skills/nostr-expert/references/tag-patterns.md new file mode 100644 index 000000000..9279a9c22 --- /dev/null +++ b/.claude/skills/nostr-expert/references/tag-patterns.md @@ -0,0 +1,336 @@ +# Tag Patterns in Quartz + +Tags are the primary way events reference other events, users, and metadata in Nostr. + +## Tag Structure + +```kotlin +typealias Tag = Array // ["tag_name", "value", "optional_param", ...] +typealias TagArray = Array +``` + +**Pattern**: `[name, value, ...optionalParams]` + +## TagArrayBuilder DSL + +```kotlin +fun tagArray(initializer: TagArrayBuilder.() -> Unit): TagArray +``` + +**Methods**: +- `add(tag)` - Append tag +- `addFirst(tag)` - Prepend tag +- `addUnique(tag)` - Replace all tags with this name +- `remove(tagName)` - Remove all tags with name +- `removeIf(predicate, toCompare)` - Conditional removal + +**Example**: +```kotlin +val tags = tagArray { + add(arrayOf("e", eventId, relayHint, "reply")) + add(arrayOf("p", pubkey)) + addUnique(arrayOf("subject", "Hello")) +} +``` + +## Core Tag Types (NIP-01) + +### e-tag (Event Reference) +```kotlin +// ["e", , , ] +arrayOf("e", eventId, "wss://relay.damus.io", "reply") +``` + +**Markers** (NIP-10): +- `root` - Root of thread +- `reply` - Direct reply target +- `mention` - Mentioned event (not reply) + +**Extensions**: +```kotlin +// nip01Core/tags/ +fun TagArrayBuilder.eTag(eventId: HexKey, relay: String? = null, marker: String? = null) +``` + +### p-tag (Pubkey Reference) +```kotlin +// ["p", , ] +arrayOf("p", pubkey, "wss://relay.damus.io") +``` + +**Usage**: Tag users, indicate recipients + +**Extensions**: +```kotlin +fun TagArrayBuilder.pTag(pubkey: HexKey, relay: String? = null) +``` + +### a-tag (Addressable Event Reference) +```kotlin +// ["a", ::, ] +arrayOf("a", "30023:${authorPubkey}:${dtag}", "wss://relay.damus.io") +``` + +**Usage**: Reference replaceable/addressable events (kinds 10000-20000, 30000-40000) + +**Extensions**: +```kotlin +fun TagArrayBuilder.aTag(kind: Int, pubkey: HexKey, dTag: String, relay: String? = null) +``` + +### d-tag (Identifier) +```kotlin +// ["d", ] +arrayOf("d", "my-article-slug") +``` + +**Usage**: Unique identifier for addressable events + +## Common Tag Extensions + +### Subject (NIP-14) +```kotlin +// nip14Subject/ +fun Event.subject(): String? +fun TagArrayBuilder.subject(text: String) +``` + +### Content Warning (NIP-36) +```kotlin +// nip36SensitiveContent/ +fun Event.contentWarning(): String? +fun TagArrayBuilder.contentWarning(reason: String = "") +``` + +### Expiration (NIP-40) +```kotlin +// nip40Expiration/ +fun Event.expiration(): Long? +fun TagArrayBuilder.expiration(unixTimestamp: Long) +``` + +### Alt Description (NIP-31) +```kotlin +// nip31Alts/ +fun Event.alt(): String? +fun TagArrayBuilder.alt(description: String) +``` + +## Specialized Tags + +### Zap Tags (NIP-57) +```kotlin +// nip57Zaps/tags/ +class BoltTag(val bolt11: String, val preimage: String?) +class DescriptionTag(val zapRequestJson: String) +``` + +### Imeta Tags (NIP-92) +```kotlin +// nip92IMeta/ +class IMetaTag(val url: String, val metadata: Map) + +// Usage: Image metadata +IMetaTag("https://example.com/image.jpg", mapOf( + "m" to "image/jpeg", + "dim" to "1920x1080", + "blurhash" to "..." +)) +``` + +### Relay Tags (NIP-65) +```kotlin +// nip65RelayList/ +class RelayTag(val url: String, val type: RelayType) +enum class RelayType { READ, WRITE, BOTH } +``` + +## Tag Query Patterns + +### Finding Tags +```kotlin +// Extension functions on TagArray +fun TagArray.firstTag(name: String): Tag? +fun TagArray.allTags(name: String): List +fun TagArray.tagValue(name: String): String? +fun TagArray.tagValues(name: String): List +``` + +**Example**: +```kotlin +val event: TextNoteEvent = ... +val subject = event.tags.tagValue("subject") +val mentions = event.tags.allTags("p").mapNotNull { it.getOrNull(1) } +``` + +### Parsing Tags +```kotlin +// Pattern: Companion object with parse methods +object ETag { + fun parse(tag: Tag): ETag? { + if (tag.getOrNull(0) != "e") return null + return ETag( + eventId = tag.getOrNull(1) ?: return null, + relay = tag.getOrNull(2), + marker = tag.getOrNull(3) + ) + } +} + +// Usage +val eTags = event.tags.mapNotNull(ETag::parse) +``` + +## Event Builder Pattern + +Combining TagArrayBuilder with event creation: + +```kotlin +fun createTextNote(content: String, replyTo: Event?): EventTemplate { + return eventTemplate( + kind = 1, + content = content, + tags = tagArray { + replyTo?.let { + eTag(it.id, marker = "reply") + pTag(it.pubKey) + it.rootEvent()?.let { root -> + eTag(root.id, marker = "root") + } + } + } + ) +} +``` + +## Hint System + +Tags can provide "hints" - optional relay URLs for fetching referenced content: + +```kotlin +// Event references +["e", eventId, "wss://relay.example.com"] // relay hint + +// Pubkey references +["p", pubkey, "wss://relay.example.com"] // relay hint + +// Addressable references +["a", "30023:pubkey:dtag", "wss://relay.example.com"] // relay hint +``` + +**Pattern**: Third parameter (index 2) is always the relay hint + +## Tag Validation + +```kotlin +// Common validations +fun validateETag(tag: Tag): Boolean { + return tag.getOrNull(0) == "e" && tag.getOrNull(1)?.isValidHex() == true +} + +fun validatePTag(tag: Tag): Boolean { + return tag.getOrNull(0) == "p" && tag.getOrNull(1)?.isValidHex() == true +} +``` + +## Performance Patterns + +### Tag Indexing +```kotlin +// TagArrayBuilder keeps an index by tag name +private val tagList = mutableMapOf>() + +// Fast lookup by name +fun remove(tagName: String) { + tagList.remove(tagName) +} +``` + +### Lazy Parsing +```kotlin +// Don't parse all tags upfront +class TextNoteEvent(...) { + private val _mentions by lazy { + tags.mapNotNull(PTag::parse) + } + + fun mentions() = _mentions +} +``` + +## Common Workflows + +### Creating a Reply +```kotlin +fun replyTo(original: TextNoteEvent, content: String): EventTemplate { + return eventTemplate( + kind = 1, + content = content, + tags = tagArray { + // Reply to this event + eTag(original.id, marker = "reply") + + // Copy root marker if exists, or mark original as root + original.rootEvent()?.let { + eTag(it.id, marker = "root") + } ?: eTag(original.id, marker = "root") + + // Tag author + pTag(original.pubKey) + + // Tag all mentioned users + original.mentions().forEach { pTag(it) } + } + ) +} +``` + +### Creating a Reaction +```kotlin +fun createReaction(targetEvent: Event, emoji: String): EventTemplate { + return eventTemplate( + kind = 7, + content = emoji, + tags = tagArray { + eTag(targetEvent.id) + pTag(targetEvent.pubKey) + } + ) +} +``` + +### Creating an Addressable Event +```kotlin +fun createArticle(title: String, content: String, slug: String): EventTemplate { + return eventTemplate( + kind = 30023, + content = content, + tags = tagArray { + addUnique(arrayOf("d", slug)) // Unique identifier + add(arrayOf("title", title)) + add(arrayOf("published_at", "${TimeUtils.now()}")) + } + ) +} +``` + +## Quick Reference + +| Tag | NIP | Usage | Example | +|-----|-----|-------|---------| +| e | 01 | Event reference | `["e", eventId, relay, marker]` | +| p | 01 | Pubkey reference | `["p", pubkey, relay]` | +| a | 01 | Addressable event | `["a", "kind:pubkey:d"]` | +| d | 01 | Identifier | `["d", "unique-id"]` | +| subject | 14 | Subject line | `["subject", "Hello"]` | +| content-warning | 36 | Content warning | `["content-warning", "nsfw"]` | +| expiration | 40 | Expiration time | `["expiration", "1234567890"]` | +| bolt11 | 57 | Lightning invoice | `["bolt11", "lnbc..."]` | +| imeta | 92 | Media metadata | `["imeta", "url", "m", "image/jpeg"]` | +| relay | 65 | User relays | `["relay", "wss://...", "read"]` | + +## Resources + +- Tag builders: `quartz/src/commonMain/.../nip01Core/tags/` +- Tag extensions: Look for `TagArrayExt.kt`, `TagArrayBuilderExt.kt` in each NIP package +- Event parsing: Each event class has tag parsing methods diff --git a/.claude/skills/nostr-expert/scripts/nip-lookup.sh b/.claude/skills/nostr-expert/scripts/nip-lookup.sh new file mode 100755 index 000000000..1270bc318 --- /dev/null +++ b/.claude/skills/nostr-expert/scripts/nip-lookup.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# Find NIP implementation files by NIP number or search term + +set -e + +QUARTZ_PATH="${QUARTZ_PATH:-./quartz/src/commonMain/kotlin/com/vitorpamplona/quartz}" + +if [ $# -eq 0 ]; then + echo "Usage: $0 " + echo "" + echo "Examples:" + echo " $0 01 # Find NIP-01 files" + echo " $0 44 # Find NIP-44 files" + echo " $0 encryption # Search for 'encryption' in NIP packages" + echo "" + echo "Set QUARTZ_PATH to override default quartz location" + exit 1 +fi + +SEARCH_TERM="$1" + +# Check if it's a number (NIP number) +if [[ "$SEARCH_TERM" =~ ^[0-9]+$ ]]; then + # Pad to 2 digits + NIP_NUM=$(printf "%02d" "$SEARCH_TERM") + echo "Searching for NIP-$NIP_NUM implementation..." + echo "================================================" + echo "" + + # Find directories matching nip##* + find "$QUARTZ_PATH" -type d -name "nip${NIP_NUM}*" | while read -r dir; do + echo "📁 $(basename "$dir")/" + find "$dir" -name "*.kt" -type f | while read -r file; do + rel_path="${file#$QUARTZ_PATH/}" + echo " └─ $rel_path" + done + echo "" + done +else + # Text search + echo "Searching for '$SEARCH_TERM' in NIP packages..." + echo "================================================" + echo "" + + find "$QUARTZ_PATH" -type d -name "nip*" | while read -r dir; do + if grep -r -l -i "$SEARCH_TERM" "$dir" --include="*.kt" 2>/dev/null | head -1 > /dev/null; then + echo "📁 $(basename "$dir")/" + grep -r -l -i "$SEARCH_TERM" "$dir" --include="*.kt" 2>/dev/null | while read -r file; do + rel_path="${file#$QUARTZ_PATH/}" + matches=$(grep -c -i "$SEARCH_TERM" "$file" 2>/dev/null || echo "0") + echo " └─ $rel_path ($matches matches)" + done + echo "" + fi + done +fi + +echo "Done." From 2a8e7cbe94da4cc9d3d43bb4220cb1002a7cab51 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 30 Dec 2025 11:42:57 -0500 Subject: [PATCH 006/137] Fixes duplicated results --- .../nip01Core/store/sqlite/QueryAssemblerTest.kt | 16 +++++++++------- .../nip01Core/store/sqlite/EventIndexesModule.kt | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt index 2a3983a8d..fe740787c 100644 --- a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt @@ -84,12 +84,14 @@ class QueryAssemblerTest { """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT event_tags.event_header_row_id as row_id FROM event_tags INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE (event_headers.created_at >= "1750889190") AND (event_tags.tag_hash = "2657743813502222172") AND (event_headers.kind = "5") AND (event_headers.pubkey = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d") + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE (event_headers.created_at >= "1750889190") AND (event_tags.tag_hash = "2657743813502222172") AND (event_headers.kind = "5") AND (event_headers.pubkey = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d") ) AS filtered ON event_headers.row_id = filtered.row_id ORDER BY created_at DESC, id - ├── SEARCH event_tags USING COVERING INDEX query_by_tags_hash (tag_hash=?) - ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + ├── CO-ROUTINE filtered + │ ├── SEARCH event_tags USING COVERING INDEX query_by_tags_hash (tag_hash=?) + │ └── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + ├── SCAN filtered ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) └── USE TEMP B-TREE FOR ORDER BY """.trimIndent(), @@ -278,7 +280,7 @@ class QueryAssemblerTest { """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT event_tags.event_header_row_id as row_id FROM event_tags INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE event_tags.tag_hash = "-4551135004136952885" ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 30 + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE event_tags.tag_hash = "-4551135004136952885" ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 30 ) AS filtered ON event_headers.row_id = filtered.row_id ORDER BY created_at DESC, id @@ -313,7 +315,7 @@ class QueryAssemblerTest { """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT event_tags.event_header_row_id as row_id FROM event_tags INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_headers.kind = "3") ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 30 + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_headers.kind = "3") ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 30 ) AS filtered ON event_headers.row_id = filtered.row_id ORDER BY created_at DESC, id @@ -345,7 +347,7 @@ class QueryAssemblerTest { """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT event_tags.event_header_row_id as row_id FROM event_tags INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_headers.pubkey = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d") ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 30 + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_headers.pubkey = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d") ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 30 ) AS filtered ON event_headers.row_id = filtered.row_id ORDER BY created_at DESC, id @@ -381,7 +383,7 @@ class QueryAssemblerTest { """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT event_tags.event_header_row_id as row_id FROM event_tags INNER JOIN event_tags as event_tagst ON event_tagst.event_header_row_id = event_tags.event_header_row_id INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tagst.tag_hash = "-6379614208644810021") AND (event_headers.kind = "1") ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 30 + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_tags as event_tagst ON event_tagst.event_header_row_id = event_tags.event_header_row_id INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tagst.tag_hash = "-6379614208644810021") AND (event_headers.kind = "1") ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 30 ) AS filtered ON event_headers.row_id = filtered.row_id ORDER BY created_at DESC, id diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt index 10fcf8301..ccc1051aa 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt @@ -466,7 +466,7 @@ class EventIndexesModule( buildString { // always do tags if there are any if (nonDTags.isNotEmpty()) { - append("SELECT event_tags.event_header_row_id as row_id FROM event_tags ") + append("SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags ") // it's quite rare to have 2 tags in the filter, but possible nonDTags.keys.forEachIndexed { index, tagName -> From 333285747b7dbaf4adeb492e441e0de68e27bc31 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 30 Dec 2025 12:06:22 -0500 Subject: [PATCH 007/137] Migrates to using indexes instead tagNames to avoid non-ASCII chars reaching the DB --- .../quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt | 4 ++-- .../quartz/nip01Core/store/sqlite/EventIndexesModule.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt index fe740787c..c84fc1848 100644 --- a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt @@ -383,14 +383,14 @@ class QueryAssemblerTest { """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_tags as event_tagst ON event_tagst.event_header_row_id = event_tags.event_header_row_id INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tagst.tag_hash = "-6379614208644810021") AND (event_headers.kind = "1") ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 30 + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_tags as event_tagsIn1 ON event_tagsIn1.event_header_row_id = event_tags.event_header_row_id INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tagsIn1.tag_hash = "-6379614208644810021") AND (event_headers.kind = "1") ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 30 ) AS filtered ON event_headers.row_id = filtered.row_id ORDER BY created_at DESC, id ├── CO-ROUTINE filtered │ ├── SEARCH event_tags USING COVERING INDEX query_by_tags_hash (tag_hash=?) │ ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - │ ├── SEARCH event_tagst USING COVERING INDEX query_by_tags_hash (tag_hash=? AND event_header_row_id=?) + │ ├── SEARCH event_tagsIn1 USING COVERING INDEX query_by_tags_hash (tag_hash=? AND event_header_row_id=?) │ └── USE TEMP B-TREE FOR ORDER BY ├── SCAN filtered ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt index ccc1051aa..c2b28d739 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt @@ -471,7 +471,7 @@ class EventIndexesModule( // it's quite rare to have 2 tags in the filter, but possible nonDTags.keys.forEachIndexed { index, tagName -> if (index > 0) { - append("INNER JOIN event_tags as event_tags$tagName ON event_tags$tagName.event_header_row_id = event_tags.event_header_row_id ") + append("INNER JOIN event_tags as event_tagsIn$index ON event_tagsIn$index.event_header_row_id = event_tags.event_header_row_id ") } else { defaultTagKey = tagName } @@ -512,7 +512,7 @@ class EventIndexesModule( if (defaultTagKey == null || defaultTagKey == tagName) { "event_tags.tag_hash" } else { - "event_tags$tagName.tag_hash" + "event_tagsIn$index.tag_hash" } equalsOrIn( From df97b7c4447d615b14056effd6b24a1c5c0d351c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 30 Dec 2025 12:09:05 -0500 Subject: [PATCH 008/137] Improves efficiency of the filter class for matching and checking errors --- .../quartz/nip01Core/relay/filters/Filter.kt | 17 ++++++++++------- .../nip01Core/relay/filters/FilterMatcher.kt | 4 +++- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt index a34cd01fb..f4d1f5aad 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt @@ -89,14 +89,17 @@ class Filter( if (it.length != 64) Log.e("FilterError", "Invalid author length $it on ${toJson()}") } // tests common tags. - tags?.get("p")?.forEach { - if (it.length != 64) Log.e("FilterError", "Invalid p-tag length $it on ${toJson()}") + if (tags != null) { + tags["p"]?.forEach { + if (it.length != 64) Log.e("FilterError", "Invalid p-tag length $it on ${toJson()}") + } + tags["e"]?.forEach { + if (it.length != 64) Log.e("FilterError", "Invalid e-tag length $it on ${toJson()}") + } + tags["a"]?.forEach { + if (Address.parse(it) == null) Log.e("FilterError", "Invalid a-tag $it on ${toJson()}") + } } - tags?.get("e")?.forEach { - if (it.length != 64) Log.e("FilterError", "Invalid e-tag length $it on ${toJson()}") - } - tags?.get("a")?.forEach { - if (Address.parse(it) == null) Log.e("FilterError", "Invalid a-tag $it on ${toJson()}") } } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterMatcher.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterMatcher.kt index 1367984ee..7c7a579c2 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterMatcher.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterMatcher.kt @@ -36,7 +36,9 @@ object FilterMatcher { if (kinds?.contains(event.kind) == false) return false if (authors?.contains(event.pubKey) == false) return false tags?.forEach { tag -> - if (!event.tags.any { it.first() == tag.key && it[1] in tag.value }) return false + val valueSet = tag.value.toSet() + // AND between keys, OR between values + if (!event.tags.any { it.size > 1 && it[0] == tag.key && it[1] in valueSet }) return false } if (event.createdAt !in (since ?: Long.MIN_VALUE)..(until ?: Long.MAX_VALUE)) { return false From c3d741ebd14b4b00f86ec75a989b1d3d99fbc856 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 30 Dec 2025 12:29:04 -0500 Subject: [PATCH 009/137] Implements AND queries for tags: https://github.com/nostr-protocol/nips/pull/1365 --- .../store/sqlite/FilterMatcherTest.kt | 173 ++++++++++++++++++ .../store/sqlite/QueryAssemblerTest.kt | 23 +++ .../store/sqlite/EventIndexesModule.kt | 57 +++++- .../quartz/nip01Core/relay/filters/Filter.kt | 17 +- .../nip01Core/relay/filters/FilterMatcher.kt | 15 ++ .../relay/filters/FilterMatcherTest.kt | 149 +++++++++++++++ .../relay/filters/FilterDeserializer.kt | 13 +- .../relay/filters/FilterSerializer.kt | 10 + 8 files changed, 442 insertions(+), 15 deletions(-) create mode 100644 quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/FilterMatcherTest.kt create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterMatcherTest.kt diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/FilterMatcherTest.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/FilterMatcherTest.kt new file mode 100644 index 000000000..22f3b5c17 --- /dev/null +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/FilterMatcherTest.kt @@ -0,0 +1,173 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.store.sqlite + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import org.junit.After +import org.junit.Before +import org.junit.Test + +class FilterMatcherTest { + private lateinit var db: EventStore + + @Before + fun setup() { + val context = ApplicationProvider.getApplicationContext() + db = EventStore(context, null) + } + + @After + fun tearDown() { + db.close() + } + + val id = "98b574c3527f0ffb30b7271084e3f07480733c7289f8de424d29eae82e36c758" + val pubkey = "46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d" + val createdAt: Long = 1683596206 + val kind = 1 + + val pTag1 = "22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954" + val pTag2 = "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24" + val pTag3 = "ec4d241c334311b3a304433ee3442be29d0e88e7ec19b85edf2bba29b93565e2" + val pTag4 = "0fe0b18b4dbf0e0aa40fcd47209b2a49b3431fc453b460efcf45ca0bd16bd6ac" + val pTag5 = "8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168" + val pTag6 = "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed" + val pTag7 = "4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0" + val pTag8 = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c" + + val rootETag = "27ac621d7dc4a932e1a79f984308e7d20656dd6fddb2ce9cdfcb6a67b9a7bcc3" + val replyETag = "be7245af96210a0dd048cab4ad38e52dbd6c09a53ea21a7edb6be8898e5727cc" + + val note = + Event( + id, + pubkey, + createdAt, + kind, + arrayOf( + arrayOf("e", rootETag, "", "root"), + arrayOf("e", replyETag, "", "reply"), + arrayOf("p", pTag1), + arrayOf("p", pTag1), + arrayOf("p", pTag2), + arrayOf("p", pTag3), + arrayOf("p", pTag4), + arrayOf("p", pTag5), + arrayOf("p", pTag6), + arrayOf("p", pTag7), + arrayOf("p", pTag8), + ), + "Astral:\n\nhttps://void.cat/d/A5Fba5B1bcxwEmeyoD9nBs.webp\n\nIris:\n\nhttps://void.cat/d/44hTcVvhRps6xYYs99QsqA.webp\n\nSnort:\n\nhttps://void.cat/d/4nJD5TRePuQChM5tzteYbU.webp\n\nAmethyst agrees with Astral which I suspect are both wrong. nostr:npub13sx6fp3pxq5rl70x0kyfmunyzaa9pzt5utltjm0p8xqyafndv95q3saapa nostr:npub1v0lxxxxutpvrelsksy8cdhgfux9l6a42hsj2qzquu2zk7vc9qnkszrqj49 nostr:npub1g53mukxnjkcmr94fhryzkqutdz2ukq4ks0gvy5af25rgmwsl4ngq43drvk nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z ", + "4aa5264965018fa12a326686ad3d3bd8beae3218dcc83689b19ca1e6baeb791531943c15363aa6707c7c0c8b2d601deca1f20c32078b2872d356cdca03b04cce", + ) + + @Test + fun matchIds() { + db.insert(note) + + db.assertQuery(note, Filter(ids = listOf(id))) + db.assertQuery(note, Filter(ids = listOf(id, rootETag))) + db.assertQuery(null, Filter(ids = listOf(rootETag))) + } + + @Test + fun matchPubkeys() { + db.insert(note) + + db.assertQuery(note, Filter(authors = listOf(pubkey))) + db.assertQuery(note, Filter(authors = listOf(pubkey, rootETag))) + db.assertQuery(null, Filter(authors = listOf(rootETag))) + } + + @Test + fun matchTags() { + db.insert(note) + + db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1)))) + db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1, pTag2, pTag3)))) + db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1, id)))) + db.assertQuery(null, Filter(tags = mapOf("p" to listOf(id)))) + } + + @Test + fun matchDualTags() { + db.insert(note) + + db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1), "e" to listOf(rootETag)))) + db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1, pTag2, pTag3), "e" to listOf(rootETag, replyETag)))) + db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1, id), "e" to listOf(rootETag, replyETag)))) + db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1, id), "e" to listOf(rootETag, pubkey)))) + db.assertQuery(null, Filter(tags = mapOf("p" to listOf(pTag1, id), "e" to listOf(id, pubkey)))) + db.assertQuery(null, Filter(tags = mapOf("p" to listOf(id), "e" to listOf(rootETag)))) + } + + @Test + fun matchAllTags() { + db.insert(note) + + db.assertQuery(note, Filter(tagsAll = mapOf("p" to listOf(pTag1)))) + db.assertQuery(note, Filter(tagsAll = mapOf("p" to listOf(pTag1, pTag2, pTag3)))) + db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(pTag1, id)))) + db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(id)))) + } + + @Test + fun matchDualAllTags() { + db.insert(note) + + db.assertQuery(note, Filter(tagsAll = mapOf("p" to listOf(pTag1), "e" to listOf(rootETag)))) + db.assertQuery(note, Filter(tagsAll = mapOf("p" to listOf(pTag1, pTag2, pTag3), "e" to listOf(rootETag, replyETag)))) + db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(pTag1, id), "e" to listOf(rootETag, replyETag)))) + db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(pTag1, id), "e" to listOf(rootETag, pubkey)))) + db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(pTag1, id), "e" to listOf(id, pubkey)))) + db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(id), "e" to listOf(rootETag)))) + } + + @Test + fun matchKinds() { + db.insert(note) + + db.assertQuery(note, Filter(kinds = listOf(kind))) + db.assertQuery(note, Filter(kinds = listOf(kind, 1221))) + db.assertQuery(null, Filter(kinds = listOf(1221))) + } + + @Test + fun matchSince() { + db.insert(note) + + db.assertQuery(note, Filter(since = createdAt)) + db.assertQuery(note, Filter(since = createdAt - 1)) + db.assertQuery(null, Filter(since = createdAt + 1)) + } + + @Test + fun matchUntil() { + db.insert(note) + + db.assertQuery(note, Filter(until = createdAt)) + db.assertQuery(note, Filter(until = createdAt + 1)) + db.assertQuery(null, Filter(until = createdAt - 1)) + } +} diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt index c84fc1848..b972beae0 100644 --- a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt @@ -479,4 +479,27 @@ class QueryAssemblerTest { sql, ) } + + @Test + fun testAllTag() { + val sql = explain(Filter(tagsAll = mapOf("p" to listOf(key1, key2)))) + println(sql) + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_tags as event_tagsAll0_1 ON event_tagsAll0_1.event_header_row_id = event_tags.event_header_row_id WHERE (event_tags.tag_hash = "884286737453847614") AND (event_tagsAll0_1.tag_hash = "-4988851810256311323") + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY created_at DESC, id + ├── CO-ROUTINE filtered + │ ├── SEARCH event_tags USING COVERING INDEX query_by_tags_hash (tag_hash=?) + │ └── SEARCH event_tagsAll0_1 USING COVERING INDEX query_by_tags_hash (tag_hash=? AND event_header_row_id=?) + ├── SCAN filtered + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + sql, + ) + } } diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt index c2b28d739..9fdd08982 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt @@ -436,6 +436,17 @@ class EventIndexesModule( } } + sealed class TagNameForQuery { + class InTags( + val tagName: String, + ) : TagNameForQuery() + + class AllTags( + val tagName: String, + val tagValueIndex: Int, + ) : TagNameForQuery() + } + // ---------------------------- // Inner row id selections // ---------------------------- @@ -447,7 +458,9 @@ class EventIndexesModule( val mustJoinSearch = (filter.search != null) - val nonDTags = filter.tags?.filter { it.key != "d" } ?: emptyMap() + val nonDTagsIn = filter.tags?.filter { it.key != "d" } ?: emptyMap() + + val nonDTagsAll = filter.tagsAll?.filter { it.key != "d" } ?: emptyMap() val hasHeaders = with(filter) { @@ -460,20 +473,30 @@ class EventIndexesModule( (limit != null) } - var defaultTagKey: String? = null + var defaultTagKey: TagNameForQuery? = null val projection = buildString { // always do tags if there are any - if (nonDTags.isNotEmpty()) { + if (nonDTagsIn.isNotEmpty() || nonDTagsAll.isNotEmpty()) { append("SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags ") // it's quite rare to have 2 tags in the filter, but possible - nonDTags.keys.forEachIndexed { index, tagName -> - if (index > 0) { + nonDTagsIn.keys.forEachIndexed { index, tagName -> + if (defaultTagKey != null) { append("INNER JOIN event_tags as event_tagsIn$index ON event_tagsIn$index.event_header_row_id = event_tags.event_header_row_id ") } else { - defaultTagKey = tagName + defaultTagKey = TagNameForQuery.InTags(tagName) + } + } + + nonDTagsAll.keys.forEachIndexed { index, tagName -> + nonDTagsAll[tagName]!!.forEachIndexed { valueIndex, tagValue -> + if (defaultTagKey != null) { + append("INNER JOIN event_tags as event_tagsAll${index}_$valueIndex ON event_tagsAll${index}_$valueIndex.event_header_row_id = event_tags.event_header_row_id ") + } else { + defaultTagKey = TagNameForQuery.AllTags(tagName, valueIndex) + } } } @@ -506,10 +529,10 @@ class EventIndexesModule( filter.since?.let { greaterThanOrEquals("event_headers.created_at", it) } filter.until?.let { lessThanOrEquals("event_headers.created_at", it) } - // there are indexes for these, starting with tags. - nonDTags.forEach { (tagName, tagValues) -> + // it's quite rare to have 2 tags in the filter, but possible + nonDTagsIn.keys.forEachIndexed { index, tagName -> val column = - if (defaultTagKey == null || defaultTagKey == tagName) { + if (defaultTagKey == null || (defaultTagKey is TagNameForQuery.InTags && defaultTagKey.tagName == tagName)) { "event_tags.tag_hash" } else { "event_tagsIn$index.tag_hash" @@ -517,12 +540,26 @@ class EventIndexesModule( equalsOrIn( column, - tagValues.map { + nonDTagsIn[tagName]!!.map { hasher.hash(tagName, it) }, ) } + // there are indexes for these, starting with tags. + nonDTagsAll.keys.forEachIndexed { index, tagName -> + nonDTagsAll[tagName]!!.forEachIndexed { valueIndex, tagValue -> + val column = + if (defaultTagKey == null || (defaultTagKey is TagNameForQuery.AllTags && defaultTagKey.tagName == tagName && defaultTagKey.tagValueIndex == valueIndex)) { + "event_tags.tag_hash" + } else { + "event_tagsAll${index}_$valueIndex.tag_hash" + } + + equals(column, hasher.hash(tagName, tagValue)) + } + } + filter.kinds?.let { equalsOrIn("event_headers.kind", it) } filter.authors?.let { equalsOrIn("event_headers.pubkey", it) } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt index f4d1f5aad..2d1fb0188 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt @@ -48,6 +48,7 @@ class Filter( val authors: List? = null, val kinds: List? = null, val tags: Map>? = null, + val tagsAll: Map>? = null, val since: Long? = null, val until: Long? = null, val limit: Int? = null, @@ -55,18 +56,19 @@ class Filter( ) : OptimizedSerializable { fun toJson() = OptimizedJsonMapper.toJson(this) - fun match(event: Event) = FilterMatcher.match(event, ids, authors, kinds, tags, since, until) + fun match(event: Event) = FilterMatcher.match(event, ids, authors, kinds, tags, tagsAll, since, until) fun copy( ids: List? = this.ids, authors: List? = this.authors, kinds: List? = this.kinds, tags: Map>? = this.tags, + tagsAll: Map>? = this.tagsAll, since: Long? = this.since, until: Long? = this.until, limit: Int? = this.limit, search: String? = this.search, - ) = Filter(ids, authors, kinds, tags, since, until, limit, search) + ) = Filter(ids, authors, kinds, tags, tagsAll, since, until, limit, search) /** * Returns true if this filter contains any non-null and non-empty criteria. @@ -76,6 +78,7 @@ class Filter( (authors != null && authors.isNotEmpty()) || (kinds != null && kinds.isNotEmpty()) || (tags != null && tags.isNotEmpty() && tags.values.all { it.isNotEmpty() }) || + (tagsAll != null && tagsAll.isNotEmpty() && tagsAll.values.all { it.isNotEmpty() }) || (since != null) || (until != null) || (limit != null) || @@ -100,6 +103,16 @@ class Filter( if (Address.parse(it) == null) Log.e("FilterError", "Invalid a-tag $it on ${toJson()}") } } + if (tagsAll != null) { + tagsAll["p"]?.forEach { + if (it.length != 64) Log.e("FilterError", "Invalid p-tag length $it on ${toJson()}") + } + tagsAll["e"]?.forEach { + if (it.length != 64) Log.e("FilterError", "Invalid e-tag length $it on ${toJson()}") + } + tagsAll["a"]?.forEach { + if (Address.parse(it) == null) Log.e("FilterError", "Invalid a-tag $it on ${toJson()}") + } } } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterMatcher.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterMatcher.kt index 7c7a579c2..0539a9d76 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterMatcher.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterMatcher.kt @@ -29,6 +29,7 @@ object FilterMatcher { authors: List? = null, kinds: List? = null, tags: Map>? = null, + tagsAll: Map>? = null, since: Long? = null, until: Long? = null, ): Boolean { @@ -40,6 +41,20 @@ object FilterMatcher { // AND between keys, OR between values if (!event.tags.any { it.size > 1 && it[0] == tag.key && it[1] in valueSet }) return false } + tagsAll?.forEach { tag -> + val eventTagValueSet = + event.tags.mapNotNullTo(mutableSetOf()) { + if (it.size > 1 && it[0] == tag.key) { + it[1] + } else { + null + } + } + // AND between keys, AND between values + for (tagValue in tag.value) { + if (tagValue !in eventTagValueSet) return false + } + } if (event.createdAt !in (since ?: Long.MIN_VALUE)..(until ?: Long.MAX_VALUE)) { return false } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterMatcherTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterMatcherTest.kt new file mode 100644 index 000000000..ea697b2f0 --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterMatcherTest.kt @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.relay.filters + +import com.vitorpamplona.quartz.nip01Core.core.Event +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class FilterMatcherTest { + val id = "98b574c3527f0ffb30b7271084e3f07480733c7289f8de424d29eae82e36c758" + val pubkey = "46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d" + val createdAt: Long = 1683596206 + val kind = 1 + + val pTag1 = "22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954" + val pTag2 = "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24" + val pTag3 = "ec4d241c334311b3a304433ee3442be29d0e88e7ec19b85edf2bba29b93565e2" + val pTag4 = "0fe0b18b4dbf0e0aa40fcd47209b2a49b3431fc453b460efcf45ca0bd16bd6ac" + val pTag5 = "8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168" + val pTag6 = "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed" + val pTag7 = "4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0" + val pTag8 = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c" + + val rootETag = "27ac621d7dc4a932e1a79f984308e7d20656dd6fddb2ce9cdfcb6a67b9a7bcc3" + val replyETag = "be7245af96210a0dd048cab4ad38e52dbd6c09a53ea21a7edb6be8898e5727cc" + + val note = + Event( + id, + pubkey, + createdAt, + kind, + arrayOf( + arrayOf("e", rootETag, "", "root"), + arrayOf("e", replyETag, "", "reply"), + arrayOf("p", pTag1), + arrayOf("p", pTag1), + arrayOf("p", pTag2), + arrayOf("p", pTag3), + arrayOf("p", pTag4), + arrayOf("p", pTag5), + arrayOf("p", pTag6), + arrayOf("p", pTag7), + arrayOf("p", pTag8), + ), + "Astral:\n\nhttps://void.cat/d/A5Fba5B1bcxwEmeyoD9nBs.webp\n\nIris:\n\nhttps://void.cat/d/44hTcVvhRps6xYYs99QsqA.webp\n\nSnort:\n\nhttps://void.cat/d/4nJD5TRePuQChM5tzteYbU.webp\n\nAmethyst agrees with Astral which I suspect are both wrong. nostr:npub13sx6fp3pxq5rl70x0kyfmunyzaa9pzt5utltjm0p8xqyafndv95q3saapa nostr:npub1v0lxxxxutpvrelsksy8cdhgfux9l6a42hsj2qzquu2zk7vc9qnkszrqj49 nostr:npub1g53mukxnjkcmr94fhryzkqutdz2ukq4ks0gvy5af25rgmwsl4ngq43drvk nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z ", + "4aa5264965018fa12a326686ad3d3bd8beae3218dcc83689b19ca1e6baeb791531943c15363aa6707c7c0c8b2d601deca1f20c32078b2872d356cdca03b04cce", + ) + + @Test + fun matchIds() { + assertTrue(note, Filter(ids = listOf(id))) + assertTrue(note, Filter(ids = listOf(id, rootETag))) + assertFalse(note, Filter(ids = listOf(rootETag))) + } + + @Test + fun matchPubkeys() { + assertTrue(note, Filter(authors = listOf(pubkey))) + assertTrue(note, Filter(authors = listOf(pubkey, rootETag))) + assertFalse(note, Filter(authors = listOf(rootETag))) + } + + @Test + fun matchTags() { + assertTrue(note, Filter(tags = mapOf("p" to listOf(pTag1)))) + assertTrue(note, Filter(tags = mapOf("p" to listOf(pTag1, pTag2, pTag3)))) + assertTrue(note, Filter(tags = mapOf("p" to listOf(pTag1, id)))) + assertFalse(note, Filter(tags = mapOf("p" to listOf(id)))) + } + + @Test + fun matchDualTags() { + assertTrue(note, Filter(tags = mapOf("p" to listOf(pTag1), "e" to listOf(rootETag)))) + assertTrue(note, Filter(tags = mapOf("p" to listOf(pTag1, pTag2, pTag3), "e" to listOf(rootETag, replyETag)))) + assertTrue(note, Filter(tags = mapOf("p" to listOf(pTag1, id), "e" to listOf(rootETag, replyETag)))) + assertTrue(note, Filter(tags = mapOf("p" to listOf(pTag1, id), "e" to listOf(rootETag, pubkey)))) + assertFalse(note, Filter(tags = mapOf("p" to listOf(pTag1, id), "e" to listOf(id, pubkey)))) + assertFalse(note, Filter(tags = mapOf("p" to listOf(id), "e" to listOf(rootETag)))) + } + + @Test + fun matchAllTags() { + assertTrue(note, Filter(tagsAll = mapOf("p" to listOf(pTag1, pTag2, pTag3)))) + assertTrue(note, Filter(tagsAll = mapOf("p" to listOf(pTag1)))) + assertFalse(note, Filter(tagsAll = mapOf("p" to listOf(pTag1, id)))) + assertFalse(note, Filter(tagsAll = mapOf("p" to listOf(id)))) + } + + @Test + fun matchDualAllTags() { + assertTrue(note, Filter(tagsAll = mapOf("p" to listOf(pTag1), "e" to listOf(rootETag)))) + assertTrue(note, Filter(tagsAll = mapOf("p" to listOf(pTag1, pTag2, pTag3), "e" to listOf(rootETag, replyETag)))) + assertFalse(note, Filter(tagsAll = mapOf("p" to listOf(pTag1, id), "e" to listOf(rootETag, replyETag)))) + assertFalse(note, Filter(tagsAll = mapOf("p" to listOf(pTag1, id), "e" to listOf(rootETag, pubkey)))) + assertFalse(note, Filter(tagsAll = mapOf("p" to listOf(pTag1, id), "e" to listOf(id, pubkey)))) + assertFalse(note, Filter(tagsAll = mapOf("p" to listOf(id), "e" to listOf(rootETag)))) + } + + @Test + fun matchKinds() { + assertTrue(note, Filter(kinds = listOf(kind))) + assertTrue(note, Filter(kinds = listOf(kind, 1221))) + assertFalse(note, Filter(kinds = listOf(1221))) + } + + @Test + fun matchSince() { + assertTrue(note, Filter(since = createdAt)) + assertTrue(note, Filter(since = createdAt - 1)) + assertFalse(note, Filter(since = createdAt + 1)) + } + + @Test + fun matchUntil() { + assertTrue(note, Filter(until = createdAt)) + assertTrue(note, Filter(until = createdAt + 1)) + assertFalse(note, Filter(until = createdAt - 1)) + } + + fun assertTrue( + event: Event, + filter: Filter, + ) = assertTrue(filter.match(event)) + + fun assertFalse( + event: Event, + filter: Filter, + ) = assertFalse(filter.match(event)) +} diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterDeserializer.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterDeserializer.kt index 7b7cbeeca..556dda5de 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterDeserializer.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterDeserializer.kt @@ -38,10 +38,16 @@ class FilterDeserializer : StdDeserializer(Filter::class.java) { class ManualFilterDeserializer { companion object { fun fromJson(jsonObject: ObjectNode): Filter { - val tags = mutableListOf() + val tagsIn = mutableListOf() jsonObject.fieldNames().forEach { if (it.startsWith("#")) { - tags.add(it.substring(1)) + tagsIn.add(it.substring(1)) + } + } + val tagsAll = mutableListOf() + jsonObject.fieldNames().forEach { + if (it.startsWith("&")) { + tagsAll.add(it.substring(1)) } } @@ -49,7 +55,8 @@ class ManualFilterDeserializer { ids = jsonObject.get("ids").mapNotNull { it.asTextOrNull() }, authors = jsonObject.get("authors").mapNotNull { it.asTextOrNull() }, kinds = jsonObject.get("kinds").mapNotNull { it.asIntOrNull() }, - tags = tags.associateWith { jsonObject.get(it).mapNotNull { it.asTextOrNull() } }, + tags = tagsIn.associateWith { jsonObject.get(it).mapNotNull { it.asTextOrNull() } }, + tagsAll = tagsAll.associateWith { jsonObject.get(it).mapNotNull { it.asTextOrNull() } }, since = jsonObject.get("since").asLongOrNull(), until = jsonObject.get("until").asLongOrNull(), limit = jsonObject.get("limit").asIntOrNull(), diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterSerializer.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterSerializer.kt index afd06b36b..47fdcc674 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterSerializer.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterSerializer.kt @@ -66,6 +66,16 @@ class FilterSerializer : StdSerializer(Filter::class.java) { } } + filter.tagsAll?.run { + entries.forEach { kv -> + gen.writeArrayFieldStart("&${kv.key}") + for (i in kv.value.indices) { + gen.writeString(kv.value[i]) + } + gen.writeEndArray() + } + } + filter.since?.run { gen.writeNumberField("since", this) } filter.until?.run { gen.writeNumberField("until", this) } filter.limit?.run { gen.writeNumberField("limit", this) } From 5eb9d230e26065cb6f88cc095cc53ee14e18e417 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 30 Dec 2025 13:00:03 -0500 Subject: [PATCH 010/137] Moves current test to a better name of Notifications Adds a Followers test --- .../store/sqlite/QueryAssemblerTest.kt | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt index b972beae0..49c9cf12a 100644 --- a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt @@ -77,9 +77,6 @@ class QueryAssemblerTest { since = 1750889190, ), ) - - println(query) - TestCase.assertEquals( """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers @@ -267,6 +264,38 @@ class QueryAssemblerTest { @Test fun testFollowersOf() { + val sql = + explain( + listOf( + Filter( + kinds = listOf(ContactListEvent.KIND), + tags = mapOf("p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")), + limit = 30, + ), + ), + ) + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_headers.kind = "3") ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 30 + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY created_at DESC, id + ├── CO-ROUTINE filtered + │ ├── SEARCH event_tags USING COVERING INDEX query_by_tags_hash (tag_hash=?) + │ ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + │ └── USE TEMP B-TREE FOR ORDER BY + ├── SCAN filtered + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + sql, + ) + } + + @Test + fun testNotificationsOf() { val sql = explain( listOf( From 7b696f5f544026a43c0c1403cc78a8d941895f04 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 30 Dec 2025 16:23:11 -0500 Subject: [PATCH 011/137] Adds created_at to tags to speed up tag-ordered queries --- .../nip01Core/store/sqlite/DeletionTest.kt | 4 +- .../store/sqlite/QueryAssemblerTest.kt | 49 ++++++++--------- .../store/sqlite/DeletionRequestModule.kt | 10 ++-- .../store/sqlite/EventIndexesModule.kt | 52 ++++++++++++++----- 4 files changed, 72 insertions(+), 43 deletions(-) diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionTest.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionTest.kt index 1f7f771b3..bb35f07be 100644 --- a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionTest.kt +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionTest.kt @@ -227,8 +227,8 @@ class DeletionTest { ON event_headers.row_id = event_tags.event_header_row_id WHERE event_tags.tag_hash IN (3221122, 223322) AND + event_tags.created_at >= 1766686500 AND event_headers.kind = 5 AND - event_headers.created_at >= 1766686500 AND event_headers.pubkey_owner_hash = 22332323 """.trimIndent() @@ -238,7 +238,7 @@ class DeletionTest { TestCase.assertEquals( """ ${sql.replace("\n","\n ")} - ├── SEARCH event_tags USING COVERING INDEX query_by_tags_hash (tag_hash=?) + ├── SEARCH event_tags USING INDEX query_by_tags_hash (tag_hash=? AND created_at>?) └── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) """.trimIndent(), explainer, diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt index 49c9cf12a..9a6000835 100644 --- a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt @@ -31,7 +31,7 @@ import org.junit.Before import org.junit.Test class QueryAssemblerTest { - val hasher = TagNameValueHasher(0L) + val hasher = TagNameValueHasher(0) val builder = EventIndexesModule(FullTextSearchModule(), { hasher }) val key1 = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d" @@ -81,13 +81,14 @@ class QueryAssemblerTest { """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE (event_headers.created_at >= "1750889190") AND (event_tags.tag_hash = "2657743813502222172") AND (event_headers.kind = "5") AND (event_headers.pubkey = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d") + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE (event_tags.tag_hash = "2657743813502222172") AND (event_tags.created_at >= "1750889190") AND (event_headers.kind = "5") AND (event_headers.pubkey = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d") ) AS filtered ON event_headers.row_id = filtered.row_id ORDER BY created_at DESC, id ├── CO-ROUTINE filtered - │ ├── SEARCH event_tags USING COVERING INDEX query_by_tags_hash (tag_hash=?) - │ └── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash (tag_hash=? AND created_at>?) + │ ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + │ └── USE TEMP B-TREE FOR DISTINCT ├── SCAN filtered ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) └── USE TEMP B-TREE FOR ORDER BY @@ -278,14 +279,14 @@ class QueryAssemblerTest { """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_headers.kind = "3") ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 30 + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_headers.kind = "3") ORDER BY event_tags.created_at DESC LIMIT 30 ) AS filtered ON event_headers.row_id = filtered.row_id ORDER BY created_at DESC, id ├── CO-ROUTINE filtered - │ ├── SEARCH event_tags USING COVERING INDEX query_by_tags_hash (tag_hash=?) + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash (tag_hash=?) │ ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - │ └── USE TEMP B-TREE FOR ORDER BY + │ └── USE TEMP B-TREE FOR DISTINCT ├── SCAN filtered ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) └── USE TEMP B-TREE FOR ORDER BY @@ -309,14 +310,13 @@ class QueryAssemblerTest { """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE event_tags.tag_hash = "-4551135004136952885" ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 30 + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE event_tags.tag_hash = "-4551135004136952885" ORDER BY event_tags.created_at DESC LIMIT 30 ) AS filtered ON event_headers.row_id = filtered.row_id ORDER BY created_at DESC, id ├── CO-ROUTINE filtered - │ ├── SEARCH event_tags USING COVERING INDEX query_by_tags_hash (tag_hash=?) - │ ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - │ └── USE TEMP B-TREE FOR ORDER BY + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash (tag_hash=?) + │ └── USE TEMP B-TREE FOR DISTINCT ├── SCAN filtered ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) └── USE TEMP B-TREE FOR ORDER BY @@ -344,14 +344,14 @@ class QueryAssemblerTest { """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_headers.kind = "3") ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 30 + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_headers.kind = "3") ORDER BY event_tags.created_at DESC LIMIT 30 ) AS filtered ON event_headers.row_id = filtered.row_id ORDER BY created_at DESC, id ├── CO-ROUTINE filtered - │ ├── SEARCH event_tags USING COVERING INDEX query_by_tags_hash (tag_hash=?) + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash (tag_hash=?) │ ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - │ └── USE TEMP B-TREE FOR ORDER BY + │ └── USE TEMP B-TREE FOR DISTINCT ├── SCAN filtered ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) └── USE TEMP B-TREE FOR ORDER BY @@ -376,14 +376,14 @@ class QueryAssemblerTest { """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_headers.pubkey = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d") ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 30 + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_headers.pubkey = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d") ORDER BY event_tags.created_at DESC LIMIT 30 ) AS filtered ON event_headers.row_id = filtered.row_id ORDER BY created_at DESC, id ├── CO-ROUTINE filtered - │ ├── SEARCH event_tags USING COVERING INDEX query_by_tags_hash (tag_hash=?) + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash (tag_hash=?) │ ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - │ └── USE TEMP B-TREE FOR ORDER BY + │ └── USE TEMP B-TREE FOR DISTINCT ├── SCAN filtered ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) └── USE TEMP B-TREE FOR ORDER BY @@ -412,15 +412,15 @@ class QueryAssemblerTest { """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_tags as event_tagsIn1 ON event_tagsIn1.event_header_row_id = event_tags.event_header_row_id INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tagsIn1.tag_hash = "-6379614208644810021") AND (event_headers.kind = "1") ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 30 + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_tags as event_tagsIn1 ON event_tagsIn1.event_header_row_id = event_tags.event_header_row_id AND event_tagsIn1.created_at = event_tags.created_at INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tagsIn1.tag_hash = "-6379614208644810021") AND (event_headers.kind = "1") ORDER BY event_tags.created_at DESC LIMIT 30 ) AS filtered ON event_headers.row_id = filtered.row_id ORDER BY created_at DESC, id ├── CO-ROUTINE filtered - │ ├── SEARCH event_tags USING COVERING INDEX query_by_tags_hash (tag_hash=?) + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash (tag_hash=?) │ ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - │ ├── SEARCH event_tagsIn1 USING COVERING INDEX query_by_tags_hash (tag_hash=? AND event_header_row_id=?) - │ └── USE TEMP B-TREE FOR ORDER BY + │ ├── SEARCH event_tagsIn1 USING INDEX query_by_tags_hash (tag_hash=? AND event_header_row_id=?) + │ └── USE TEMP B-TREE FOR DISTINCT ├── SCAN filtered ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) └── USE TEMP B-TREE FOR ORDER BY @@ -517,13 +517,14 @@ class QueryAssemblerTest { """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_tags as event_tagsAll0_1 ON event_tagsAll0_1.event_header_row_id = event_tags.event_header_row_id WHERE (event_tags.tag_hash = "884286737453847614") AND (event_tagsAll0_1.tag_hash = "-4988851810256311323") + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_tags as event_tagsAll0_1 ON event_tagsAll0_1.event_header_row_id = event_tags.event_header_row_id AND event_tagsAll0_1.created_at = event_tags.created_at WHERE (event_tags.tag_hash = "884286737453847614") AND (event_tagsAll0_1.tag_hash = "-4988851810256311323") ) AS filtered ON event_headers.row_id = filtered.row_id ORDER BY created_at DESC, id ├── CO-ROUTINE filtered - │ ├── SEARCH event_tags USING COVERING INDEX query_by_tags_hash (tag_hash=?) - │ └── SEARCH event_tagsAll0_1 USING COVERING INDEX query_by_tags_hash (tag_hash=? AND event_header_row_id=?) + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash (tag_hash=?) + │ ├── SEARCH event_tagsAll0_1 USING INDEX query_by_tags_hash (tag_hash=? AND created_at=?) + │ └── USE TEMP B-TREE FOR DISTINCT ├── SCAN filtered ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) └── USE TEMP B-TREE FOR ORDER BY diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionRequestModule.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionRequestModule.kt index 32e567fd1..de7b13e59 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionRequestModule.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionRequestModule.kt @@ -43,7 +43,11 @@ class DeletionRequestModule( BEFORE INSERT ON event_headers FOR EACH ROW BEGIN - -- Check for ID-based deletion record + -- Checks if this event hasn't already been deleted. If so, reject it. + + -- Highly optimized using hash + created_at (duplicated in tags) index: + ---- Expects tag_hash to be non-existant for new events (quick exit) + ---- Expects created-at >= event's to not exist (quick exit) SELECT RAISE(ABORT, 'blocked: a deletion event exists') WHERE EXISTS ( SELECT 1 FROM event_tags @@ -51,9 +55,9 @@ class DeletionRequestModule( ON event_headers.row_id = event_tags.event_header_row_id WHERE event_tags.tag_hash IN (NEW.etag_hash, NEW.atag_hash) AND + event_tags.created_at >= NEW.created_at AND event_headers.kind = 5 AND - event_headers.pubkey_owner_hash = NEW.pubkey_owner_hash AND - event_headers.created_at >= NEW.created_at + event_headers.pubkey_owner_hash = NEW.pubkey_owner_hash ); END; """.trimIndent(), diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt index 9fdd08982..981260c58 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt @@ -63,6 +63,7 @@ class EventIndexesModule( CREATE TABLE event_tags ( event_header_row_id INTEGER NOT NULL, tag_hash INTEGER NOT NULL, + created_at INTEGER NOT NULL, FOREIGN KEY (event_header_row_id) REFERENCES event_headers(row_id) ON DELETE CASCADE ) """.trimIndent(), @@ -70,12 +71,15 @@ class EventIndexesModule( db.execSQL("CREATE UNIQUE INDEX event_headers_id ON event_headers (id)") db.execSQL("CREATE INDEX query_by_kind_pubkey_dtag_idx ON event_headers (kind, pubkey, d_tag)") - db.execSQL("CREATE INDEX query_by_created_at_id ON event_headers (created_at desc, id)") + db.execSQL("CREATE INDEX query_by_created_at_id ON event_headers (created_at DESC, id)") + // need to check if this is actually needed. - db.execSQL("CREATE INDEX query_by_created_at_kind_key ON event_headers (created_at desc, kind, pubkey)") + db.execSQL("CREATE INDEX query_by_created_at_kind_key ON event_headers (created_at DESC, kind, pubkey)") db.execSQL("CREATE INDEX fk_event_tags_header_id ON event_tags (event_header_row_id)") - db.execSQL("CREATE INDEX query_by_tags_hash ON event_tags (tag_hash, event_header_row_id)") + + // This is a very slow index to build (half the insert time goes here) but it is extremely effective. + db.execSQL("CREATE INDEX query_by_tags_hash ON event_tags (tag_hash, created_at DESC)") // Prevent updates to maintain immutability db.execSQL( @@ -117,9 +121,9 @@ class EventIndexesModule( val sqlInsertTags = """ INSERT OR ROLLBACK INTO event_tags - (event_header_row_id, tag_hash) + (event_header_row_id, tag_hash, created_at) VALUES - (?,?) + (?,?,?) """.trimIndent() fun insert( @@ -177,6 +181,7 @@ class EventIndexesModule( indexableTags.forEach { stmtTags.bindLong(1, headerId) stmtTags.bindLong(2, it) + stmtTags.bindLong(3, event.createdAt) stmtTags.executeInsert() } @@ -462,6 +467,16 @@ class EventIndexesModule( val nonDTagsAll = filter.tagsAll?.filter { it.key != "d" } ?: emptyMap() + val reverseLookup = nonDTagsIn.isNotEmpty() || nonDTagsAll.isNotEmpty() + + val needHeaders = + with(filter) { + (ids != null) || + (authors != null && authors.isNotEmpty()) || + (kinds != null && kinds.isNotEmpty()) || + (tags != null && tags.containsKey("d")) + } + val hasHeaders = with(filter) { (ids != null) || @@ -478,13 +493,13 @@ class EventIndexesModule( val projection = buildString { // always do tags if there are any - if (nonDTagsIn.isNotEmpty() || nonDTagsAll.isNotEmpty()) { + if (reverseLookup) { append("SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags ") // it's quite rare to have 2 tags in the filter, but possible nonDTagsIn.keys.forEachIndexed { index, tagName -> if (defaultTagKey != null) { - append("INNER JOIN event_tags as event_tagsIn$index ON event_tagsIn$index.event_header_row_id = event_tags.event_header_row_id ") + append("INNER JOIN event_tags as event_tagsIn$index ON event_tagsIn$index.event_header_row_id = event_tags.event_header_row_id AND event_tagsIn$index.created_at = event_tags.created_at ") } else { defaultTagKey = TagNameForQuery.InTags(tagName) } @@ -493,14 +508,14 @@ class EventIndexesModule( nonDTagsAll.keys.forEachIndexed { index, tagName -> nonDTagsAll[tagName]!!.forEachIndexed { valueIndex, tagValue -> if (defaultTagKey != null) { - append("INNER JOIN event_tags as event_tagsAll${index}_$valueIndex ON event_tagsAll${index}_$valueIndex.event_header_row_id = event_tags.event_header_row_id ") + append("INNER JOIN event_tags as event_tagsAll${index}_$valueIndex ON event_tagsAll${index}_$valueIndex.event_header_row_id = event_tags.event_header_row_id AND event_tagsAll${index}_$valueIndex.created_at = event_tags.created_at ") } else { defaultTagKey = TagNameForQuery.AllTags(tagName, valueIndex) } } } - if (hasHeaders) { + if (needHeaders) { append("INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id ") } @@ -525,10 +540,6 @@ class EventIndexesModule( // ids reduce the filter the most filter.ids?.let { equalsOrIn("event_headers.id", it) } - // range search is bad but most of the time these are up the top with few elements. - filter.since?.let { greaterThanOrEquals("event_headers.created_at", it) } - filter.until?.let { lessThanOrEquals("event_headers.created_at", it) } - // it's quite rare to have 2 tags in the filter, but possible nonDTagsIn.keys.forEachIndexed { index, tagName -> val column = @@ -560,6 +571,15 @@ class EventIndexesModule( } } + // range search is bad but most of the time these are up the top with few elements. + if (reverseLookup) { + filter.since?.let { greaterThanOrEquals("event_tags.created_at", it) } + filter.until?.let { lessThanOrEquals("event_tags.created_at", it) } + } else { + filter.since?.let { greaterThanOrEquals("event_headers.created_at", it) } + filter.until?.let { lessThanOrEquals("event_headers.created_at", it) } + } + filter.kinds?.let { equalsOrIn("event_headers.kind", it) } filter.authors?.let { equalsOrIn("event_headers.pubkey", it) } @@ -580,7 +600,11 @@ class EventIndexesModule( val whereClause = if (filter.limit != null) { - "${clause.conditions} ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT ${filter.limit}" + if (reverseLookup) { + "${clause.conditions} ORDER BY event_tags.created_at DESC LIMIT ${filter.limit}" + } else { + "${clause.conditions} ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT ${filter.limit}" + } } else { clause.conditions } From 65859a9269af2f719ad1708e638c58b5d4972672 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Wed, 31 Dec 2025 07:13:27 +0200 Subject: [PATCH 012/137] update claude.md --- .claude/CLAUDE.md | 52 +- .claude/agents/compose-ui.md | 169 ------ .claude/agents/kotlin-coroutines.md | 187 ------- .claude/agents/kotlin-multiplatform.md | 127 ----- .claude/agents/nostr-protocol.md | 168 ------ .claude/skills/kotlin-coroutines/SKILL.md | 419 +++++++++++++++ .../references/advanced-flow-operators.md | 309 +++++++++++ .../references/relay-patterns.md | 480 +++++++++++++++++ .../references/testing-coroutines.md | 493 ++++++++++++++++++ 9 files changed, 1743 insertions(+), 661 deletions(-) delete mode 100644 .claude/agents/compose-ui.md delete mode 100644 .claude/agents/kotlin-coroutines.md delete mode 100644 .claude/agents/kotlin-multiplatform.md delete mode 100644 .claude/agents/nostr-protocol.md create mode 100644 .claude/skills/kotlin-coroutines/SKILL.md create mode 100644 .claude/skills/kotlin-coroutines/references/advanced-flow-operators.md create mode 100644 .claude/skills/kotlin-coroutines/references/relay-patterns.md create mode 100644 .claude/skills/kotlin-coroutines/references/testing-coroutines.md diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 9ecd75d86..feac27e1a 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -40,21 +40,53 @@ amethyst/ | **DI** | Manual / Koin | | **Build** | Gradle 8.x, Kotlin 2.1.0 | -## Agents +## Skills -Use these specialized agents for domain expertise: +Specialized skills provide domain expertise with bundled resources and patterns: -| Agent | Expertise | When to Use | +| Skill | Expertise | When to Use | |-------|-----------|-------------| -| `nostr-protocol` | NIPs, events, relays, crypto | Protocol questions, NIP implementation | -| `kotlin-multiplatform` | KMP, source sets, expect/actual | Project structure, code sharing | -| `compose-ui` | Composables, Desktop features | UI components, navigation | -| `kotlin-coroutines` | Flows, async, concurrency | Data streams, async operations | +| `nostr-expert` | Nostr protocol (Quartz library) | Event types, NIPs, tags, signing, Bech32 | +| `kotlin-expert` | Advanced Kotlin patterns | StateFlow, sealed classes, @Immutable, DSLs | +| `kotlin-coroutines` | Advanced async patterns | supervisorScope, callbackFlow, relay pools, testing | +| `kotlin-multiplatform` | Platform abstraction | expect/actual, source sets, sharing decisions | +| `compose-expert` | Shared UI components | Material3, state hoisting, recomposition | +| `android-expert` | Android platform | Navigation, permissions, lifecycle, Material3 | +| `desktop-expert` | Desktop platform | Window, MenuBar, Tray, keyboard shortcuts | +| `gradle-expert` | Build system | Dependencies, versioning, packaging, optimization | + +## Workflow + +**When you ask for a feature:** + +1. **Quick skill assessment** - I identify which skills are relevant +2. **Propose which skills** - I present which skills I'll use for the task +3. **Get approval** - You review and approve (or adjust) the skill selection +4. **Review plan using approved skills** - I invoke the approved skills to create detailed implementation plan +5. **Execute with skills** - Skills collaborate to implement the feature + +**Example:** +``` +You: "Add video support to notes" +Me: "I'll use: + - /nostr-expert (NIP-71 video events) + - /compose-expert (video player UI) + - /android-expert (platform video APIs) + Proceed?" +You: "yes" +Me: [invokes skills to create plan] + "Plan from skills: + 1. nostr-expert: Use NIP-71 kind 34235 for video events... + 2. compose-expert: Create VideoPlayer composable in commons... + 3. android-expert: Use ExoPlayer for Android... + Proceed with implementation?" +You: "yes" +Me: [implements using skill guidance] +``` ## Commands - `/desktop-run` - Build and run desktop app -- `/extract ` - Move composable to shared code - `/nip ` - Get NIP implementation guidance ## Feature Workflow @@ -96,8 +128,8 @@ Before coding, create a plan that categorizes work into three buckets: When extracting UI components: 1. Identify reusable composables in Android code -2. Use `/extract ` to move to `commons/commonMain/` -3. Create expect/actual declarations for platform-specific behavior +2. Move to `commons/commonMain/` (consult `/compose-expert` for patterns) +3. Create expect/actual declarations for platform-specific behavior (consult `/kotlin-multiplatform`) 4. Update both Android and Desktop to use shared component **Note:** `quartz/` is protocol-only (no composables). Shared UI goes in `commons/` after converting it to KMP. diff --git a/.claude/agents/compose-ui.md b/.claude/agents/compose-ui.md deleted file mode 100644 index d51b2eca8..000000000 --- a/.claude/agents/compose-ui.md +++ /dev/null @@ -1,169 +0,0 @@ ---- -name: compose-ui -description: Automatically invoked when working with Compose Multiplatform UI code, @Composable functions, desktop Window/MenuBar/Tray, navigation patterns, or UI components in desktopApp/ or shared UI modules. -tools: Read, Edit, Write, Bash, Grep, Glob, Task, WebFetch -model: sonnet ---- - -# Compose Multiplatform UI Agent - -You are a Compose Multiplatform UI expert specializing in shared composables and desktop-specific features. - -## Auto-Trigger Contexts - -Activate when user works with: -- `@Composable` functions -- `desktopApp/` module files -- `Window`, `MenuBar`, `Tray` components -- Navigation patterns (NavigationRail, screens) -- Material3 theming -- Keyboard shortcuts, context menus - -## Core Knowledge - -### Desktop Entry Point -```kotlin -fun main() = application { - Window( - onCloseRequest = ::exitApplication, - state = rememberWindowState(width = 1200.dp, height = 800.dp), - title = "Amethyst Desktop" - ) { - MenuBar { - Menu("File") { - Item("New Note", shortcut = KeyShortcut(Key.N, ctrl = true)) { } - Item("Quit", onClick = ::exitApplication) - } - } - App() - } -} -``` - -### Desktop-Specific Features - -**Menu Bar** -```kotlin -MenuBar { - Menu("File") { - Item("New", shortcut = KeyShortcut(Key.N, ctrl = true)) { } - Separator() - Item("Quit", onClick = ::exitApplication) - } -} -``` - -**System Tray** -```kotlin -Tray( - icon = painterResource("icon.png"), - menu = { - Item("Show", onClick = { windowVisible = true }) - Item("Exit", onClick = ::exitApplication) - } -) -``` - -**Context Menus** -```kotlin -ContextMenuArea(items = { - listOf( - ContextMenuItem("Copy") { copyToClipboard(text) }, - ContextMenuItem("Reply") { openReply() } - ) -}) { - Text(content) -} -``` - -**Keyboard Shortcuts** -```kotlin -Modifier.onKeyEvent { event -> - when { - event.isCtrlPressed && event.key == Key.Enter -> { send(); true } - event.key == Key.Escape -> { close(); true } - else -> false - } -} -``` - -### Navigation Pattern -```kotlin -@Composable -fun DesktopLayout(currentScreen: Screen, onNavigate: (Screen) -> Unit) { - Row(Modifier.fillMaxSize()) { - NavigationRail { - NavigationRailItem( - icon = { Icon(Icons.Default.Home, "Feed") }, - selected = currentScreen == Screen.Feed, - onClick = { onNavigate(Screen.Feed) } - ) - // More items... - } - Box(Modifier.weight(1f)) { - when (currentScreen) { - Screen.Feed -> FeedScreen() - Screen.Messages -> MessagesScreen() - } - } - } -} -``` - -### Platform Differences - -| Aspect | Android | Desktop | -|--------|---------|---------| -| **Entry** | Activity | main() + Window | -| **Navigation** | Bottom nav | Sidebar / MenuBar | -| **Input** | Touch | Mouse + Keyboard | -| **Windows** | Single | Multi-window | -| **Menus** | Overflow | MenuBar | - -## Workflow - -### 1. Assess Task -- Shared composable or desktop-specific? -- Navigation change or component work? -- State management needs? - -### 2. Investigate -```bash -# Find existing composables -grep -r "@Composable" desktopApp/src/ -# Check navigation structure -grep -r "Screen\|navigate" desktopApp/src/ -``` - -### 3. Implement -- Shared composables go in shared UI module -- Desktop-specific (Window, MenuBar) in desktopApp -- Use Material3 components -- Handle keyboard/mouse input for desktop - -### 4. Verify -```bash -./gradlew :desktopApp:run -``` - -## State Management -```kotlin -class FeedViewModel { - private val _state = MutableStateFlow(FeedState()) - val state: StateFlow = _state.asStateFlow() -} - -@Composable -fun FeedScreen(viewModel: FeedViewModel) { - val state by viewModel.state.collectAsState() - // UI based on state -} -``` - -## Constraints - -- Prefer shared composables in commonMain when possible -- Desktop-specific features only in desktopApp -- Follow Material3 design guidelines -- Support keyboard navigation for accessibility -- Test on macOS, Windows, Linux when possible diff --git a/.claude/agents/kotlin-coroutines.md b/.claude/agents/kotlin-coroutines.md deleted file mode 100644 index ed1679972..000000000 --- a/.claude/agents/kotlin-coroutines.md +++ /dev/null @@ -1,187 +0,0 @@ ---- -name: kotlin-coroutines -description: Automatically invoked when working with coroutines, Flow, StateFlow, SharedFlow, suspend functions, CoroutineScope, channels, or async patterns in Kotlin code. -tools: Read, Edit, Write, Bash, Grep, Glob, Task, WebFetch -model: sonnet ---- - -# Kotlin Coroutines Agent - -You are a Kotlin Coroutines expert specializing in async patterns, Flow, and structured concurrency. - -## Auto-Trigger Contexts - -Activate when user works with: -- `suspend` functions -- `Flow`, `StateFlow`, `SharedFlow` -- `CoroutineScope`, `launch`, `async` -- `Channel`, `channelFlow` -- Dispatchers configuration -- Exception handling in coroutines - -## Core Knowledge - -### Coroutine Builders -```kotlin -// launch: fire-and-forget -val job = launch { delay(1000); println("Done") } - -// async: returns result -val deferred = async { computeValue() } -val result = deferred.await() - -// Structured concurrency -suspend fun loadProfile(userId: String) = coroutineScope { - val user = async { fetchUser(userId) } - val notes = async { fetchNotes(userId) } - Profile(user.await(), notes.await()) -} -``` - -### Dispatchers - -| Dispatcher | Use Case | -|------------|----------| -| `Dispatchers.Main` | UI updates | -| `Dispatchers.IO` | Network, disk I/O | -| `Dispatchers.Default` | CPU-intensive | - -### Flow (Cold Streams) -```kotlin -fun observeNotes(): Flow> = flow { - while (true) { - emit(repository.getNotes()) - delay(30_000) - } -} - -// Operators -repository.observeNotes() - .map { it.filter { note -> note.isVisible } } - .distinctUntilChanged() - .catch { emit(emptyList()) } - .flowOn(Dispatchers.IO) - .collect { updateUI(it) } -``` - -### StateFlow (Hot, Always Has Value) -```kotlin -class FeedViewModel { - private val _state = MutableStateFlow(FeedState()) - val state: StateFlow = _state.asStateFlow() - - fun updateFilter(filter: Filter) { - _state.update { it.copy(filter = filter) } - } -} -``` - -### SharedFlow (Hot, Configurable Replay) -```kotlin -private val _events = MutableSharedFlow( - replay = 0, - extraBufferCapacity = 64, - onBufferOverflow = BufferOverflow.DROP_OLDEST -) -val events: SharedFlow = _events.asSharedFlow() -``` - -### Channels -```kotlin -fun relayEvents(relay: Relay): Flow = channelFlow { - relay.connect() - relay.onEvent { event -> trySend(event) } - awaitClose { relay.disconnect() } -} -``` - -### Exception Handling -```kotlin -val handler = CoroutineExceptionHandler { _, e -> - log.error("Coroutine failed", e) -} -val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default + handler) - -// supervisorScope: child failures don't cancel siblings -supervisorScope { - launch { task1() } // Can fail independently - launch { task2() } // Continues if task1 fails -} -``` - -## Nostr-Specific Patterns - -### Relay Connection Pool -```kotlin -class RelayPool(private val scope: CoroutineScope) { - fun connect(url: String) { - scope.launch { - supervisorScope { - launch { connection.receiveLoop() } - launch { connection.sendLoop() } - } - } - } - - fun observeEvents(): Flow = relays.values - .map { it.events } - .merge() - .distinctBy { it.id } -} -``` - -### Subscription Management -```kotlin -fun subscribe(filters: List): Flow = channelFlow { - val subId = UUID.randomUUID().toString() - try { - relayPool.activeRelays.collect { relays -> - relays.forEach { relay -> - launch { relay.subscribe(subId, filters).collect { send(it) } } - } - } - } finally { - relayPool.unsubscribe(subId) - } -} -``` - -## Workflow - -### 1. Assess Task -- Cold stream (Flow) or hot stream (StateFlow/SharedFlow)? -- Need structured concurrency? -- Error handling strategy? - -### 2. Investigate -```bash -# Find coroutine usage -grep -r "suspend \|launch\|async\|Flow<" quartz/src/ -# Check existing patterns -grep -r "CoroutineScope\|StateFlow" quartz/src/ -``` - -### 3. Implement -- Use appropriate dispatcher -- Implement proper cancellation -- Handle exceptions at right level -- Use structured concurrency - -### 4. Test -```kotlin -@Test -fun `test async operation`() = runTest { - val result = viewModel.loadData() - advanceUntilIdle() - assertEquals(expected, result) -} -``` - -## Constraints - -- Always use structured concurrency -- Never use `GlobalScope` -- Handle cancellation cooperatively (`ensureActive()`, `yield()`) -- Use `SupervisorJob` when children should fail independently -- Prefer Flow over callbacks -- Use `flowOn` to switch dispatchers, not `withContext` in flow diff --git a/.claude/agents/kotlin-multiplatform.md b/.claude/agents/kotlin-multiplatform.md deleted file mode 100644 index 419a7c740..000000000 --- a/.claude/agents/kotlin-multiplatform.md +++ /dev/null @@ -1,127 +0,0 @@ ---- -name: kotlin-multiplatform -description: Automatically invoked when working with KMP project structure, build.gradle.kts files, expect/actual declarations, source sets (commonMain, androidMain, jvmMain), or multiplatform migration tasks. Expert in code sharing across Android and Desktop JVM. -tools: Read, Edit, Write, Bash, Grep, Glob, Task, WebFetch -model: sonnet ---- - -# Kotlin Multiplatform Agent - -You are a Kotlin Multiplatform expert specializing in KMP architecture for Android and Desktop JVM targets. - -## Auto-Trigger Contexts - -Activate when user works with: -- `build.gradle.kts` files with multiplatform plugin -- Files in `commonMain/`, `androidMain/`, `jvmMain/` source sets -- `expect` or `actual` declarations -- Cross-platform library selection -- Migration from Android-only to multiplatform - -## Core Knowledge - -### Source Set Hierarchy -``` - commonMain - │ - ┌───────────┼───────────┐ - │ │ │ - jvmMain nativeMain jsMain - │ │ - ┌──────┴───┐ ┌───┴───┐ - │ │ │ │ -androidMain desktopMain iosMain -``` - -### expect/actual Pattern -```kotlin -// commonMain - Declaration -expect class PlatformContext -expect fun getPlatform(): Platform - -// androidMain -actual class PlatformContext(val context: Context) -actual fun getPlatform(): Platform = Platform.Android - -// jvmMain (Desktop) -actual class PlatformContext -actual fun getPlatform(): Platform = Platform.Desktop -``` - -### Platform Dependencies - -| Concern | Android | Desktop JVM | -|---------|---------|-------------| -| **Crypto** | secp256k1-kmp-jni-android | secp256k1-kmp-jni-jvm | -| **Storage** | Android KeyStore | Java KeyStore / File | -| **Sodium** | lazysodium-android | lazysodium-java | -| **UI** | Jetpack Compose | Compose Desktop | - -## Workflow - -### 1. Assess Task -- Identify if task involves shared vs platform-specific code -- Check existing source set structure -- Understand dependency requirements - -### 2. Investigate -```bash -# Check existing structure -ls -la quartz/src/ -# Find expect declarations -grep -r "expect " quartz/src/commonMain/ -# Find actual implementations -grep -r "actual " quartz/src/androidMain/ quartz/src/jvmMain/ -``` - -### 3. Implement -- Place shared code in `commonMain` -- Create `expect` declarations for platform APIs -- Implement `actual` in each target source set -- Update `build.gradle.kts` dependencies per source set - -### 4. Verify -```bash -./gradlew :quartz:build -./gradlew :desktopApp:compileKotlinJvm -``` - -## Quartz KMP Structure - -``` -quartz/src/ -├── commonMain/kotlin/ # Shared Nostr protocol -├── commonTest/kotlin/ # Shared tests -├── androidMain/kotlin/ # Android crypto, storage -└── jvmMain/kotlin/ # Desktop crypto, storage -``` - -## Key Abstractions for This Project - -- `CryptoProvider` - secp256k1 signing/verification -- `SodiumProvider` - NIP-44 encryption -- `SecureStorage` - Key storage -- `PlatformContext` - Platform-specific context - -## Constraints - -- Maximize code in `commonMain` (target 70-80%) -- Use KMP-compatible libraries only in commonMain -- Platform implementations must have identical signatures -- Run tests on all targets before completing - -## Resources - -Reference these GitHub repositories for KMP patterns and libraries: - -| Repository | Focus | Key Examples | -|------------|-------|--------------| -| [joreilly](https://github.com/joreilly) | KMP samples | PeopleInSpace, Confetti, GeminiKMP | -| [touchlab](https://github.com/touchlab) | KMP tooling | Kermit (logging), Stately (state), SKIE | -| [cashapp](https://github.com/cashapp) | KMP libraries | SQLDelight, Turbine, Molecule | - -**Useful libraries from these sources:** -- `SQLDelight` - Type-safe SQL for all platforms -- `Kermit` - Multiplatform logging -- `Turbine` - Testing Kotlin Flows -- `Molecule` - Build UI state with Compose diff --git a/.claude/agents/nostr-protocol.md b/.claude/agents/nostr-protocol.md deleted file mode 100644 index ea6073f66..000000000 --- a/.claude/agents/nostr-protocol.md +++ /dev/null @@ -1,168 +0,0 @@ ---- -name: nostr-protocol -description: Automatically invoked when working with Nostr events, NIPs, relay communication, cryptographic operations (signing, encryption), or Quartz library code for protocol implementation. -tools: Read, Edit, Write, Bash, Grep, Glob, Task, WebFetch -model: sonnet ---- - -# Nostr Protocol Agent - -You are a Nostr Protocol expert specializing in NIPs, events, relays, and cryptographic operations. - -## Auto-Trigger Contexts - -Activate when user works with: -- Event classes (kind, tags, content, sig) -- NIP implementations -- Relay WebSocket communication -- Cryptographic operations (secp256k1, NIP-44) -- Quartz library protocol code -- Filters and subscriptions - -## Core Knowledge - -### Event Structure -```kotlin -data class Event( - val id: String, // SHA256 of serialized event - val pubkey: String, // 32-byte hex public key - val created_at: Long, // Unix timestamp - val kind: Int, // Event type - val tags: List>, - val content: String, - val sig: String // 64-byte Schnorr signature -) - -// Event ID = SHA256([0, pubkey, created_at, kind, tags, content]) -``` - -### NIP Categories - -| Category | NIPs | Scope | -|----------|------|-------| -| **Core** | 01, 02, 10, 11 | Basic events, follows, threads, relay info | -| **Messaging** | 04, 17, 44 | DMs, encrypted messaging | -| **Social** | 18, 25, 32, 51 | Reactions, reports, lists | -| **Identity** | 05, 19, 39, 46 | DNS, bech32, bunker, signer | -| **Media** | 23, 30, 54, 94 | Long-form, audio, video, blobs | -| **Payments** | 47, 57, 60 | Wallet Connect, zaps, cashu | - -### Relay Messages -``` -Client -> Relay: - ["REQ", , ...] # Subscribe - ["EVENT", ] # Publish - ["CLOSE", ] # Unsubscribe - -Relay -> Client: - ["EVENT", , ] # Event received - ["EOSE", ] # End of stored events - ["OK", , , ] # Publish result - ["NOTICE", ] # Info message -``` - -### Cryptographic Operations - -**Signing (Schnorr/BIP-340)** -```kotlin -val signature = Secp256k1.sign( - data = eventHash, - privateKey = privateKeyBytes -) -``` - -**NIP-44 Encryption (Modern)** -```kotlin -val ciphertext = Nip44.encrypt( - plaintext = message, - sharedSecret = computeSharedSecret(myPrivKey, theirPubKey) -) -``` - -**NIP-04 Encryption (Deprecated)** -```kotlin -// AES-256-CBC - only for legacy compatibility -val ciphertext = Nip04.encrypt(message, sharedSecret) -``` - -### Common Event Kinds - -| Kind | NIP | Description | -|------|-----|-------------| -| 0 | 01 | Metadata (profile) | -| 1 | 01 | Short text note | -| 3 | 02 | Follows list | -| 4 | 04 | Encrypted DM (deprecated) | -| 7 | 25 | Reaction | -| 1984 | 32 | Report | -| 30023 | 23 | Long-form content | - -### Tag Patterns -```kotlin -// Reply threading (NIP-10) -tags = listOf( - listOf("e", rootEventId, relayUrl, "root"), - listOf("e", replyToId, relayUrl, "reply"), - listOf("p", authorPubkey) -) - -// Mentions -tags = listOf( - listOf("p", mentionedPubkey), - listOf("t", "hashtag") -) -``` - -## Workflow - -### 1. Assess Task -- Which NIP(s) are involved? -- Event creation or parsing? -- Relay communication? -- Crypto operations needed? - -### 2. Investigate -```bash -# Check existing NIP implementations -grep -r "kind.*=" quartz/src/ -# Find event classes -grep -r "class.*Event" quartz/src/ -# Check crypto usage -grep -r "Secp256k1\|Nip44\|sign\|verify" quartz/src/ -``` - -### 3. Reference NIP Spec -```bash -# Fetch NIP specification -curl https://raw.githubusercontent.com/nostr-protocol/nips/master/XX.md -``` - -### 4. Implement -- Follow NIP spec exactly -- Use existing Quartz patterns -- Validate event structure -- Test signature verification - -### 5. Verify -```bash -./gradlew :quartz:test -``` - -## Quartz Library Structure -``` -quartz/src/commonMain/kotlin/ -├── events/ # Event types per NIP -├── encoders/ # Bech32, hex encoding -├── crypto/ # Signing, encryption -├── relay/ # WebSocket communication -└── filters/ # Subscription filters -``` - -## Constraints - -- Always follow NIP specifications exactly -- Use NIP-44 for new encryption (not NIP-04) -- Validate all incoming events (sig, id) -- Never log private keys or decrypted content -- Use existing Quartz classes when available -- Test with real relay responses when possible diff --git a/.claude/skills/kotlin-coroutines/SKILL.md b/.claude/skills/kotlin-coroutines/SKILL.md new file mode 100644 index 000000000..f49d1dfcc --- /dev/null +++ b/.claude/skills/kotlin-coroutines/SKILL.md @@ -0,0 +1,419 @@ +--- +name: kotlin-coroutines +description: Advanced Kotlin coroutines patterns for AmethystMultiplatform. Use when working with: (1) Structured concurrency (supervisorScope, coroutineScope), (2) Advanced Flow operators (flatMapLatest, combine, merge, shareIn, stateIn), (3) Channels and callbackFlow, (4) Dispatcher management and context switching, (5) Exception handling (CoroutineExceptionHandler, SupervisorJob), (6) Testing async code (runTest, Turbine), (7) Nostr relay connection pools and subscriptions, (8) Backpressure handling in event streams. Delegates to kotlin-expert for basic StateFlow/SharedFlow patterns. Complements nostr-expert for relay communication. +--- + +# Kotlin Coroutines - Advanced Async Patterns + +Expert guidance for complex async operations in Amethyst: relay pools, event streams, structured concurrency, and testing. + +## Mental Model + +``` +Async Architecture in Amethyst: + +Relay Pool (supervisorScope) + ├── Relay 1 (launch) → callbackFlow → Events + ├── Relay 2 (launch) → callbackFlow → Events + └── Relay 3 (launch) → callbackFlow → Events + ↓ + merge() → distinctBy(id) → shareIn + ↓ + Multiple Collectors (ViewModels, Services) +``` + +**Key principles:** +- **supervisorScope** - Children fail independently +- **callbackFlow** - Bridge callbacks to Flow +- **shareIn/stateIn** - Hot flows from cold +- **Backpressure** - buffer(), conflate(), DROP_OLDEST + +## When to Use This Skill + +Use for **advanced** async patterns: +- Multi-relay subscriptions with supervisorScope +- Complex Flow operators (flatMapLatest, combine, merge) +- callbackFlow for Android callbacks (connectivity, location) +- Backpressure handling in high-frequency streams +- Exception handling with CoroutineExceptionHandler +- Testing coroutines with runTest and Turbine + +**Delegate to kotlin-expert for:** +- Basic StateFlow/SharedFlow patterns +- Simple viewModelScope.launch +- MutableStateFlow → asStateFlow() + +## Core Patterns + +### Pattern: callbackFlow for Relay Subscriptions + +```kotlin +// Real pattern from NostrClientStaticReqAsStateFlow.kt +fun INostrClient.reqAsFlow( + relay: NormalizedRelayUrl, + filters: List, +): Flow> = callbackFlow { + val subId = RandomInstance.randomChars(10) + var hasBeenLive = false + val eventIds = mutableSetOf() + var currentEvents = listOf() + + val listener = object : IRequestListener { + override fun onEvent(event: Event, ...) { + if (event.id !in eventIds) { + currentEvents = if (hasBeenLive) { + // After EOSE: prepend + listOf(event) + currentEvents + } else { + // Before EOSE: append + currentEvents + event + } + eventIds.add(event.id) + trySend(currentEvents) + } + } + + override fun onEose(...) { + hasBeenLive = true + } + } + + openReqSubscription(subId, mapOf(relay to filters), listener) + + awaitClose { close(subId) } +} +``` + +**Key techniques:** +1. Deduplication with Set +2. EOSE handling (append → prepend strategy) +3. trySend (non-blocking from callback) +4. awaitClose for cleanup + +### Pattern: Structured Concurrency for Relays + +```kotlin +suspend fun connectToRelays(relays: List) = supervisorScope { + relays.forEach { relay -> + launch { + try { + relay.connect() + relay.subscribe(filters).collect { event -> + eventChannel.send(event) + } + } catch (e: IOException) { + Log.e("Relay", "Connection failed: ${relay.url}", e) + // Other relays continue + } + } + } +} +``` + +**Why supervisorScope:** +- One relay failure doesn't cancel others +- All cancelled together when scope cancelled +- Proper cleanup guaranteed + +### Pattern: Exception Handling for Services + +```kotlin +// Real pattern from PushNotificationReceiverService.kt +class MyService : Service() { + val exceptionHandler = CoroutineExceptionHandler { _, throwable -> + Log.e("Service", "Caught: ${throwable.message}", throwable) + } + + private val scope = CoroutineScope( + Dispatchers.IO + SupervisorJob() + exceptionHandler + ) + + override fun onDestroy() { + scope.cancel() + super.onDestroy() + } +} +``` + +**Pattern benefits:** +- SupervisorJob: children fail independently +- ExceptionHandler: log instead of crash +- Scoped lifecycle: cancel all on destroy + +### Pattern: Network Connectivity as Flow + +```kotlin +// Real pattern from ConnectivityFlow.kt +val status = callbackFlow { + val networkCallback = object : NetworkCallback() { + override fun onAvailable(network: Network) { + trySend(ConnectivityStatus.Active(...)) + } + override fun onLost(network: Network) { + trySend(ConnectivityStatus.Off) + } + } + + connectivityManager.registerCallback(networkCallback) + + // Initial state + activeNetwork?.let { trySend(ConnectivityStatus.Active(...)) } + + awaitClose { + connectivityManager.unregisterCallback(networkCallback) + } +} + .distinctUntilChanged() + .debounce(200) // Stabilize flapping + .flowOn(Dispatchers.IO) +``` + +**Key patterns:** +1. Emit initial state immediately +2. Register callback in flow body +3. Cleanup in awaitClose +4. Stabilize with debounce + distinctUntilChanged + +### Pattern: Merge Events from Multiple Relays + +```kotlin +fun observeFromRelays( + relays: List, + filters: List +): Flow = + relays.map { relay -> + client.reqAsFlow(relay, filters) + .flatMapConcat { it.asFlow() } + }.merge() + .distinctBy { it.id } +``` + +**Flow:** +- Each relay: `Flow>` +- flatMapConcat: flatten to `Flow` +- merge(): combine all relays +- distinctBy: deduplicate across relays + +## Advanced Operators + +For comprehensive coverage of Flow operators: +- **flatMapLatest, combine, zip, merge** → See [advanced-flow-operators.md](references/advanced-flow-operators.md) +- **shareIn, stateIn** → Conversion to hot flows +- **buffer, conflate** → Backpressure strategies +- **debounce, sample** → Rate limiting + +### Quick Reference + +| Operator | Use Case | Example | +|----------|----------|---------| +| **flatMapLatest** | Cancel previous, switch to new | Search (cancel old query) | +| **combine** | Latest from ALL flows | combine(account, settings, connectivity) | +| **merge** | Single stream from multiple | merge(relay1, relay2, relay3) | +| **shareIn** | Multiple collectors, single upstream | Share expensive computation | +| **stateIn** | StateFlow from Flow | ViewModel state | +| **buffer(DROP_OLDEST)** | High-frequency streams | Real-time event feed | +| **conflate** | Latest only | UI updates | +| **debounce** | Wait for quiet period | Search input | + +## Nostr Relay Patterns + +For complete relay-specific patterns: +→ See [relay-patterns.md](references/relay-patterns.md) + +Covers: +- Multi-relay subscription management +- Connection lifecycle and reconnection +- Event deduplication strategies +- Backpressure for high-frequency events +- EOSE handling patterns + +## Testing + +For comprehensive testing patterns: +→ See [testing-coroutines.md](references/testing-coroutines.md) + +**Quick testing pattern:** + +```kotlin +@Test +fun `relay subscription receives events`() = runTest { + val client = FakeNostrClient() + + client.reqAsFlow(relay, filters).test { + assertEquals(emptyList(), awaitItem()) + + client.sendEvent(event1) + assertEquals(listOf(event1), awaitItem()) + + cancelAndIgnoreRemainingEvents() + } +} +``` + +**Testing tools:** +- `runTest` - Virtual time, auto cleanup +- Turbine `.test {}` - Flow assertions +- `advanceTimeBy()` - Control time +- Fake implementations over mocks + +## Common Scenarios + +### Scenario: Implement New Relay Feature + +**Steps:** +1. callbackFlow for subscription +2. Deduplication (Set of event IDs) +3. awaitClose for cleanup +4. Test with FakeNostrClient + +**Example:** Add subscription for specific event kind + +```kotlin +fun observeKind(kind: Int): Flow = callbackFlow { + val listener = object : IRequestListener { + override fun onEvent(event: Event, ...) { + if (event.kind == kind) { + trySend(event) + } + } + } + client.subscribe(listener) + awaitClose { client.unsubscribe(listener) } +} +``` + +### Scenario: Handle Network Connectivity Changes + +**Steps:** +1. callbackFlow for connectivity +2. flatMapLatest to reconnect +3. debounce to stabilize +4. Exception handling for failures + +**Example:** Reconnect relays on connectivity + +```kotlin +connectivityFlow + .flatMapLatest { status -> + when (status) { + Active -> relayPool.observeEvents() + else -> emptyFlow() + } + } + .catch { e -> Log.e("Error", e) } + .collect { event -> handleEvent(event) } +``` + +### Scenario: Optimize Multi-Collector Performance + +**Steps:** +1. Use shareIn for expensive upstream +2. Configure SharingStarted strategy +3. Set replay buffer size +4. Test with multiple collectors + +**Example:** Share relay subscription + +```kotlin +val events: SharedFlow = client + .reqAsFlow(relay, filters) + .flatMapConcat { it.asFlow() } + .shareIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + replay = 0 + ) +``` + +## Anti-Patterns + +❌ **Using GlobalScope** +```kotlin +GlobalScope.launch { /* Leaks, no structured concurrency */ } +``` + +✅ **Use scoped coroutines** +```kotlin +viewModelScope.launch { /* Cancelled with ViewModel */ } +``` + +--- + +❌ **Forgetting awaitClose** +```kotlin +callbackFlow { + registerCallback() + // Missing cleanup! +} +``` + +✅ **Always cleanup** +```kotlin +callbackFlow { + registerCallback() + awaitClose { unregisterCallback() } +} +``` + +--- + +❌ **Blocking in Flow** +```kotlin +flow.map { Thread.sleep(1000); process(it) } +``` + +✅ **Suspend, don't block** +```kotlin +flow.map { delay(1000); process(it) }.flowOn(Dispatchers.Default) +``` + +--- + +❌ **Ignoring backpressure** +```kotlin +fastProducer.collect { slowConsumer(it) } // Blocks producer! +``` + +✅ **Handle backpressure** +```kotlin +fastProducer + .buffer(64, BufferOverflow.DROP_OLDEST) + .collect { slowConsumer(it) } +``` + +## Delegation + +**Use kotlin-expert for:** +- Basic StateFlow/SharedFlow patterns +- viewModelScope.launch usage +- Simple MutableStateFlow → asStateFlow() + +**Use nostr-expert for:** +- Nostr protocol details (NIPs, event structure) +- Event creation and signing +- Cryptographic operations + +**This skill provides:** +- Advanced async patterns +- Structured concurrency +- Complex Flow operators +- Testing strategies +- Relay-specific async patterns + +## Resources + +- **references/advanced-flow-operators.md** - All Flow operators with examples +- **references/relay-patterns.md** - Nostr relay async patterns from codebase +- **references/testing-coroutines.md** - Complete testing guide + +## Quick Decision Tree + +``` +Need async operation? + ├─ Simple ViewModel state update → kotlin-expert (StateFlow) + ├─ Android callback → This skill (callbackFlow) + ├─ Multiple concurrent operations → This skill (supervisorScope) + ├─ Complex Flow transformation → This skill (references/advanced-flow-operators.md) + ├─ Relay subscription → This skill (references/relay-patterns.md) + └─ Testing async code → This skill (references/testing-coroutines.md) +``` diff --git a/.claude/skills/kotlin-coroutines/references/advanced-flow-operators.md b/.claude/skills/kotlin-coroutines/references/advanced-flow-operators.md new file mode 100644 index 000000000..d7cee8c45 --- /dev/null +++ b/.claude/skills/kotlin-coroutines/references/advanced-flow-operators.md @@ -0,0 +1,309 @@ +# Advanced Flow Operators + +Comprehensive guide to Flow operators for complex async patterns in Amethyst. + +## Transformation Operators + +### flatMapLatest - Cancel Previous, Switch to New + +**Use when:** Latest value matters, previous operations should cancel + +```kotlin +// User types in search box → cancel previous search +searchQuery + .flatMapLatest { query -> + repository.search(query) // Cancels previous search + } + .collect { results -> updateUI(results) } +``` + +**Amethyst pattern:** +```kotlin +// Switch relays based on latest account +accountFlow + .flatMapLatest { account -> + relayPool.observeEvents(account.relays) + } +``` + +### flatMapConcat - Sequential Processing + +**Use when:** Order matters, process one at a time + +```kotlin +eventIds + .flatMapConcat { id -> + repository.fetchEvent(id) + } + .collect { event -> process(event) } +``` + +### flatMapMerge - Concurrent Processing + +**Use when:** Process multiple simultaneously, order doesn't matter + +```kotlin +relays + .flatMapMerge(concurrency = 10) { relay -> + relay.subscribe(filters) + } + .collect { event -> handleEvent(event) } +``` + +## Combination Operators + +### combine - Latest from Multiple Flows + +**Use when:** Need latest value from ALL flows + +```kotlin +combine( + accountFlow, + settingsFlow, + connectivityFlow +) { account, settings, connectivity -> + AppState(account, settings, connectivity) +}.collect { state -> render(state) } +``` + +**Pattern:** Re-emits whenever ANY source emits + +### zip - Pair Values in Order + +**Use when:** Need corresponding values from flows + +```kotlin +zip(requestFlow, responseFlow) { req, res -> + Pair(req, res) +} +``` + +**Pattern:** Waits for BOTH to emit before pairing + +### merge - Combine Multiple Flows + +**Use when:** Treat multiple flows as single stream + +```kotlin +merge( + relay1.events, + relay2.events, + relay3.events +).collect { event -> handleEvent(event) } +``` + +## Backpressure & Buffering + +### shareIn - Hot Flow from Cold + +**Use when:** Multiple collectors should share single upstream + +```kotlin +val sharedEvents = repository.observeEvents() + .shareIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + replay = 0 + ) + +// Multiple collectors share same upstream +sharedEvents.collect { /* collector 1 */ } +sharedEvents.collect { /* collector 2 */ } +``` + +**SharingStarted strategies:** +- `Eagerly` - Start immediately, never stop +- `Lazily` - Start on first subscriber, never stop +- `WhileSubscribed(stopTimeout)` - Stop after last unsubscribe + timeout + +### stateIn - StateFlow from Cold Flow + +**Use when:** Convert Flow to StateFlow (always has value) + +```kotlin +val uiState: StateFlow = repository.observeData() + .map { data -> UiState.Success(data) } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = UiState.Loading + ) +``` + +**Amethyst pattern:** +```kotlin +// Connectivity status as StateFlow +val connectivity: StateFlow = + connectivityFlow.status + .stateIn( + scope = serviceScope, + started = SharingStarted.Eagerly, + initialValue = ConnectivityStatus.Off + ) +``` + +### buffer - Control Backpressure + +**Use when:** Producer faster than consumer + +```kotlin +eventFlow + .buffer(capacity = 64, onBufferOverflow = BufferOverflow.DROP_OLDEST) + .collect { event -> slowProcessor(event) } +``` + +**Strategies:** +- `SUSPEND` - Slow down producer (default) +- `DROP_OLDEST` - Drop oldest in buffer +- `DROP_LATEST` - Drop newest emission + +### conflate - Keep Only Latest + +**Use when:** Only latest value matters, skip intermediate + +```kotlin +locationFlow + .conflate() // Skip intermediate locations + .collect { location -> updateMap(location) } +``` + +## Debouncing & Throttling + +### debounce - Wait for Quiet Period + +**Use when:** Wait for user to stop typing + +```kotlin +searchQuery + .debounce(300) // Wait 300ms after last emission + .flatMapLatest { query -> search(query) } +``` + +**Amethyst pattern:** +```kotlin +// ConnectivityFlow.kt:87 +connectivityFlow + .distinctUntilChanged() + .debounce(200) // Wait 200ms for network to stabilize + .flowOn(Dispatchers.IO) +``` + +### sample - Periodic Sampling + +**Use when:** Rate-limit high-frequency emissions + +```kotlin +sensorData + .sample(1000) // Sample every 1 second + .collect { data -> process(data) } +``` + +## Error Handling + +### catch - Handle Upstream Errors + +**Use when:** Graceful degradation needed + +```kotlin +repository.fetchData() + .catch { e -> + Log.e("Error", e) + emit(emptyList()) // Fallback value + } + .collect { data -> updateUI(data) } +``` + +**Pattern:** Only catches UPSTREAM errors, not in collect block + +### retry/retryWhen - Automatic Retry + +```kotlin +relayConnection + .retry(3) { cause -> + cause is IOException // Only retry on network errors + } +``` + +## Context Switching + +### flowOn - Change Upstream Dispatcher + +**Use when:** Offload work from current context + +```kotlin +repository.fetchData() + .map { heavyProcessing(it) } + .flowOn(Dispatchers.Default) // Heavy work on Default + .collect { updateUI(it) } // Collect on Main +``` + +**Critical:** Only affects UPSTREAM operators + +**Amethyst pattern:** +```kotlin +// ConnectivityFlow.kt:87 +callbackFlow { /* ... */ } + .distinctUntilChanged() + .debounce(200) + .flowOn(Dispatchers.IO) // All upstream on IO +``` + +## Common Patterns + +### Pattern: Multi-Relay Subscription + +```kotlin +fun observeFromMultipleRelays(relays: List, filters: List): Flow = + relays.map { relay -> + relay.subscribe(filters) + }.merge() + .distinctBy { it.id } +``` + +### Pattern: Load + Cache + Observe + +```kotlin +fun observeWithCache(id: String): Flow = flow { + // Emit cached value immediately + cache[id]?.let { emit(it) } + + // Then observe updates + emitAll(repository.observe(id)) +}.distinctUntilChanged() +``` + +### Pattern: Retry with Exponential Backoff + +```kotlin +fun Flow.retryWithBackoff( + maxRetries: Int = 3, + initialDelay: Long = 1000 +): Flow = retryWhen { cause, attempt -> + if (attempt >= maxRetries || cause !is IOException) { + false + } else { + delay(initialDelay * (1L shl attempt.toInt())) + true + } +} +``` + +## Performance Tips + +1. **Use shareIn for expensive operations** + - Compute once, share with multiple collectors + +2. **Choose right backpressure strategy** + - UI updates: `conflate()` or `DROP_OLDEST` + - Events: `buffer()` with appropriate size + +3. **flowOn placement matters** + - Place after expensive operators to offload them + +4. **Avoid unnecessary emissions** + - Use `distinctUntilChanged()` when appropriate + - Consider `debounce()` for high-frequency sources + +5. **StateFlow vs SharedFlow** + - StateFlow: Always has value, conflates + - SharedFlow: Optional replay, configurable buffering diff --git a/.claude/skills/kotlin-coroutines/references/relay-patterns.md b/.claude/skills/kotlin-coroutines/references/relay-patterns.md new file mode 100644 index 000000000..2b8f46af3 --- /dev/null +++ b/.claude/skills/kotlin-coroutines/references/relay-patterns.md @@ -0,0 +1,480 @@ +# Nostr Relay Async Patterns + +Proven coroutine patterns for Nostr relay connections, subscriptions, and event streaming in Amethyst. + +## Core Pattern: callbackFlow for Relay Subscriptions + +### Pattern: Subscription as Flow + +**Real implementation from NostrClientStaticReqAsStateFlow.kt:** + +```kotlin +fun INostrClient.reqAsFlow( + relay: NormalizedRelayUrl, + filters: List, +): Flow> = + callbackFlow { + val subId = RandomInstance.randomChars(10) + var hasBeenLive = false + val eventIds = mutableSetOf() + var currentEvents = listOf() + + val listener = object : IRequestListener { + override fun onEvent( + event: Event, + isLive: Boolean, + relay: NormalizedRelayUrl, + forFilters: List?, + ) { + if (event.id !in eventIds) { + if (hasBeenLive) { + // After EOSE: prepend new events + val list = ArrayList(1 + currentEvents.size) + list.add(event) + list.addAll(currentEvents) + currentEvents = list + } else { + // Before EOSE: append events + currentEvents = currentEvents + event + } + eventIds.add(event.id) + trySend(currentEvents) + } + } + + override fun onEose( + relay: NormalizedRelayUrl, + forFilters: List?, + ) { + hasBeenLive = true + } + } + + openReqSubscription(subId, mapOf(relay to filters), listener) + + awaitClose { + close(subId) + } + } +``` + +**Key techniques:** +1. **callbackFlow** - Bridge callback API to Flow +2. **Deduplication** - `eventIds` set prevents duplicates +3. **EOSE handling** - Changes insertion strategy (append → prepend) +4. **awaitClose** - Cleanup when flow cancelled +5. **trySend** - Non-blocking emission from callback + +## Multi-Relay Patterns + +### Pattern: Merge Events from Multiple Relays + +```kotlin +fun observeFromRelays( + relays: List, + filters: List +): Flow = + relays.map { relay -> + client.reqAsFlow(relay, filters) + .flatMapConcat { it.asFlow() } + }.merge() + .distinctBy { it.id } +``` + +**Explanation:** +- Each relay produces `Flow>` +- `flatMapConcat` flattens to `Flow` +- `merge()` combines all relay flows +- `distinctBy` deduplicates across relays + +### Pattern: Concurrent Relay Operations with supervisorScope + +```kotlin +suspend fun subscribeToRelays( + relays: List, + filters: List +) = supervisorScope { + relays.forEach { relay -> + launch { + relay.subscribe(filters).collect { event -> + eventChannel.send(event) + } + } + } +} +``` + +**Why supervisorScope:** +- If one relay fails, others continue +- All children cancelled when scope cancelled +- Structured concurrency maintained + +## Backpressure Handling + +### Pattern: Buffer with Drop Strategy + +**For high-frequency event streams:** + +```kotlin +relayFlow + .buffer( + capacity = 64, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + .collect { event -> processEvent(event) } +``` + +**Strategy selection:** +- `DROP_OLDEST` - For real-time feeds (lose old events OK) +- `DROP_LATEST` - For priority queues (lose new events OK) +- `SUSPEND` - For critical events (slow down producer) + +### Pattern: Conflate for UI Updates + +```kotlin +val uiEvents: Flow = relayEvents + .map { event -> toUiEvent(event) } + .conflate() // Skip intermediate, show latest + .flowOn(Dispatchers.Default) +``` + +## Connection Management + +### Pattern: Network Connectivity as Flow + +**Real implementation from ConnectivityFlow.kt:** + +```kotlin +@OptIn(FlowPreview::class) +val status = callbackFlow { + trySend(ConnectivityStatus.StartingService) + + val connectivityManager = context.getConnectivityManager() + + val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + connectivityManager.getNetworkCapabilities(network)?.let { + trySend(ConnectivityStatus.Active( + network.networkHandle, + it.isMeteredOrMobileData() + )) + } + } + + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities + ) { + val isMobile = networkCapabilities.isMeteredOrMobileData() + trySend(ConnectivityStatus.Active( + network.networkHandle, + isMobile + )) + } + + override fun onLost(network: Network) { + trySend(ConnectivityStatus.Off) + } + } + + connectivityManager.registerDefaultNetworkCallback(networkCallback) + + // Send initial state + connectivityManager.activeNetwork?.let { network -> + connectivityManager.getNetworkCapabilities(network)?.let { + trySend(ConnectivityStatus.Active( + network.networkHandle, + it.isMeteredOrMobileData() + )) + } + } + + awaitClose { + connectivityManager.unregisterNetworkCallback(networkCallback) + trySend(ConnectivityStatus.Off) + } +} + .distinctUntilChanged() + .debounce(200) // Stabilize rapid changes + .flowOn(Dispatchers.IO) +``` + +**Key patterns:** +1. **Initial state** - Emit current connectivity immediately +2. **Callback registration** - Register listener in flow body +3. **Cleanup** - Unregister in `awaitClose` +4. **Stabilization** - `debounce(200)` prevents flapping +5. **Deduplication** - `distinctUntilChanged()` skips redundant updates + +### Pattern: Reconnect on Connectivity Change + +```kotlin +connectivityFlow + .flatMapLatest { status -> + when (status) { + is ConnectivityStatus.Active -> { + relayPool.connectAll() + relayPool.observeEvents() + } + else -> emptyFlow() + } + } + .collect { event -> handleEvent(event) } +``` + +## Exception Handling in Async Operations + +### Pattern: CoroutineExceptionHandler + SupervisorJob + +**Real implementation from PushNotificationReceiverService.kt:** + +```kotlin +class PushNotificationReceiverService : FirebaseMessagingService() { + // Catch all uncaught exceptions + val exceptionHandler = CoroutineExceptionHandler { _, throwable -> + Log.e("AmethystCoroutine", "Caught exception: ${throwable.message}", throwable) + } + + // Children fail independently, handler catches all + private val scope = CoroutineScope( + Dispatchers.IO + SupervisorJob() + exceptionHandler + ) + + override fun onMessageReceived(remoteMessage: RemoteMessage) { + scope.launch(Dispatchers.IO) { + parseMessage(remoteMessage.data)?.let { receiveIfNew(it) } + } + } + + override fun onDestroy() { + scope.cancel() + super.onDestroy() + } +} +``` + +**Why this pattern:** +- **SupervisorJob** - One failure doesn't cancel others +- **ExceptionHandler** - Log exceptions, don't crash +- **Scoped lifecycle** - Cancel all on destroy + +### Pattern: Retry with Backoff for Relay Connections + +```kotlin +fun connectWithRetry(relay: Relay): Flow = flow { + var attempt = 0 + val maxRetries = 5 + val baseDelay = 1000L + + while (attempt < maxRetries) { + try { + emit(ConnectionStatus.Connecting) + relay.connect() + emit(ConnectionStatus.Connected) + return@flow + } catch (e: Exception) { + attempt++ + emit(ConnectionStatus.Error(e, attempt)) + + if (attempt < maxRetries) { + val delay = baseDelay * (1L shl attempt) // Exponential backoff + delay(delay) + } + } + } + emit(ConnectionStatus.Failed) +} +``` + +## Subscription Lifecycle + +### Pattern: Auto-Cleanup Subscription + +```kotlin +@Composable +fun ObserveRelayEvents( + filters: List, + onEvent: (Event) -> Unit +) { + val scope = rememberCoroutineScope() + + DisposableEffect(filters) { + val job = scope.launch { + relayClient.reqAsFlow(filters).collect { events -> + events.forEach { onEvent(it) } + } + } + + onDispose { + job.cancel() // Cancels flow, triggers awaitClose + } + } +} +``` + +**Lifecycle:** +1. Composable enters → subscribe +2. filters change → cancel + re-subscribe +3. Composable leaves → cancel + cleanup + +### Pattern: Multiple Concurrent Subscriptions + +```kotlin +fun observeMultipleFeeds( + account: Account +): Flow = channelFlow { + supervisorScope { + // Home feed + launch { + client.reqAsFlow(filters = homeFeedFilters) + .collect { events -> events.forEach { send(it) } } + } + + // Notifications + launch { + client.reqAsFlow(filters = notificationFilters) + .collect { events -> events.forEach { send(it) } } + } + + // DMs + launch { + client.reqAsFlow(filters = dmFilters) + .collect { events -> events.forEach { send(it) } } + } + } +} +``` + +**Benefits:** +- All subscriptions run concurrently +- One failure doesn't affect others (supervisorScope) +- Single output channel for all events + +## Performance Optimization + +### Pattern: Shared Upstream for Multiple Collectors + +```kotlin +class RelayViewModel(private val client: INostrClient) : ViewModel() { + val events: SharedFlow = client + .reqAsFlow(relay, filters) + .flatMapConcat { it.asFlow() } + .shareIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + replay = 0 + ) +} + +// Multiple collectors share single relay subscription +events.collect { /* UI 1 */ } +events.collect { /* UI 2 */ } +``` + +### Pattern: Event Deduplication Cache + +```kotlin +class EventCache { + private val seen = mutableSetOf() + + fun filterNew(events: List): List = + events.filter { event -> + if (event.id in seen) { + false + } else { + seen.add(event.id) + true + } + } +} + +val deduplicatedEvents = relayEvents + .map { events -> cache.filterNew(events) } + .filter { it.isNotEmpty() } +``` + +## Testing Relay Flows + +### Pattern: Test with Fake Relay + +```kotlin +@Test +fun `subscription receives events`() = runTest { + val fakeRelay = FakeRelay() + val client = NostrClient(fakeRelay) + + val events = mutableListOf() + val job = launch { + client.reqAsFlow(relay, filters).collect { list -> + events.addAll(list) + } + } + + // Simulate relay responses + fakeRelay.sendEvent(testEvent1) + advanceTimeBy(100) + fakeRelay.sendEvent(testEvent2) + advanceTimeBy(100) + + assertEquals(2, events.size) + job.cancel() +} +``` + +## Common Pitfalls + +### ❌ Forgetting awaitClose + +```kotlin +// BAD: Subscription never cleaned up +callbackFlow { + relay.subscribe(listener) + // Missing awaitClose! +} +``` + +```kotlin +// GOOD: Proper cleanup +callbackFlow { + relay.subscribe(listener) + awaitClose { + relay.unsubscribe(listener) + } +} +``` + +### ❌ Using GlobalScope + +```kotlin +// BAD: Unstructured, leaks +GlobalScope.launch { + relay.connect() +} +``` + +```kotlin +// GOOD: Scoped to lifecycle +viewModelScope.launch { + relay.connect() +} +``` + +### ❌ Blocking in Flow Operators + +```kotlin +// BAD: Blocks collector +flow.map { event -> + Thread.sleep(1000) // Blocks! + process(event) +} +``` + +```kotlin +// GOOD: Use flowOn to offload +flow + .map { event -> + delay(1000) // Suspends, doesn't block + process(event) + } + .flowOn(Dispatchers.Default) +``` diff --git a/.claude/skills/kotlin-coroutines/references/testing-coroutines.md b/.claude/skills/kotlin-coroutines/references/testing-coroutines.md new file mode 100644 index 000000000..a2083c49c --- /dev/null +++ b/.claude/skills/kotlin-coroutines/references/testing-coroutines.md @@ -0,0 +1,493 @@ +# Testing Coroutines + +Comprehensive guide for testing async code with runTest, Turbine, and best practices. + +## runTest - Standard Testing + +### Basic Pattern + +```kotlin +@Test +fun `test suspend function`() = runTest { + val result = repository.fetchData() + assertEquals(expected, result) +} +``` + +**What runTest does:** +- Skips delays automatically +- Provides TestScope +- Advances virtual time +- Waits for all coroutines to complete + +### Testing StateFlow + +```kotlin +@Test +fun `stateflow updates correctly`() = runTest { + val viewModel = MyViewModel() + + // Initial state + assertEquals(UiState.Loading, viewModel.state.value) + + // Trigger action + viewModel.loadData() + advanceUntilIdle() // Run all pending coroutines + + // Verify final state + assertEquals(UiState.Success(data), viewModel.state.value) +} +``` + +### Testing with Time Control + +```kotlin +@Test +fun `debounce works correctly`() = runTest { + val viewModel = SearchViewModel() + + viewModel.search("a") + advanceTimeBy(100) // 100ms passed + + viewModel.search("ab") + advanceTimeBy(100) + + viewModel.search("abc") + advanceTimeBy(300) // Debounce completes + + // Only "abc" should have triggered search + assertEquals(listOf("abc"), viewModel.searchQueries) +} +``` + +**Time control functions:** +- `advanceTimeBy(millis)` - Move virtual time forward +- `advanceUntilIdle()` - Run all pending work +- `runCurrent()` - Run currently scheduled tasks only + +## Turbine - Flow Testing Library + +### Basic Collection Testing + +```kotlin +@Test +fun `flow emits expected values`() = runTest { + repository.observeData().test { + assertEquals(Item1, awaitItem()) + assertEquals(Item2, awaitItem()) + assertEquals(Item3, awaitItem()) + awaitComplete() + } +} +``` + +### Testing Flow Transformations + +```kotlin +@Test +fun `map transforms correctly`() = runTest { + val source = flowOf(1, 2, 3) + + source + .map { it * 2 } + .test { + assertEquals(2, awaitItem()) + assertEquals(4, awaitItem()) + assertEquals(6, awaitItem()) + awaitComplete() + } +} +``` + +### Testing Relay Subscriptions + +```kotlin +@Test +fun `relay subscription receives events`() = runTest { + val fakeClient = FakeNostrClient() + + fakeClient.reqAsFlow(relay, filters).test { + // Initially empty + assertEquals(emptyList(), awaitItem()) + + // Send event + fakeClient.sendEvent(event1) + assertEquals(listOf(event1), awaitItem()) + + // Send another + fakeClient.sendEvent(event2) + assertEquals(listOf(event1, event2), awaitItem()) + + cancelAndIgnoreRemainingEvents() + } +} +``` + +### Testing Error Handling + +```kotlin +@Test +fun `catch handles errors gracefully`() = runTest { + val errorFlow = flow { + emit(1) + throw IOException("Network error") + }.catch { emit(-1) } // Fallback value + + errorFlow.test { + assertEquals(1, awaitItem()) + assertEquals(-1, awaitItem()) + awaitComplete() + } +} +``` + +### Testing StateFlow with Turbine + +```kotlin +@Test +fun `stateflow emits updates`() = runTest { + val viewModel = MyViewModel() + + viewModel.state.test { + // Skip initial value + assertEquals(UiState.Loading, awaitItem()) + + // Trigger update + viewModel.loadData() + assertEquals(UiState.Success(data), awaitItem()) + + cancelAndIgnoreRemainingEvents() + } +} +``` + +**Turbine assertions:** +- `awaitItem()` - Get next emission or fail +- `awaitComplete()` - Verify flow completed +- `awaitError()` - Verify flow threw exception +- `expectNoEvents()` - Assert no emissions in timeframe +- `cancelAndIgnoreRemainingEvents()` - Stop test + +## Testing Patterns for Amethyst + +### Pattern: Test Relay Connection Flow + +```kotlin +@Test +fun `reconnects on connectivity change`() = runTest { + val connectivityFlow = MutableStateFlow(ConnectivityStatus.Off) + val relayPool = FakeRelayPool() + + connectivityFlow + .flatMapLatest { status -> + when (status) { + is ConnectivityStatus.Active -> relayPool.connectAll() + else -> emptyFlow() + } + } + .test { + // Initially offline + expectNoEvents() + + // Go online + connectivityFlow.value = ConnectivityStatus.Active(1L, false) + assertTrue(relayPool.connected) + + cancelAndIgnoreRemainingEvents() + } +} +``` + +### Pattern: Test Event Deduplication + +```kotlin +@Test +fun `deduplicates events across relays`() = runTest { + val relay1 = FakeRelay() + val relay2 = FakeRelay() + + merge(relay1.events, relay2.events) + .distinctBy { it.id } + .test { + // Both relays send same event + relay1.send(event1) + relay2.send(event1) + + // Only one emission + assertEquals(event1, awaitItem()) + expectNoEvents() + + cancelAndIgnoreRemainingEvents() + } +} +``` + +### Pattern: Test Backpressure Handling + +```kotlin +@Test +fun `drops oldest events when buffer full`() = runTest { + val fastProducer = flow { + repeat(100) { emit(it) } + } + + fastProducer + .buffer(capacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST) + .test { + // Slow consumer + delay(100) + + // Should have dropped oldest, kept newest + val items = mutableListOf() + repeat(10) { + items.add(awaitItem()) + } + + // Newest items present + assertTrue(90 in items) + assertTrue(99 in items) + + awaitComplete() + } +} +``` + +### Pattern: Test Concurrent Subscriptions + +```kotlin +@Test +fun `multiple subscriptions run concurrently`() = runTest { + val client = FakeNostrClient() + + val feed1 = async { client.reqAsFlow(relay1, filters1).first() } + val feed2 = async { client.reqAsFlow(relay2, filters2).first() } + + client.sendTo(relay1, event1) + client.sendTo(relay2, event2) + + assertEquals(listOf(event1), feed1.await()) + assertEquals(listOf(event2), feed2.await()) +} +``` + +## Fakes and Mocks + +### Fake NostrClient + +```kotlin +class FakeNostrClient : INostrClient { + private val subscriptions = mutableMapOf>() + + override fun reqAsFlow( + relay: NormalizedRelayUrl, + filters: List + ): Flow> = callbackFlow { + val subId = RandomInstance.randomChars(10) + val flow = MutableSharedFlow() + subscriptions[subId] = flow + + val events = mutableListOf() + flow.collect { event -> + events.add(event) + send(events.toList()) + } + + awaitClose { + subscriptions.remove(subId) + } + } + + fun sendEvent(event: Event) { + subscriptions.values.forEach { it.tryEmit(event) } + } + + fun sendTo(relay: NormalizedRelayUrl, event: Event) { + subscriptions[relay.url]?.tryEmit(event) + } +} +``` + +### Fake Relay Pool + +```kotlin +class FakeRelayPool { + var connected = false + private val _events = MutableSharedFlow() + val events: SharedFlow = _events.asSharedFlow() + + fun connectAll(): Flow = flow { + connected = true + emit(Unit) + } + + fun disconnect() { + connected = false + } + + suspend fun sendEvent(event: Event) { + _events.emit(event) + } +} +``` + +## Testing Exception Handling + +### Test CoroutineExceptionHandler + +```kotlin +@Test +fun `exception handler catches errors`() = runTest { + val errors = mutableListOf() + + val handler = CoroutineExceptionHandler { _, throwable -> + errors.add(throwable) + } + + val scope = CoroutineScope( + Dispatchers.Unconfined + SupervisorJob() + handler + ) + + scope.launch { + throw IOException("Test error") + } + + advanceUntilIdle() + + assertEquals(1, errors.size) + assertTrue(errors[0] is IOException) +} +``` + +### Test Retry Logic + +```kotlin +@Test +fun `retries failed connections`() = runTest { + var attempts = 0 + val maxRetries = 3 + + flow { + attempts++ + if (attempts < maxRetries) { + throw IOException("Connection failed") + } + emit("Success") + } + .retry(maxRetries) + .test { + assertEquals("Success", awaitItem()) + awaitComplete() + assertEquals(3, attempts) + } +} +``` + +## Common Testing Patterns + +### Pattern: Verify No Emissions After Cancellation + +```kotlin +@Test +fun `no emissions after cancellation`() = runTest { + val flow = flow { + emit(1) + delay(1000) + emit(2) // Should not emit + } + + flow.test { + assertEquals(1, awaitItem()) + cancel() + + // Verify no more emissions + expectNoEvents() + } +} +``` + +### Pattern: Test Time-Based Operations + +```kotlin +@Test +fun `periodic emission works`() = runTest { + flow { + repeat(3) { + emit(it) + delay(1000) + } + }.test { + assertEquals(0, awaitItem()) + + advanceTimeBy(1000) + assertEquals(1, awaitItem()) + + advanceTimeBy(1000) + assertEquals(2, awaitItem()) + + awaitComplete() + } +} +``` + +### Pattern: Test Hot Flow Conversion + +```kotlin +@Test +fun `shareIn creates hot flow`() = runTest { + var emissions = 0 + val source = flow { + repeat(3) { + emissions++ + emit(it) + } + } + + val shared = source.shareIn( + scope = this, + started = SharingStarted.Eagerly, + replay = 1 + ) + + // First collector + shared.take(2).collect() + assertEquals(2, emissions) // Emitted 0, 1 + + // Second collector - shares upstream + shared.take(1).collect() + assertEquals(3, emissions) // Only emitted 2, not restarted + + cancel() +} +``` + +## Best Practices + +1. **Use runTest for all coroutine tests** + - Provides virtual time + - Automatic cleanup + +2. **Use Turbine for Flow testing** + - Clearer assertions + - Better error messages + +3. **Test both success and error paths** + - Normal flow + - Exception handling + - Edge cases + +4. **Control virtual time explicitly** + - Don't rely on real delays + - Use `advanceTimeBy()` and `advanceUntilIdle()` + +5. **Create fakes, not mocks** + - Simpler to maintain + - More realistic behavior + - Easier to debug + +6. **Test cancellation behavior** + - Verify cleanup happens + - Check no emissions after cancel + +7. **Test concurrent operations** + - Use `async` to spawn concurrent work + - Verify independence with SupervisorJob From 628f86e1a1a4382b2b02a65fd98100980de6eb09 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Wed, 31 Dec 2025 07:46:50 +0200 Subject: [PATCH 013/137] update claude.md --- .claude/CLAUDE.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index feac27e1a..3c280dfc3 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -91,14 +91,64 @@ Me: [implements using skill guidance] ## Feature Workflow +**CRITICAL: Always check existing implementations first before creating new code!** + When picking up a new task or feature, follow this process: +### Step 0: Survey Existing Implementation (MANDATORY) + +**Before writing ANY code, thoroughly audit ALL modules:** + +1. **Search for existing implementations across all modules:** + ```bash + # Search in quartz for protocol/business logic + grep -r "class.*Manager\|object.*Cache\|class.*Filter" quartz/src/commonMain/ + + # Search in commons for UI components + grep -r "@Composable.*Card\|@Composable.*View\|@Composable.*Dialog" commons/src/ + + # Search in amethyst for Android patterns + grep -r "class.*ViewModel\|class.*Account\|class.*State" amethyst/src/main/java/ + + # Search for specific functionality + grep -r "fun isFollowing\|fun subscribe\|fun getMetadata" {quartz,commons,amethyst}/src/ + ``` + +2. **Understand existing architecture patterns:** + - Event stores and caching systems + - State management patterns (StateFlow, mutable states) + - ViewModel patterns and lifecycle handling + - Filter builders and relay subscription patterns + - UI component hierarchies + +3. **Key principle:** Most logic already exists! Your job is to: + - **Reuse** existing protocol/business logic from quartz + - **Extract** shareable UI components from amethyst to commons + - Create **platform-specific** ViewModels/navigation for Desktop + - **NOT** duplicate existing managers, caches, or state systems + +4. **Document findings in implementation plan as a matrix:** + + | File/Component | Status | Location | Action | + |----------------|--------|----------|--------| + | FilterBuilders | ✅ Exists | quartz/relay/filters/ | Reuse as-is | + | NoteCard | 📦 Extract | amethyst/ui/note/ → commons/ | Extract to commons | + | HomeFeedViewModel | 🆕 New | Create in desktopApp/ | Create for Desktop | + | ProfileCache | ⚠️ Avoid | N/A | Already in User/Account pattern | + + **Legend:** + - ✅ **Reuse** - Exists and can be used directly + - 📦 **Extract** - Exists in Android, needs extraction to commons + - 🆕 **New** - Doesn't exist, needs creation (platform-specific only) + - ⚠️ **Avoid** - Duplicate functionality, use existing pattern instead + ### Step 1: Analyze Android Implementation -Start by examining the existing Android Amethyst codebase: +After surveying (Step 0), deeply examine the Android implementation: 1. Find the relevant feature/component in `amethyst/` module 2. Understand the current implementation patterns 3. Identify dependencies and integrations +4. Map out what code can be shared vs platform-specific ### Step 2: Create Implementation Plan From f3b8589fea9add64981406a77b1ac3fe5d28dca4 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Wed, 31 Dec 2025 13:00:25 +0200 Subject: [PATCH 014/137] update claude.md --- .claude/CLAUDE.md | 22 +++++++++++--------- .claude/skills/kotlin-multiplatform/SKILL.md | 18 +++++++++------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 3c280dfc3..72b5c7264 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -25,7 +25,7 @@ amethyst/ **Sharing Philosophy:** - `quartz/` = Business logic, protocol, data (no UI) -- `commons/` = Shared UI components, icons, composables +- `commons/` = Shared UI components, icons, composables, **ViewModels** - `amethyst/` & `desktopApp/` = Platform-native layouts and navigation ## Tech Stack @@ -123,8 +123,8 @@ When picking up a new task or feature, follow this process: 3. **Key principle:** Most logic already exists! Your job is to: - **Reuse** existing protocol/business logic from quartz - - **Extract** shareable UI components from amethyst to commons - - Create **platform-specific** ViewModels/navigation for Desktop + - **Extract** shareable UI components AND ViewModels from amethyst to commons + - Create **platform-specific** layouts/navigation for Desktop - **NOT** duplicate existing managers, caches, or state systems 4. **Document findings in implementation plan as a matrix:** @@ -133,7 +133,7 @@ When picking up a new task or feature, follow this process: |----------------|--------|----------|--------| | FilterBuilders | ✅ Exists | quartz/relay/filters/ | Reuse as-is | | NoteCard | 📦 Extract | amethyst/ui/note/ → commons/ | Extract to commons | - | HomeFeedViewModel | 🆕 New | Create in desktopApp/ | Create for Desktop | + | HomeFeedViewModel | 📦 Extract | amethyst/ → commons/commonMain/viewmodels/ | Extract to commons | | ProfileCache | ⚠️ Avoid | N/A | Already in User/Account pattern | **Legend:** @@ -156,24 +156,26 @@ Before coding, create a plan that categorizes work into three buckets: | Category | Description | Location | |----------|-------------|----------| -| **Android-Specific** | Platform APIs, navigation, layouts | `amethyst/`, `androidMain/` | -| **Reusable (Shared)** | Business logic, UI components, state | `quartz/commonMain/`, `commons/` (convert to KMP) | -| **Desktop-Specific** | Desktop navigation, layouts, platform APIs | `desktopApp/`, `jvmMain/` | +| **Android-Specific** | Platform-native layouts, navigation patterns | `amethyst/`, `androidMain/` | +| **Reusable (Shared)** | Business logic, UI components, **ViewModels**, state management | `quartz/commonMain/`, `commons/commonMain/` | +| **Desktop-Specific** | Desktop-native layouts, navigation patterns, platform APIs | `desktopApp/`, `jvmMain/` | ### Step 3: Code Sharing Strategy **Share:** - Business logic and data models → `quartz/commonMain/` -- Major UI components (cards, lists, dialogs) → `commons/` (convert to KMP as needed) -- State management and ViewModels → shared +- Major UI components (cards, lists, dialogs) → `commons/commonMain/` +- **ViewModels** (state, business logic) → `commons/commonMain/viewmodels/` - Icons and visual assets → `commons/commonMain/` **Keep Platform-Native:** +- **Screen composables** (layout, scaffolding) - Desktop uses `Window`, Android uses `Activity` - Navigation patterns (sidebar vs bottom nav) -- Screen layouts and scaffolding - Platform-specific interactions (gestures, keyboard shortcuts) - System integrations (notifications, file pickers) +**Rationale:** ViewModels contain platform-agnostic state management (StateFlow/SharedFlow) and business logic. Screens consume ViewModels but render differently (Desktop sidebar + content area vs Android bottom nav). + ### Step 4: Extract Shared Components When extracting UI components: diff --git a/.claude/skills/kotlin-multiplatform/SKILL.md b/.claude/skills/kotlin-multiplatform/SKILL.md index 13df020fd..b0aa2f8d6 100644 --- a/.claude/skills/kotlin-multiplatform/SKILL.md +++ b/.claude/skills/kotlin-multiplatform/SKILL.md @@ -182,12 +182,13 @@ Quick decision guidelines based on codebase patterns: ### Sometimes Abstract - **Business logic:** YES - state machines, data processing -- **UI state:** NO - ViewModels platform-specific -- **Why:** Separate concerns, business logic reusable +- **ViewModels:** YES - state + business logic shareable (StateFlow/SharedFlow) +- **Screen layouts:** NO - platform-native (Window vs Activity) +- **Why:** ViewModels contain platform-agnostic state; Screens render differently per platform ### Rarely Abstract -- **UI components** (composables with platform dependencies) -- **Why:** Platform paradigms differ (bottom nav vs sidebar) +- **Complex UI components** (composables with heavy platform dependencies) +- **Why:** Platform paradigms can differ significantly ### Never Abstract - **Navigation** (Activity vs Window fundamentally different) @@ -201,8 +202,8 @@ Quick decision guidelines based on codebase patterns: |-----------|---------|-----------| | PubKeyFormatter, ZapFormatter | ✅ YES | Pure Kotlin, no platform APIs | | TimeAgoFormatter | ⚠️ ABSTRACTED | Needs StringProvider for localized strings | -| Navigation (INav) | ❌ NO | Activity vs Window too different | -| AccountViewModel | ⚠️ PARTIAL | Business logic → IAccountState (shared), UI state → platform ViewModels | +| ViewModels (state + logic) | ✅ YES | StateFlow/SharedFlow platform-agnostic, Compose Multiplatform lifecycle compatible | +| Screen layouts (Scaffold, nav) | ❌ NO | Window vs Activity, sidebar vs bottom nav fundamentally different | | Image loading (Coil) | ⚠️ ABSTRACTED | Coil 3.x supports KMP, needs expect/actual wrapper | ## expect/actual Mechanics @@ -380,9 +381,10 @@ import com.fasterxml.jackson.databind.ObjectMapper | Crypto (varies by platform) | expect in commonMain, actual in platforms | Different security APIs per platform | | I/O, logging | expect in commonMain, actual in platforms | Platform implementations differ | | State (business logic) | commonMain or commons/jvmAndroid | Reusable StateFlow patterns | -| State (UI) | Platform ViewModels | Platform-specific lifecycle | +| **ViewModels** | **commons/commonMain/viewmodels/** | **StateFlow/SharedFlow + logic shareable, Compose MP lifecycle compatible** | | UI formatters (pure) | commons/commonMain | Reusable, no dependencies | -| UI components (complex) | Platform-specific | Paradigms differ | +| UI components (simple) | commons/commonMain | Cards, buttons, dialogs | +| **Screen layouts** | **Platform-specific** | **Window vs Activity, sidebar vs bottom nav** | | Navigation | Platform-specific only | Activity vs Window too different | | Permissions | Platform-specific only | APIs incompatible | | Platform UX (menus, etc.) | Platform-specific only | Native feel required | From 4f1ad11d79796ad5e28e1e2ad58092b6ae29bb0e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 31 Dec 2025 09:15:20 -0500 Subject: [PATCH 015/137] Moves SQLBuilder to its own class --- .../store/sqlite/QueryAssemblerTest.kt | 2 +- .../store/sqlite/EventIndexesModule.kt | 435 ---------------- .../nip01Core/store/sqlite/QueryBuilder.kt | 467 ++++++++++++++++++ .../store/sqlite/SQLiteEventStore.kt | 18 +- 4 files changed, 478 insertions(+), 444 deletions(-) create mode 100644 quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt index 9a6000835..67162ad12 100644 --- a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt @@ -32,7 +32,7 @@ import org.junit.Test class QueryAssemblerTest { val hasher = TagNameValueHasher(0) - val builder = EventIndexesModule(FullTextSearchModule(), { hasher }) + val builder = QueryBuilder(FullTextSearchModule(), { hasher }) val key1 = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d" val key2 = "f3ac434d61bc0f491a814782ccfdf9c439dae1f0bde9097ad4a245f4c495cd14" diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt index 981260c58..e8410d5a5 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt @@ -20,18 +20,12 @@ */ package com.vitorpamplona.quartz.nip01Core.store.sqlite -import android.database.Cursor import android.database.sqlite.SQLiteDatabase import com.vitorpamplona.quartz.nip01Core.core.AddressSerializer import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.Kind import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper -import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip01Core.store.sqlite.sql.where import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent -import com.vitorpamplona.quartz.utils.EventFactory class EventIndexesModule( val fts: FullTextSearchModule, @@ -188,437 +182,8 @@ class EventIndexesModule( return headerId } - fun planQuery( - filter: Filter, - hasher: TagNameValueHasher, - db: SQLiteDatabase, - ): String { - val rowIdSubQuery = prepareRowIDSubQueries(filter, hasher) - - return if (rowIdSubQuery == null) { - val query = makeEverythingQuery() - db.explainQuery(query) - } else { - val query = makeQueryIn(rowIdSubQuery.sql) - db.explainQuery(query, rowIdSubQuery.args.toTypedArray()) - } - } - - fun query( - filter: Filter, - db: SQLiteDatabase, - ): List { - val rowIdSubQuery = prepareRowIDSubQueries(filter, hasher(db)) - - return if (rowIdSubQuery == null) { - db.runQuery(makeEverythingQuery()) - } else { - db.runQuery(makeQueryIn(rowIdSubQuery.sql), rowIdSubQuery.args) - } - } - - fun query( - filter: Filter, - db: SQLiteDatabase, - onEach: (T) -> Unit, - ) { - val rowIdSubQuery = prepareRowIDSubQueries(filter, hasher(db)) - - return if (rowIdSubQuery == null) { - db.runQuery(makeEverythingQuery(), onEach = onEach) - } else { - db.runQuery(makeQueryIn(rowIdSubQuery.sql), rowIdSubQuery.args, onEach) - } - } - - fun planQuery( - filters: List, - hasher: TagNameValueHasher, - db: SQLiteDatabase, - ): String { - val rowIdSubQuery = unionSubqueriesIfNeeded(filters, hasher) - - return if (rowIdSubQuery == null) { - val query = makeEverythingQuery() - db.explainQuery(query) - } else { - val query = makeQueryIn(rowIdSubQuery.sql) - db.explainQuery(query, rowIdSubQuery.args.toTypedArray()) - } - } - - fun query( - filters: List, - db: SQLiteDatabase, - ): List { - val rowIdSubqueries = unionSubqueriesIfNeeded(filters, hasher(db)) ?: return db.runQuery(makeEverythingQuery()) - return db.runQuery(makeQueryIn(rowIdSubqueries.sql), rowIdSubqueries.args) - } - - fun query( - filters: List, - db: SQLiteDatabase, - onEach: (T) -> Unit, - ) { - val rowIdSubqueries = unionSubqueriesIfNeeded(filters, hasher(db)) - - if (rowIdSubqueries == null) { - db.runQuery(makeEverythingQuery(), onEach = onEach) - } else { - db.runQuery(makeQueryIn(rowIdSubqueries.sql), rowIdSubqueries.args, onEach) - } - } - - private fun makeEverythingQuery() = "SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers ORDER BY created_at DESC, id" - - private fun makeQueryIn(rowIdQuery: String) = - """ - SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - INNER JOIN ( - $rowIdQuery - ) AS filtered - ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC, id - """.trimIndent() - - private fun SQLiteDatabase.runQuery( - sql: String, - args: List = emptyList(), - ): List = - rawQuery(sql, args.toTypedArray()).use { cursor -> - ArrayList(cursor.count).apply { - while (cursor.moveToNext()) { - add(cursor.toEvent()) - } - } - } - - private inline fun SQLiteDatabase.runQuery( - sql: String, - args: List = emptyList(), - onEach: (T) -> Unit, - ) = rawQuery(sql, args.toTypedArray()).use { cursor -> - while (cursor.moveToNext()) { - onEach(cursor.toEvent()) - } - } - - private fun Cursor.toEvent() = - EventFactory.create( - getString(0).intern(), - getString(1).intern(), - getLong(2), - getInt(3), - OptimizedJsonMapper.fromJsonToTagArray(getString(4)), - getString(5), - getString(6), - ) - - class RawEvent( - val id: HexKey, - val pubKey: HexKey, - val createdAt: Long, - val kind: Kind, - val jsonTags: String, - val content: String, - val sig: HexKey, - ) { - fun toEvent() = - EventFactory.create( - id.intern(), - pubKey.intern(), - createdAt, - kind, - OptimizedJsonMapper.fromJsonToTagArray(jsonTags), - content, - sig, - ) - } - - private fun Cursor.toRawEvent() = - RawEvent( - getString(0), - getString(1), - getLong(2), - getInt(3), - getString(4), - getString(5), - getString(6), - ) - - // -------------- - // Counts - // ------------- - fun count( - filter: Filter, - db: SQLiteDatabase, - ): Int { - val rowIdSubQuery = prepareRowIDSubQueries(filter, hasher(db)) - - return if (rowIdSubQuery == null) { - db.countEverything() - } else { - db.countIn(rowIdSubQuery.sql, rowIdSubQuery.args) - } - } - - fun count( - filters: List, - db: SQLiteDatabase, - ): Int { - val rowIdSubqueries = unionSubqueriesIfNeeded(filters, hasher(db)) ?: return db.countEverything() - - return db.countIn(rowIdSubqueries.sql, rowIdSubqueries.args) - } - - private fun SQLiteDatabase.countEverything() = runCount("SELECT count(*) as count FROM event_headers") - - private fun SQLiteDatabase.countIn( - rowIdQuery: String, - args: List, - ) = runCount("SELECT COUNT(*) as count FROM ($rowIdQuery)", args) - - private fun SQLiteDatabase.runCount( - sql: String, - args: List = emptyList(), - ): Int = - rawQuery(sql, args.toTypedArray()).use { cursor -> - cursor.moveToNext() - cursor.getInt(0) - } - - // -------------- - // Deletes - // ------------- - fun delete( - filter: Filter, - db: SQLiteDatabase, - ): Int { - val rowIdQuery = prepareRowIDSubQueries(filter, hasher(db)) - - return if (rowIdQuery == null) { - 0 - } else { - db.runDelete(rowIdQuery.sql, rowIdQuery.args) - } - } - - fun delete( - filters: List, - db: SQLiteDatabase, - ): Int { - val rowIdSubqueries = unionSubqueriesIfNeeded(filters, hasher(db)) ?: return 0 - - return db.runDelete(rowIdSubqueries.sql, rowIdSubqueries.args) - } - - private fun SQLiteDatabase.runDelete( - sql: String, - args: List = emptyList(), - ): Int = delete("event_headers", "row_id IN ($sql)", args.toTypedArray()) - - // --------------------------------- - // Prepare unions of all the filters - // --------------------------------- - fun unionSubqueriesIfNeeded( - filters: List, - hasher: TagNameValueHasher, - ): RowIdSubQuery? { - val inner = - filters.mapNotNull { filter -> - prepareRowIDSubQueries(filter, hasher) - } - - if (inner.isEmpty()) return null - - return if (inner.size == 1) { - inner.first() - } else { - RowIdSubQuery( - sql = inner.joinToString("\n UNION\n ") { "SELECT row_id FROM (${it.sql})" }, - args = inner.flatMap { it.args }, - ) - } - } - - sealed class TagNameForQuery { - class InTags( - val tagName: String, - ) : TagNameForQuery() - - class AllTags( - val tagName: String, - val tagValueIndex: Int, - ) : TagNameForQuery() - } - - // ---------------------------- - // Inner row id selections - // ---------------------------- - fun prepareRowIDSubQueries( - filter: Filter, - hasher: TagNameValueHasher, - ): RowIdSubQuery? { - if (!filter.isFilledFilter()) return null - - val mustJoinSearch = (filter.search != null) - - val nonDTagsIn = filter.tags?.filter { it.key != "d" } ?: emptyMap() - - val nonDTagsAll = filter.tagsAll?.filter { it.key != "d" } ?: emptyMap() - - val reverseLookup = nonDTagsIn.isNotEmpty() || nonDTagsAll.isNotEmpty() - - val needHeaders = - with(filter) { - (ids != null) || - (authors != null && authors.isNotEmpty()) || - (kinds != null && kinds.isNotEmpty()) || - (tags != null && tags.containsKey("d")) - } - - val hasHeaders = - with(filter) { - (ids != null) || - (authors != null && authors.isNotEmpty()) || - (kinds != null && kinds.isNotEmpty()) || - (tags != null && tags.containsKey("d")) || - (since != null) || - (until != null) || - (limit != null) - } - - var defaultTagKey: TagNameForQuery? = null - - val projection = - buildString { - // always do tags if there are any - if (reverseLookup) { - append("SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags ") - - // it's quite rare to have 2 tags in the filter, but possible - nonDTagsIn.keys.forEachIndexed { index, tagName -> - if (defaultTagKey != null) { - append("INNER JOIN event_tags as event_tagsIn$index ON event_tagsIn$index.event_header_row_id = event_tags.event_header_row_id AND event_tagsIn$index.created_at = event_tags.created_at ") - } else { - defaultTagKey = TagNameForQuery.InTags(tagName) - } - } - - nonDTagsAll.keys.forEachIndexed { index, tagName -> - nonDTagsAll[tagName]!!.forEachIndexed { valueIndex, tagValue -> - if (defaultTagKey != null) { - append("INNER JOIN event_tags as event_tagsAll${index}_$valueIndex ON event_tagsAll${index}_$valueIndex.event_header_row_id = event_tags.event_header_row_id AND event_tagsAll${index}_$valueIndex.created_at = event_tags.created_at ") - } else { - defaultTagKey = TagNameForQuery.AllTags(tagName, valueIndex) - } - } - } - - if (needHeaders) { - append("INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id ") - } - - if (mustJoinSearch) { - append("INNER JOIN ${fts.tableName} ON ${fts.tableName}.${fts.eventHeaderRowIdName} = event_tags.event_header_row_id ") - } - } else if (mustJoinSearch) { - append("SELECT ${fts.tableName}.${fts.eventHeaderRowIdName} as row_id FROM ${fts.tableName} ") - - if (hasHeaders) { - append("INNER JOIN event_headers ON event_headers.row_id = ${fts.tableName}.${fts.eventHeaderRowIdName}") - } - } else { - // no tags and no search. - append("SELECT event_headers.row_id as row_id FROM event_headers ") - } - } - - val clause = - where { - // the order should match indexes - // ids reduce the filter the most - filter.ids?.let { equalsOrIn("event_headers.id", it) } - - // it's quite rare to have 2 tags in the filter, but possible - nonDTagsIn.keys.forEachIndexed { index, tagName -> - val column = - if (defaultTagKey == null || (defaultTagKey is TagNameForQuery.InTags && defaultTagKey.tagName == tagName)) { - "event_tags.tag_hash" - } else { - "event_tagsIn$index.tag_hash" - } - - equalsOrIn( - column, - nonDTagsIn[tagName]!!.map { - hasher.hash(tagName, it) - }, - ) - } - - // there are indexes for these, starting with tags. - nonDTagsAll.keys.forEachIndexed { index, tagName -> - nonDTagsAll[tagName]!!.forEachIndexed { valueIndex, tagValue -> - val column = - if (defaultTagKey == null || (defaultTagKey is TagNameForQuery.AllTags && defaultTagKey.tagName == tagName && defaultTagKey.tagValueIndex == valueIndex)) { - "event_tags.tag_hash" - } else { - "event_tagsAll${index}_$valueIndex.tag_hash" - } - - equals(column, hasher.hash(tagName, tagValue)) - } - } - - // range search is bad but most of the time these are up the top with few elements. - if (reverseLookup) { - filter.since?.let { greaterThanOrEquals("event_tags.created_at", it) } - filter.until?.let { lessThanOrEquals("event_tags.created_at", it) } - } else { - filter.since?.let { greaterThanOrEquals("event_headers.created_at", it) } - filter.until?.let { lessThanOrEquals("event_headers.created_at", it) } - } - - filter.kinds?.let { equalsOrIn("event_headers.kind", it) } - filter.authors?.let { equalsOrIn("event_headers.pubkey", it) } - - // there are indexes for these, starting with tags. - filter.tags?.forEach { (tagName, tagValues) -> - if (tagName == "d") { - equalsOrIn("event_headers.d_tag", tagValues) - } - } - - // if search is included, SQLLite will always start here. - filter.search?.let { - if (it.isNotBlank()) { - match(fts.tableName, it) - } - } - } - - val whereClause = - if (filter.limit != null) { - if (reverseLookup) { - "${clause.conditions} ORDER BY event_tags.created_at DESC LIMIT ${filter.limit}" - } else { - "${clause.conditions} ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT ${filter.limit}" - } - } else { - clause.conditions - } - - return RowIdSubQuery("$projection WHERE $whereClause", clause.args) - } - override fun deleteAll(db: SQLiteDatabase) { db.execSQL("DELETE FROM event_tags") db.execSQL("DELETE FROM event_headers") } - - data class RowIdSubQuery( - val sql: String, - val args: List, - ) } diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt new file mode 100644 index 000000000..0d3a5bb79 --- /dev/null +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt @@ -0,0 +1,467 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.store.sqlite + +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Kind +import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.store.sqlite.sql.where +import com.vitorpamplona.quartz.utils.EventFactory +import kotlin.collections.component1 +import kotlin.collections.component2 + +class QueryBuilder( + val fts: FullTextSearchModule, + val hasher: (db: SQLiteDatabase) -> TagNameValueHasher, +) { + fun planQuery( + filter: Filter, + hasher: TagNameValueHasher, + db: SQLiteDatabase, + ): String { + val rowIdSubQuery = prepareRowIDSubQueries(filter, hasher) + + return if (rowIdSubQuery == null) { + val query = makeEverythingQuery() + db.explainQuery(query) + } else { + val query = makeQueryIn(rowIdSubQuery.sql) + db.explainQuery(query, rowIdSubQuery.args.toTypedArray()) + } + } + + fun query( + filter: Filter, + db: SQLiteDatabase, + ): List { + val rowIdSubQuery = prepareRowIDSubQueries(filter, hasher(db)) + + return if (rowIdSubQuery == null) { + db.runQuery(makeEverythingQuery()) + } else { + db.runQuery(makeQueryIn(rowIdSubQuery.sql), rowIdSubQuery.args) + } + } + + fun query( + filter: Filter, + db: SQLiteDatabase, + onEach: (T) -> Unit, + ) { + val rowIdSubQuery = prepareRowIDSubQueries(filter, hasher(db)) + + return if (rowIdSubQuery == null) { + db.runQuery(makeEverythingQuery(), onEach = onEach) + } else { + db.runQuery(makeQueryIn(rowIdSubQuery.sql), rowIdSubQuery.args, onEach) + } + } + + fun planQuery( + filters: List, + hasher: TagNameValueHasher, + db: SQLiteDatabase, + ): String { + val rowIdSubQuery = unionSubqueriesIfNeeded(filters, hasher) + + return if (rowIdSubQuery == null) { + val query = makeEverythingQuery() + db.explainQuery(query) + } else { + val query = makeQueryIn(rowIdSubQuery.sql) + db.explainQuery(query, rowIdSubQuery.args.toTypedArray()) + } + } + + fun query( + filters: List, + db: SQLiteDatabase, + ): List { + val rowIdSubqueries = unionSubqueriesIfNeeded(filters, hasher(db)) ?: return db.runQuery(makeEverythingQuery()) + return db.runQuery(makeQueryIn(rowIdSubqueries.sql), rowIdSubqueries.args) + } + + fun query( + filters: List, + db: SQLiteDatabase, + onEach: (T) -> Unit, + ) { + val rowIdSubqueries = unionSubqueriesIfNeeded(filters, hasher(db)) + + if (rowIdSubqueries == null) { + db.runQuery(makeEverythingQuery(), onEach = onEach) + } else { + db.runQuery(makeQueryIn(rowIdSubqueries.sql), rowIdSubqueries.args, onEach) + } + } + + private fun makeEverythingQuery() = "SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers ORDER BY created_at DESC, id" + + private fun makeQueryIn(rowIdQuery: String) = + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + $rowIdQuery + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY created_at DESC, id + """.trimIndent() + + private fun SQLiteDatabase.runQuery( + sql: String, + args: List = emptyList(), + ): List = + rawQuery(sql, args.toTypedArray()).use { cursor -> + ArrayList(cursor.count).apply { + while (cursor.moveToNext()) { + add(cursor.toEvent()) + } + } + } + + private inline fun SQLiteDatabase.runQuery( + sql: String, + args: List = emptyList(), + onEach: (T) -> Unit, + ) = rawQuery(sql, args.toTypedArray()).use { cursor -> + while (cursor.moveToNext()) { + onEach(cursor.toEvent()) + } + } + + private fun Cursor.toEvent() = + EventFactory.create( + getString(0).intern(), + getString(1).intern(), + getLong(2), + getInt(3), + OptimizedJsonMapper.fromJsonToTagArray(getString(4)), + getString(5), + getString(6), + ) + + class RawEvent( + val id: HexKey, + val pubKey: HexKey, + val createdAt: Long, + val kind: Kind, + val jsonTags: String, + val content: String, + val sig: HexKey, + ) { + fun toEvent() = + EventFactory.create( + id.intern(), + pubKey.intern(), + createdAt, + kind, + OptimizedJsonMapper.fromJsonToTagArray(jsonTags), + content, + sig, + ) + } + + private fun Cursor.toRawEvent() = + RawEvent( + getString(0), + getString(1), + getLong(2), + getInt(3), + getString(4), + getString(5), + getString(6), + ) + + // -------------- + // Counts + // ------------- + fun count( + filter: Filter, + db: SQLiteDatabase, + ): Int { + val rowIdSubQuery = prepareRowIDSubQueries(filter, hasher(db)) + + return if (rowIdSubQuery == null) { + db.countEverything() + } else { + db.countIn(rowIdSubQuery.sql, rowIdSubQuery.args) + } + } + + fun count( + filters: List, + db: SQLiteDatabase, + ): Int { + val rowIdSubqueries = unionSubqueriesIfNeeded(filters, hasher(db)) ?: return db.countEverything() + + return db.countIn(rowIdSubqueries.sql, rowIdSubqueries.args) + } + + private fun SQLiteDatabase.countEverything() = runCount("SELECT count(*) as count FROM event_headers") + + private fun SQLiteDatabase.countIn( + rowIdQuery: String, + args: List, + ) = runCount("SELECT COUNT(*) as count FROM ($rowIdQuery)", args) + + private fun SQLiteDatabase.runCount( + sql: String, + args: List = emptyList(), + ): Int = + rawQuery(sql, args.toTypedArray()).use { cursor -> + cursor.moveToNext() + cursor.getInt(0) + } + + // -------------- + // Deletes + // ------------- + fun delete( + filter: Filter, + db: SQLiteDatabase, + ): Int { + val rowIdQuery = prepareRowIDSubQueries(filter, hasher(db)) + + return if (rowIdQuery == null) { + 0 + } else { + db.runDelete(rowIdQuery.sql, rowIdQuery.args) + } + } + + fun delete( + filters: List, + db: SQLiteDatabase, + ): Int { + val rowIdSubqueries = unionSubqueriesIfNeeded(filters, hasher(db)) ?: return 0 + + return db.runDelete(rowIdSubqueries.sql, rowIdSubqueries.args) + } + + private fun SQLiteDatabase.runDelete( + sql: String, + args: List = emptyList(), + ): Int = delete("event_headers", "row_id IN ($sql)", args.toTypedArray()) + + // --------------------------------- + // Prepare unions of all the filters + // --------------------------------- + fun unionSubqueriesIfNeeded( + filters: List, + hasher: TagNameValueHasher, + ): RowIdSubQuery? { + val inner = + filters.mapNotNull { filter -> + prepareRowIDSubQueries(filter, hasher) + } + + if (inner.isEmpty()) return null + + return if (inner.size == 1) { + inner.first() + } else { + RowIdSubQuery( + sql = inner.joinToString("\n UNION\n ") { "SELECT row_id FROM (${it.sql})" }, + args = inner.flatMap { it.args }, + ) + } + } + + sealed class TagNameForQuery { + class InTags( + val tagName: String, + ) : TagNameForQuery() + + class AllTags( + val tagName: String, + val tagValueIndex: Int, + ) : TagNameForQuery() + } + + // ---------------------------- + // Inner row id selections + // ---------------------------- + fun prepareRowIDSubQueries( + filter: Filter, + hasher: TagNameValueHasher, + ): RowIdSubQuery? { + if (!filter.isFilledFilter()) return null + + val mustJoinSearch = (filter.search != null) + + val nonDTagsIn = filter.tags?.filter { it.key != "d" } ?: emptyMap() + + val nonDTagsAll = filter.tagsAll?.filter { it.key != "d" } ?: emptyMap() + + val reverseLookup = nonDTagsIn.isNotEmpty() || nonDTagsAll.isNotEmpty() + + val needHeaders = + with(filter) { + (ids != null) || + (authors != null && authors.isNotEmpty()) || + (kinds != null && kinds.isNotEmpty()) || + (tags != null && tags.containsKey("d")) + } + + val hasHeaders = + with(filter) { + (ids != null) || + (authors != null && authors.isNotEmpty()) || + (kinds != null && kinds.isNotEmpty()) || + (tags != null && tags.containsKey("d")) || + (since != null) || + (until != null) || + (limit != null) + } + + var defaultTagKey: TagNameForQuery? = null + + val projection = + buildString { + // always do tags if there are any + if (reverseLookup) { + append("SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags ") + + // it's quite rare to have 2 tags in the filter, but possible + nonDTagsIn.keys.forEachIndexed { index, tagName -> + if (defaultTagKey != null) { + append("INNER JOIN event_tags as event_tagsIn$index ON event_tagsIn$index.event_header_row_id = event_tags.event_header_row_id AND event_tagsIn$index.created_at = event_tags.created_at ") + } else { + defaultTagKey = TagNameForQuery.InTags(tagName) + } + } + + nonDTagsAll.keys.forEachIndexed { index, tagName -> + nonDTagsAll[tagName]!!.forEachIndexed { valueIndex, tagValue -> + if (defaultTagKey != null) { + append("INNER JOIN event_tags as event_tagsAll${index}_$valueIndex ON event_tagsAll${index}_$valueIndex.event_header_row_id = event_tags.event_header_row_id AND event_tagsAll${index}_$valueIndex.created_at = event_tags.created_at ") + } else { + defaultTagKey = TagNameForQuery.AllTags(tagName, valueIndex) + } + } + } + + if (needHeaders) { + append("INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id ") + } + + if (mustJoinSearch) { + append("INNER JOIN ${fts.tableName} ON ${fts.tableName}.${fts.eventHeaderRowIdName} = event_tags.event_header_row_id ") + } + } else if (mustJoinSearch) { + append("SELECT ${fts.tableName}.${fts.eventHeaderRowIdName} as row_id FROM ${fts.tableName} ") + + if (hasHeaders) { + append("INNER JOIN event_headers ON event_headers.row_id = ${fts.tableName}.${fts.eventHeaderRowIdName}") + } + } else { + // no tags and no search. + append("SELECT event_headers.row_id as row_id FROM event_headers ") + } + } + + val clause = + where { + // the order should match indexes + // ids reduce the filter the most + filter.ids?.let { equalsOrIn("event_headers.id", it) } + + // it's quite rare to have 2 tags in the filter, but possible + nonDTagsIn.keys.forEachIndexed { index, tagName -> + val column = + if (defaultTagKey == null || (defaultTagKey is TagNameForQuery.InTags && defaultTagKey.tagName == tagName)) { + "event_tags.tag_hash" + } else { + "event_tagsIn$index.tag_hash" + } + + equalsOrIn( + column, + nonDTagsIn[tagName]!!.map { + hasher.hash(tagName, it) + }, + ) + } + + // there are indexes for these, starting with tags. + nonDTagsAll.keys.forEachIndexed { index, tagName -> + nonDTagsAll[tagName]!!.forEachIndexed { valueIndex, tagValue -> + val column = + if (defaultTagKey == null || (defaultTagKey is TagNameForQuery.AllTags && defaultTagKey.tagName == tagName && defaultTagKey.tagValueIndex == valueIndex)) { + "event_tags.tag_hash" + } else { + "event_tagsAll${index}_$valueIndex.tag_hash" + } + + equals(column, hasher.hash(tagName, tagValue)) + } + } + + // range search is bad but most of the time these are up the top with few elements. + if (reverseLookup) { + filter.since?.let { greaterThanOrEquals("event_tags.created_at", it) } + filter.until?.let { lessThanOrEquals("event_tags.created_at", it) } + } else { + filter.since?.let { greaterThanOrEquals("event_headers.created_at", it) } + filter.until?.let { lessThanOrEquals("event_headers.created_at", it) } + } + + filter.kinds?.let { equalsOrIn("event_headers.kind", it) } + filter.authors?.let { equalsOrIn("event_headers.pubkey", it) } + + // there are indexes for these, starting with tags. + filter.tags?.forEach { (tagName, tagValues) -> + if (tagName == "d") { + equalsOrIn("event_headers.d_tag", tagValues) + } + } + + // if search is included, SQLLite will always start here. + filter.search?.let { + if (it.isNotBlank()) { + match(fts.tableName, it) + } + } + } + + val whereClause = + if (filter.limit != null) { + if (reverseLookup) { + "${clause.conditions} ORDER BY event_tags.created_at DESC LIMIT ${filter.limit}" + } else { + "${clause.conditions} ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT ${filter.limit}" + } + } else { + clause.conditions + } + + return RowIdSubQuery("$projection WHERE $whereClause", clause.args) + } + + data class RowIdSubQuery( + val sql: String, + val args: List, + ) +} diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt index c1e72a3bb..c51d50753 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt @@ -57,6 +57,8 @@ class SQLiteEventStore( val expirationModule = ExpirationModule() val rightToVanishModule = RightToVanishModule(seedModule::hasher) + val queryBuilder = QueryBuilder(fullTextSearchModule, seedModule::hasher) + val modules = listOf( seedModule, @@ -171,30 +173,30 @@ class SQLiteEventStore( } } - fun query(filter: Filter): List = eventIndexModule.query(filter, readableDatabase) + fun query(filter: Filter): List = queryBuilder.query(filter, readableDatabase) - fun query(filters: List): List = eventIndexModule.query(filters, readableDatabase) + fun query(filters: List): List = queryBuilder.query(filters, readableDatabase) fun query( filter: Filter, onEach: (T) -> Unit, - ) = eventIndexModule.query(filter, readableDatabase, onEach) + ) = queryBuilder.query(filter, readableDatabase, onEach) fun query( filters: List, onEach: (T) -> Unit, - ) = eventIndexModule.query(filters, readableDatabase, onEach) + ) = queryBuilder.query(filters, readableDatabase, onEach) - fun count(filter: Filter): Int = eventIndexModule.count(filter, readableDatabase) + fun count(filter: Filter): Int = queryBuilder.count(filter, readableDatabase) - fun count(filters: List): Int = eventIndexModule.count(filters, readableDatabase) + fun count(filters: List): Int = queryBuilder.count(filters, readableDatabase) fun delete(filter: Filter) { - eventIndexModule.delete(filter, writableDatabase) + queryBuilder.delete(filter, writableDatabase) } fun delete(filters: List) { - eventIndexModule.delete(filters, writableDatabase) + queryBuilder.delete(filters, writableDatabase) } fun delete(id: HexKey): Int = writableDatabase.delete("event_headers", "id = ?", arrayOf(id)) From 780ea487aa12b746cb825edac985492d411f987a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 31 Dec 2025 09:42:54 -0500 Subject: [PATCH 016/137] Adds a test case to simulate reports --- .../store/sqlite/QueryAssemblerTest.kt | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt index 67162ad12..58f97af0c 100644 --- a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt @@ -532,4 +532,41 @@ class QueryAssemblerTest { sql, ) } + + @Test + fun testReportLikeFilter() { + val sql = + explain( + Filter( + kinds = listOf(ContactListEvent.KIND), + authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + tags = + mapOf( + "p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + ), + limit = 500, + ), + ) + + println(sql) + + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_headers.kind = "3") AND (event_headers.pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") ORDER BY event_tags.created_at DESC LIMIT 500 + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY created_at DESC, id + ├── CO-ROUTINE filtered + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash (tag_hash=?) + │ ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + │ └── USE TEMP B-TREE FOR DISTINCT + ├── SCAN filtered + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + sql, + ) + } } From 4f7972df90e3db06176cbbe7061f8f77dd9e1395 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 31 Dec 2025 13:37:02 -0500 Subject: [PATCH 017/137] Removes logs --- .../nip01Core/store/sqlite/QueryAssemblerTest.kt | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt index 58f97af0c..46c836c0d 100644 --- a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt @@ -25,6 +25,7 @@ import androidx.test.core.app.ApplicationProvider import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import junit.framework.TestCase +import junit.framework.TestCase.assertEquals import org.junit.After import org.junit.Assert import org.junit.Before @@ -337,9 +338,6 @@ class QueryAssemblerTest { ), ), ) - - println(sql) - TestCase.assertEquals( """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers @@ -512,7 +510,6 @@ class QueryAssemblerTest { @Test fun testAllTag() { val sql = explain(Filter(tagsAll = mapOf("p" to listOf(key1, key2)))) - println(sql) TestCase.assertEquals( """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers @@ -547,10 +544,7 @@ class QueryAssemblerTest { limit = 500, ), ) - - println(sql) - - TestCase.assertEquals( + assertEquals( """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( From 6713c3bd507bf884cb491ff38452bffea11b3bab Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 31 Dec 2025 13:39:17 -0500 Subject: [PATCH 018/137] Adds performance and flexibility by exposing Raw methods --- .../nip01Core/store/sqlite/QueryBuilder.kt | 199 ++++++++++-------- .../store/sqlite/SQLiteEventStore.kt | 38 ++++ 2 files changed, 147 insertions(+), 90 deletions(-) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt index 0d3a5bb79..94713826c 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt @@ -23,8 +23,6 @@ package com.vitorpamplona.quartz.nip01Core.store.sqlite import android.database.Cursor import android.database.sqlite.SQLiteDatabase import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.Kind import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.store.sqlite.sql.where @@ -36,47 +34,66 @@ class QueryBuilder( val fts: FullTextSearchModule, val hasher: (db: SQLiteDatabase) -> TagNameValueHasher, ) { - fun planQuery( - filter: Filter, - hasher: TagNameValueHasher, - db: SQLiteDatabase, - ): String { - val rowIdSubQuery = prepareRowIDSubQueries(filter, hasher) - - return if (rowIdSubQuery == null) { - val query = makeEverythingQuery() - db.explainQuery(query) - } else { - val query = makeQueryIn(rowIdSubQuery.sql) - db.explainQuery(query, rowIdSubQuery.args.toTypedArray()) - } - } - + // ------------ + // Main methods + // ------------ fun query( filter: Filter, db: SQLiteDatabase, - ): List { - val rowIdSubQuery = prepareRowIDSubQueries(filter, hasher(db)) - - return if (rowIdSubQuery == null) { - db.runQuery(makeEverythingQuery()) - } else { - db.runQuery(makeQueryIn(rowIdSubQuery.sql), rowIdSubQuery.args) - } - } + ): List = db.runQuery(toSql(filter, hasher(db))) fun query( filter: Filter, db: SQLiteDatabase, onEach: (T) -> Unit, - ) { - val rowIdSubQuery = prepareRowIDSubQueries(filter, hasher(db)) + ) = db.runQuery(toSql(filter, hasher(db)), onEach) - return if (rowIdSubQuery == null) { - db.runQuery(makeEverythingQuery(), onEach = onEach) - } else { - db.runQuery(makeQueryIn(rowIdSubQuery.sql), rowIdSubQuery.args, onEach) - } + fun query( + filters: List, + db: SQLiteDatabase, + ): List = db.runQuery(toSql(filters, hasher(db))) + + fun query( + filters: List, + db: SQLiteDatabase, + onEach: (T) -> Unit, + ) = db.runQuery(toSql(filters, hasher(db)), onEach) + + // --------------------------- + // Raw methods for performance + // --------------------------- + fun rawQuery( + filter: Filter, + db: SQLiteDatabase, + ): List = db.runRawQuery(toSql(filter, hasher(db))) + + fun rawQuery( + filter: Filter, + db: SQLiteDatabase, + onEach: (RawEvent) -> Unit, + ) = db.runRawQuery(toSql(filter, hasher(db)), onEach) + + fun rawQuery( + filters: List, + db: SQLiteDatabase, + ): List = db.runRawQuery(toSql(filters, hasher(db))) + + fun rawQuery( + filters: List, + db: SQLiteDatabase, + onEach: (RawEvent) -> Unit, + ) = db.runRawQuery(toSql(filters, hasher(db)), onEach) + + // ----------- + // Debug Tools + // ----------- + fun planQuery( + filter: Filter, + hasher: TagNameValueHasher, + db: SQLiteDatabase, + ): String { + val query = toSql(filter, hasher) + return db.explainQuery(query.sql, query.args.toTypedArray()) } fun planQuery( @@ -84,36 +101,45 @@ class QueryBuilder( hasher: TagNameValueHasher, db: SQLiteDatabase, ): String { - val rowIdSubQuery = unionSubqueriesIfNeeded(filters, hasher) + val query = toSql(filters, hasher) + return db.explainQuery(query.sql, query.args.toTypedArray()) + } - return if (rowIdSubQuery == null) { - val query = makeEverythingQuery() - db.explainQuery(query) + fun toSql( + filter: Filter, + hasher: TagNameValueHasher, + ): QuerySpec { + val rowIdSubqueries = prepareRowIDSubQueries(filter, hasher) + + return if (rowIdSubqueries == null) { + QuerySpec( + makeEverythingQuery(), + emptyList(), + ) } else { - val query = makeQueryIn(rowIdSubQuery.sql) - db.explainQuery(query, rowIdSubQuery.args.toTypedArray()) + QuerySpec( + makeQueryIn(rowIdSubqueries.sql), + rowIdSubqueries.args, + ) } } - fun query( + fun toSql( filters: List, - db: SQLiteDatabase, - ): List { - val rowIdSubqueries = unionSubqueriesIfNeeded(filters, hasher(db)) ?: return db.runQuery(makeEverythingQuery()) - return db.runQuery(makeQueryIn(rowIdSubqueries.sql), rowIdSubqueries.args) - } + hasher: TagNameValueHasher, + ): QuerySpec { + val rowIdSubqueries = unionSubqueriesIfNeeded(filters, hasher) - fun query( - filters: List, - db: SQLiteDatabase, - onEach: (T) -> Unit, - ) { - val rowIdSubqueries = unionSubqueriesIfNeeded(filters, hasher(db)) - - if (rowIdSubqueries == null) { - db.runQuery(makeEverythingQuery(), onEach = onEach) + return if (rowIdSubqueries == null) { + QuerySpec( + makeEverythingQuery(), + emptyList(), + ) } else { - db.runQuery(makeQueryIn(rowIdSubqueries.sql), rowIdSubqueries.args, onEach) + QuerySpec( + makeQueryIn(rowIdSubqueries.sql), + rowIdSubqueries.args, + ) } } @@ -129,11 +155,8 @@ class QueryBuilder( ORDER BY created_at DESC, id """.trimIndent() - private fun SQLiteDatabase.runQuery( - sql: String, - args: List = emptyList(), - ): List = - rawQuery(sql, args.toTypedArray()).use { cursor -> + private fun SQLiteDatabase.runQuery(query: QuerySpec): List = + rawQuery(query.sql, query.args.toTypedArray()).use { cursor -> ArrayList(cursor.count).apply { while (cursor.moveToNext()) { add(cursor.toEvent()) @@ -141,16 +164,33 @@ class QueryBuilder( } } + private fun SQLiteDatabase.runRawQuery(query: QuerySpec): List = + rawQuery(query.sql, query.args.toTypedArray()).use { cursor -> + ArrayList(cursor.count).apply { + while (cursor.moveToNext()) { + add(cursor.toRawEvent()) + } + } + } + private inline fun SQLiteDatabase.runQuery( - sql: String, - args: List = emptyList(), + query: QuerySpec, onEach: (T) -> Unit, - ) = rawQuery(sql, args.toTypedArray()).use { cursor -> + ) = rawQuery(query.sql, query.args.toTypedArray()).use { cursor -> while (cursor.moveToNext()) { onEach(cursor.toEvent()) } } + private inline fun SQLiteDatabase.runRawQuery( + query: QuerySpec, + onEach: (RawEvent) -> Unit, + ) = rawQuery(query.sql, query.args.toTypedArray()).use { cursor -> + while (cursor.moveToNext()) { + onEach(cursor.toRawEvent()) + } + } + private fun Cursor.toEvent() = EventFactory.create( getString(0).intern(), @@ -162,27 +202,6 @@ class QueryBuilder( getString(6), ) - class RawEvent( - val id: HexKey, - val pubKey: HexKey, - val createdAt: Long, - val kind: Kind, - val jsonTags: String, - val content: String, - val sig: HexKey, - ) { - fun toEvent() = - EventFactory.create( - id.intern(), - pubKey.intern(), - createdAt, - kind, - OptimizedJsonMapper.fromJsonToTagArray(jsonTags), - content, - sig, - ) - } - private fun Cursor.toRawEvent() = RawEvent( getString(0), @@ -271,7 +290,7 @@ class QueryBuilder( fun unionSubqueriesIfNeeded( filters: List, hasher: TagNameValueHasher, - ): RowIdSubQuery? { + ): QuerySpec? { val inner = filters.mapNotNull { filter -> prepareRowIDSubQueries(filter, hasher) @@ -282,7 +301,7 @@ class QueryBuilder( return if (inner.size == 1) { inner.first() } else { - RowIdSubQuery( + QuerySpec( sql = inner.joinToString("\n UNION\n ") { "SELECT row_id FROM (${it.sql})" }, args = inner.flatMap { it.args }, ) @@ -306,7 +325,7 @@ class QueryBuilder( fun prepareRowIDSubQueries( filter: Filter, hasher: TagNameValueHasher, - ): RowIdSubQuery? { + ): QuerySpec? { if (!filter.isFilledFilter()) return null val mustJoinSearch = (filter.search != null) @@ -457,10 +476,10 @@ class QueryBuilder( clause.conditions } - return RowIdSubQuery("$projection WHERE $whereClause", clause.args) + return QuerySpec("$projection WHERE $whereClause", clause.args) } - data class RowIdSubQuery( + data class QuerySpec( val sql: String, val args: List, ) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt index c51d50753..f7faf6792 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt @@ -27,10 +27,13 @@ import android.database.sqlite.SQLiteOpenHelper import androidx.core.database.sqlite.transaction import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Kind +import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper import com.vitorpamplona.quartz.nip01Core.core.isEphemeral import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.store.IEventStore import com.vitorpamplona.quartz.nip40Expiration.isExpired +import com.vitorpamplona.quartz.utils.EventFactory import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -187,6 +190,20 @@ class SQLiteEventStore( onEach: (T) -> Unit, ) = queryBuilder.query(filters, readableDatabase, onEach) + fun rawQuery(filter: Filter): List = queryBuilder.rawQuery(filter, readableDatabase) + + fun rawQuery(filters: List): List = queryBuilder.rawQuery(filters, readableDatabase) + + fun rawQuery( + filter: Filter, + onEach: (RawEvent) -> Unit, + ) = queryBuilder.rawQuery(filter, readableDatabase, onEach) + + fun rawQuery( + filters: List, + onEach: (RawEvent) -> Unit, + ) = queryBuilder.rawQuery(filters, readableDatabase, onEach) + fun count(filter: Filter): Int = queryBuilder.count(filter, readableDatabase) fun count(filters: List): Int = queryBuilder.count(filters, readableDatabase) @@ -203,3 +220,24 @@ class SQLiteEventStore( fun deleteExpiredEvents() = expirationModule.deleteExpiredEvents(writableDatabase) } + +class RawEvent( + val id: HexKey, + val pubKey: HexKey, + val createdAt: Long, + val kind: Kind, + val jsonTags: String, + val content: String, + val sig: HexKey, +) { + fun toEvent() = + EventFactory.create( + id.intern(), + pubKey.intern(), + createdAt, + kind, + OptimizedJsonMapper.fromJsonToTagArray(jsonTags), + content, + sig, + ) +} From 5d7c84d35716f64c07a3da4fd71c68b8a4015680 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 31 Dec 2025 16:53:41 -0500 Subject: [PATCH 019/137] Setting higher cache size --- .../quartz/nip01Core/store/sqlite/SQLiteEventStore.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt index f7faf6792..e1854e344 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt @@ -78,6 +78,9 @@ class SQLiteEventStore( override fun onConfigure(db: SQLiteDatabase) { super.onConfigure(db) + // 32MB memory cache + db.execSQL("PRAGMA cache_size=-32000;") + // makes sure the FKs are sane db.setForeignKeyConstraintsEnabled(true) @@ -87,7 +90,7 @@ class SQLiteEventStore( // The DB can be corrupted if the OS is shutdown before sync, which generally // doesn't happen on Android - db.execSQL("PRAGMA synchronous = OFF") + db.execSQL("PRAGMA synchronous = OFF;") } override fun onCreate(db: SQLiteDatabase) { From 393366d9cc7d9320079b3b40c3a120f3a8c94e75 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 31 Dec 2025 18:41:20 -0500 Subject: [PATCH 020/137] Adds size of the DB as a function to the lib --- .../quartz/nip01Core/store/sqlite/SQLiteEventStore.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt index e1854e344..3796b019d 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt @@ -93,6 +93,13 @@ class SQLiteEventStore( db.execSQL("PRAGMA synchronous = OFF;") } + fun dbSizeMB(): Int { + val f1 = context.getDatabasePath(dbName) + val f2 = context.getDatabasePath("$dbName-wal") + val total = f1.length() + f2.length() + return (total / (1024 * 1024)).toInt() + } + override fun onCreate(db: SQLiteDatabase) { modules.forEach { it.create(db) From eb47c191729e020d41885a20376f36f1f277afec Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Thu, 1 Jan 2026 07:26:39 +0200 Subject: [PATCH 021/137] move models to commons --- .../vitorpamplona/amethyst/model/Account.kt | 5 + .../amethyst/model/AccountSettings.kt | 5 + .../amethyst/model/AccountSyncedSettings.kt | 5 + .../model/AccountSyncedSettingsInternal.kt | 5 + .../amethyst/model/AntiSpamFilter.kt | 5 + .../vitorpamplona/amethyst/model/Channel.kt | 5 + .../vitorpamplona/amethyst/model/Constants.kt | 5 + .../amethyst/model/HashtagIcon.kt | 5 + .../amethyst/model/LargeSoftCache.kt | 5 + .../model/LargeSoftCacheAddressExt.kt | 5 + .../amethyst/model/LocalCache.kt | 3 + .../amethyst/model/MediaAspectRatioCache.kt | 5 + .../vitorpamplona/amethyst/model/MiniFhir.kt | 5 + .../amethyst/model/ParticipantListBuilder.kt | 5 + .../amethyst/model/ThreadAssembler.kt | 5 + .../amethyst/model/ThreadLevelCalculator.kt | 5 + .../amethyst/model/UiSettings.kt | 5 + .../amethyst/model/UiSettingsFlow.kt | 5 + .../amethyst/model/UrlCachedPreviewer.kt | 5 + .../com/vitorpamplona/amethyst/model/User.kt | 352 ------------------ .../model/accountsCache/AccountCacheState.kt | 5 + .../PrivateStorageRelayListDecryptionCache.kt | 5 + .../edits/PrivateStorageRelayListState.kt | 4 +- .../model/emphChat/EphemeralChatChannel.kt | 5 + .../EphemeralChatListDecryptionCache.kt | 5 + .../model/emphChat/EphemeralChatListState.kt | 4 +- .../model/localRelays/LocalRelayListState.kt | 5 + .../AccountHomeRelayState.kt | 5 + .../AccountOutboxRelayState.kt | 5 + .../NotificationInboxRelayState.kt | 5 + .../nip01UserMetadata/UserMetadataState.kt | 7 +- .../DeclaredFollowsPerOutboxRelay.kt | 5 + .../FollowListOutboxOrProxyRelays.kt | 5 + .../FollowListReusedOutboxOrProxyRelays.kt | 5 + .../nip02FollowLists/FollowsPerOutboxRelay.kt | 5 + .../nip02FollowLists/Kind3FollowListState.kt | 9 +- .../IncomingOtsEventVerifier.kt | 2 +- .../amethyst/model/nip03Timestamp/OtsState.kt | 2 +- .../TorAwareOkHttpOtsResolverBuilder.kt | 5 + .../model/nip11RelayInfo/LoadRelayInfo.kt | 5 + .../nip11RelayInfo/Nip11CachedRetriever.kt | 5 + .../model/nip11RelayInfo/Nip11Retriever.kt | 5 + .../model/nip11RelayInfo/RetrieveResult.kt | 5 + .../model/nip17Dms/DmInboxRelayState.kt | 5 + .../model/nip17Dms/DmRelayListState.kt | 4 +- .../model/nip18Reposts/RepostAction.kt | 2 +- .../model/nip25Reactions/ReactionAction.kt | 4 +- .../nip28PublicChats/PublicChatChannel.kt | 4 +- .../PublicChatListDecryptionCache.kt | 5 + .../nip28PublicChats/PublicChatListState.kt | 4 +- .../model/nip30CustomEmojis/EmojiPackState.kt | 4 +- .../nip38UserStatuses/UserStatusAction.kt | 4 +- .../nip47WalletConnect/NwcSignerState.kt | 2 +- .../model/nip51Lists/BookmarkListState.kt | 6 +- .../model/nip51Lists/HiddenUsersState.kt | 5 + .../blockPeopleList/BlockPeopleListState.kt | 4 +- .../BlockedRelayListDecryptionCache.kt | 5 + .../blockedRelays/BlockedRelayListState.kt | 4 +- .../BroadcastRelayListDecryptionCache.kt | 5 + .../BroadcastRelayListState.kt | 4 +- .../geohashLists/GeohashListCard.kt | 5 + .../GeohashListDecryptionCache.kt | 2 +- .../geohashLists/GeohashListState.kt | 4 +- .../HashtagListDecryptionCache.kt | 5 + .../hashtagLists/HashtagListState.kt | 4 +- .../IndexerRelayListDecryptionCache.kt | 5 + .../indexerRelays/IndexerRelayListState.kt | 4 +- .../LabeledBookmarkList.kt | 5 + .../LabeledBookmarkListsState.kt | 4 +- .../muteList/MuteListDecryptionCache.kt | 5 + .../nip51Lists/muteList/MuteListState.kt | 4 +- .../nip51Lists/peopleList/FollowListsState.kt | 6 +- .../model/nip51Lists/peopleList/PeopleList.kt | 7 +- .../peopleList/PeopleListDecryptionCache.kt | 5 + .../nip51Lists/peopleList/PeopleListsState.kt | 6 +- .../ProxyRelayListDecryptionCache.kt | 5 + .../proxyRelays/ProxyRelayListState.kt | 4 +- .../relayLists/GenericRelayListCache.kt | 2 +- .../nip51Lists/relayLists/RelayListCard.kt | 5 + .../SearchRelayListDecryptionCache.kt | 5 + .../searchRelays/SearchRelayListState.kt | 4 +- .../TrustedRelayListDecryptionCache.kt | 5 + .../trustedRelays/TrustedRelayListState.kt | 4 +- .../LiveActivitiesChannel.kt | 4 +- .../model/nip56Reports/ReportAction.kt | 4 +- .../nip65RelayList/Nip65RelayListState.kt | 4 +- .../CommunityListDecryptionCache.kt | 5 + .../nip72Communities/CommunityListState.kt | 6 +- .../nip78AppSpecific/AppSpecificState.kt | 2 +- .../FileStorageServerListState.kt | 4 +- .../nipB7Blossom/BlossomServerListState.kt | 4 +- .../model/observables/CreatedAtComparator.kt | 4 +- .../observables/LatestByKindAndAuthor.kt | 4 +- .../model/observables/LatestByKindWithETag.kt | 2 +- .../preferences/AccountPreferenceStores.kt | 5 + .../AccountSecretsEncryptedStores.kt | 5 + .../model/preferences/DataStoreExt.kt | 5 + .../model/preferences/EncryptedDataStore.kt | 5 + .../model/preferences/KeyStoreEncryption.kt | 5 + .../model/preferences/TorSharedPreferences.kt | 5 + .../model/preferences/UISharedPreferences.kt | 5 + .../preferences/UpdatablePropertyFlow.kt | 5 + .../EmptyRoleBasedHttpClientBuilder.kt | 5 + .../IRoleBasedHttpClientBuilder.kt | 5 + .../RoleBasedHttpClientBuilder.kt | 5 + .../SingleRoleBasedHttpClientBuilder.kt | 5 + .../amethyst/model/privateChats/Chatroom.kt | 6 +- .../model/privateChats/ChatroomList.kt | 4 +- .../serverList/MergedFollowListsState.kt | 5 + .../MergedFollowPlusMineRelayListsState.kt | 5 + ...dFollowPlusMineWithIndexRelayListsState.kt | 5 + ...FollowPlusMineWithSearchRelayListsState.kt | 5 + .../model/serverList/MergedServerListState.kt | 5 + .../serverList/TrustedRelayListsState.kt | 5 + .../model/topNavFeeds/CommunityRelayLoader.kt | 2 +- .../model/topNavFeeds/FeedDecryptionCaches.kt | 5 + .../topNavFeeds/FeedTopNavFilterState.kt | 5 + .../model/topNavFeeds/IFeedFlowsType.kt | 5 + .../model/topNavFeeds/IFeedTopNavFilter.kt | 5 + .../topNavFeeds/IFeedTopNavPerRelayFilter.kt | 5 + .../IFeedTopNavPerRelayFilterSet.kt | 5 + .../MergedTopFeedAuthorListsState.kt | 5 + .../model/topNavFeeds/OutboxLoaderState.kt | 5 + .../model/topNavFeeds/OutboxRelayLoader.kt | 4 +- .../AllFollowsByOutboxTopNavFilter.kt | 5 + .../AllFollowsByProxyTopNavFilter.kt | 5 + .../allFollows/AllFollowsFeedFlow.kt | 5 + .../AllFollowsTopNavPerRelayFilter.kt | 5 + .../AllFollowsTopNavPerRelayFilterSet.kt | 5 + .../AllUserFollowsByOutboxTopNavFilter.kt | 5 + .../AllUserFollowsByProxyTopNavFilter.kt | 5 + .../allUserFollows/AllUserFollowsFeedFlow.kt | 5 + .../Kind3UserFollowsFeedFlow.kt | 5 + .../topNavFeeds/aroundMe/AroundMeExpander.kt | 5 + .../topNavFeeds/aroundMe/AroundMeFeedFlow.kt | 5 + .../aroundMe/LocationTopNavFilter.kt | 5 + .../aroundMe/LocationTopNavPerRelayFilter.kt | 5 + .../LocationTopNavPerRelayFilterSet.kt | 5 + .../topNavFeeds/global/GlobalFeedFlow.kt | 5 + .../topNavFeeds/global/GlobalTopNavFilter.kt | 5 + .../global/GlobalTopNavPerRelayFilter.kt | 5 + .../global/GlobalTopNavPerRelayFilterSet.kt | 5 + .../hashtag/HashtagTopNavFilter.kt | 5 + .../hashtag/HashtagTopNavPerRelayFilter.kt | 5 + .../hashtag/HashtagTopNavPerRelayFilterSet.kt | 5 + .../topNavFeeds/noteBased/NoteFeedFlow.kt | 2 +- .../AllCommunitiesTopNavFilter.kt | 5 + .../AllCommunitiesTopNavPerRelayFilter.kt | 5 + .../AllCommunitiesTopNavPerRelayFilterSet.kt | 5 + .../author/AuthorsByOutboxTopNavFilter.kt | 5 + .../author/AuthorsByProxyTopNavFilter.kt | 5 + .../author/AuthorsTopNavPerRelayFilter.kt | 5 + .../author/AuthorsTopNavPerRelayFilterSet.kt | 5 + .../community/SingleCommunityTopNavFilter.kt | 5 + .../SingleCommunityTopNavPerRelayFilter.kt | 5 + .../SingleCommunityTopNavPerRelayFilterSet.kt | 5 + .../muted/MutedAuthorsByOutboxTopNavFilter.kt | 5 + .../muted/MutedAuthorsByProxyTopNavFilter.kt | 5 + .../muted/MutedAuthorsTopNavPerRelayFilter.kt | 5 + .../MutedAuthorsTopNavPerRelayFilterSet.kt | 5 + .../topNavFeeds/unknown/UnknownFeedFlow.kt | 5 + .../unknown/UnknownTopNavFilter.kt | 5 + .../unknown/UnknownTopNavPerRelayFilterSet.kt | 5 + .../torState/AccountsTorStateConnector.kt | 5 + .../model/torState/TorRelayEvaluation.kt | 5 + .../model/torState/TorRelaySettings.kt | 5 + .../amethyst/model/torState/TorRelayState.kt | 5 + .../TrustProviderListDecryptionCache.kt | 5 + .../TrustProviderListState.kt | 4 +- .../amethyst/service/ZapPaymentHandler.kt | 4 +- .../EventNotificationConsumer.kt | 2 +- .../relayClient/CacheClientConnector.kt | 2 +- .../PerUserAndFollowListEoseManager.kt | 2 +- .../eoseManagers/PerUserEoseManager.kt | 2 +- .../reqCommand/account/AccountObservers.kt | 2 +- .../drafts/AccountDraftsEoseManager.kt | 2 +- .../AccountFollowsLoaderSubAssembler.kt | 2 +- .../follows/FilterFindFollowMetadataForKey.kt | 2 +- .../metadata/AccountMetadataEoseManager.kt | 2 +- ...NotificationsEoseFromInboxRelaysManager.kt | 2 +- ...otificationsEoseFromRandomRelaysManager.kt | 2 +- .../AccountGiftWrapsEoseManager.kt | 2 +- .../reqCommand/channel/ChannelObservers.kt | 2 +- .../event/EventFinderFilterAssembler.kt | 2 +- .../EventFinderFilterAssemblerSubscription.kt | 2 +- .../reqCommand/event/EventObservers.kt | 6 +- .../loaders/FilterMissingAddressables.kt | 2 +- .../event/loaders/FilterMissingEvents.kt | 4 +- .../watchers/EventWatcherSubAssembler.kt | 4 +- .../FilterRepliesAndReactionsToAddresses.kt | 2 +- .../FilterRepliesAndReactionsToNotes.kt | 2 +- .../NWCFinderFilterAssemblerSubscription.kt | 2 +- .../user/UserFinderFilterAssembler.kt | 2 +- .../UserFinderFilterAssemblerSubscription.kt | 2 +- .../reqCommand/user/UserObservers.kt | 8 +- .../loaders/UserOutboxFinderSubAssembler.kt | 2 +- .../user/watchers/FilterUserMetadataForKey.kt | 2 +- .../user/watchers/UserCardsSubAssembler.kt | 2 +- .../user/watchers/UserReportsSubAssembler.kt | 2 +- .../user/watchers/UserWatcherSubAssembler.kt | 2 +- .../subassemblies/FilterByEvent.kt | 2 +- .../amethyst/service/relays/EOSE.kt | 2 +- .../amethyst/ui/actions/EditPostView.kt | 2 +- .../amethyst/ui/actions/EditPostViewModel.kt | 4 +- .../amethyst/ui/actions/NewMessageTagger.kt | 6 +- .../amethyst/ui/components/ClickableRoute.kt | 4 +- .../ui/components/ReusableZapButton.kt | 4 +- .../amethyst/ui/components/RichTextViewer.kt | 4 +- .../ui/components/SensitivityWarning.kt | 2 +- .../markdown/MarkdownMediaRenderer.kt | 2 +- .../ui/components/toasts/ToastManager.kt | 2 +- .../components/toasts/multiline/ErrorList.kt | 2 +- .../toasts/multiline/MultiErrorToastMsg.kt | 2 +- .../multiline/MultiUserErrorMessageDialog.kt | 2 +- .../amethyst/ui/dal/DefaultFeedOrder.kt | 2 +- .../ui/feeds/ChannelFeedContentState.kt | 2 +- .../amethyst/ui/feeds/FeedContentState.kt | 2 +- .../amethyst/ui/feeds/FeedState.kt | 2 +- .../drawer/AccountSwitchBottomSheet.kt | 2 +- .../ui/navigation/drawer/DrawerContent.kt | 2 +- .../ui/navigation/routes/RouteMaker.kt | 4 +- .../navigation/topbars/FeedFilterSpinner.kt | 2 +- .../amethyst/ui/note/BlankNote.kt | 2 +- .../amethyst/ui/note/BlockReportChecker.kt | 2 +- .../amethyst/ui/note/DisplayAuthorBanner.kt | 2 +- .../vitorpamplona/amethyst/ui/note/Gallery.kt | 2 +- .../vitorpamplona/amethyst/ui/note/Loaders.kt | 6 +- .../amethyst/ui/note/MultiSetCompose.kt | 6 +- .../ui/note/NIP05VerificationDisplay.kt | 6 +- .../amethyst/ui/note/NoteCompose.kt | 4 +- .../amethyst/ui/note/NoteQuickActionMenu.kt | 6 +- .../amethyst/ui/note/PollNote.kt | 4 +- .../amethyst/ui/note/PollNoteViewModel.kt | 4 +- .../amethyst/ui/note/ReactionsRow.kt | 4 +- .../amethyst/ui/note/RelayCompose.kt | 2 +- .../amethyst/ui/note/RelayListBox.kt | 2 +- .../amethyst/ui/note/RelayListRow.kt | 2 +- .../amethyst/ui/note/ReplyInformation.kt | 4 +- .../ui/note/UpdateReactionTypeDialog.kt | 2 +- .../amethyst/ui/note/UserCompose.kt | 2 +- .../amethyst/ui/note/UserProfilePicture.kt | 4 +- .../amethyst/ui/note/UsernameDisplay.kt | 4 +- .../amethyst/ui/note/WatchNoteEvent.kt | 2 +- .../amethyst/ui/note/ZapCustomDialog.kt | 4 +- .../amethyst/ui/note/ZapNoteCompose.kt | 4 +- .../note/creators/invoice/InvoiceRequest.kt | 2 +- .../ui/note/creators/notify/Notifying.kt | 2 +- .../userSuggestions/ShowUserSuggestionList.kt | 2 +- .../userSuggestions/UserSuggestionState.kt | 2 +- .../ui/note/creators/zapsplits/IZapField.kt | 2 +- .../note/creators/zapsplits/SplitConversor.kt | 2 +- .../ui/note/elements/DefaultImageHeader.kt | 4 +- .../ui/note/elements/DisplayCommunity.kt | 2 +- .../ui/note/elements/DisplayHashtags.kt | 2 +- .../amethyst/ui/note/elements/DisplayOts.kt | 2 +- .../ui/note/elements/DisplayReward.kt | 2 +- .../amethyst/ui/note/elements/DropDownMenu.kt | 4 +- .../amethyst/ui/note/elements/ForkInfo.kt | 2 +- .../amethyst/ui/note/elements/TimeAgo.kt | 2 +- .../nip22Comments/CommentPostViewModel.kt | 4 +- .../nip22Comments/GenericCommentPostScreen.kt | 2 +- .../amethyst/ui/note/types/AppDefinition.kt | 2 +- .../amethyst/ui/note/types/AudioTrack.kt | 4 +- .../amethyst/ui/note/types/Badge.kt | 4 +- .../amethyst/ui/note/types/ChannelMessage.kt | 2 +- .../amethyst/ui/note/types/ChatMessage.kt | 2 +- .../ui/note/types/ChatMessageEncryptedFile.kt | 2 +- .../amethyst/ui/note/types/Classifieds.kt | 2 +- .../amethyst/ui/note/types/CommunityHeader.kt | 6 +- .../amethyst/ui/note/types/Emoji.kt | 2 +- .../amethyst/ui/note/types/FileHeader.kt | 2 +- .../amethyst/ui/note/types/FileStorage.kt | 2 +- .../amethyst/ui/note/types/FollowList.kt | 2 +- .../amethyst/ui/note/types/Git.kt | 4 +- .../amethyst/ui/note/types/Highlight.kt | 4 +- .../ui/note/types/InteractiveStory.kt | 4 +- .../amethyst/ui/note/types/LiveActivity.kt | 4 +- .../ui/note/types/LiveActivityChatMessage.kt | 2 +- .../amethyst/ui/note/types/LongForm.kt | 2 +- .../amethyst/ui/note/types/MedicalData.kt | 2 +- .../types/NIP90ContentDiscoveryResponse.kt | 2 +- .../amethyst/ui/note/types/NIP90Status.kt | 2 +- .../amethyst/ui/note/types/PeopleList.kt | 4 +- .../amethyst/ui/note/types/PictureDisplay.kt | 2 +- .../amethyst/ui/note/types/PinList.kt | 2 +- .../amethyst/ui/note/types/Poll.kt | 2 +- .../amethyst/ui/note/types/PrivateMessage.kt | 2 +- .../amethyst/ui/note/types/PublicMessage.kt | 4 +- .../amethyst/ui/note/types/Reaction.kt | 2 +- .../amethyst/ui/note/types/RelayList.kt | 2 +- .../ui/note/types/RenderPostApproval.kt | 2 +- .../amethyst/ui/note/types/Report.kt | 2 +- .../amethyst/ui/note/types/Text.kt | 2 +- .../ui/note/types/TextModification.kt | 2 +- .../amethyst/ui/note/types/Torrent.kt | 2 +- .../amethyst/ui/note/types/TorrentComment.kt | 2 +- .../amethyst/ui/note/types/Video.kt | 2 +- .../amethyst/ui/note/types/VideoDisplay.kt | 2 +- .../amethyst/ui/note/types/VoiceTrack.kt | 2 +- .../amethyst/ui/note/types/Wiki.kt | 2 +- .../amethyst/ui/screen/FeedViewModel.kt | 2 +- .../amethyst/ui/screen/TopNavFilterState.kt | 2 +- .../amethyst/ui/screen/UserFeedState.kt | 2 +- .../amethyst/ui/screen/UserFeedViewModel.kt | 2 +- .../loggedIn/AccountFeedContentStates.kt | 2 +- .../ui/screen/loggedIn/AccountViewModel.kt | 6 +- .../loggedIn/DecryptAndIndexProcessor.kt | 2 +- .../bookmarkgroups/display/ArticleListView.kt | 2 +- .../display/BookmarkGroupItemOptions.kt | 2 +- .../bookmarkgroups/display/PostListView.kt | 2 +- .../ArticleBookmarkListManagementScreen.kt | 2 +- .../PostBookmarkListManagementScreen.kt | 2 +- .../dal/BookmarkPrivateFeedFilter.kt | 2 +- .../bookmarks/dal/BookmarkPublicFeedFilter.kt | 2 +- .../loggedIn/chats/feed/ChatFeedView.kt | 2 +- .../loggedIn/chats/feed/ChatMessageCompose.kt | 2 +- .../screen/loggedIn/chats/feed/ChatTimeAgo.kt | 2 +- .../loggedIn/chats/feed/DrawAuthorInfo.kt | 4 +- .../chats/feed/NewDateOrSubjectDivisor.kt | 2 +- .../types/RenderChangeChannelMetadataNote.kt | 2 +- .../feed/types/RenderCreateChannelNote.kt | 2 +- .../chats/feed/types/RenderDraftEvent.kt | 2 +- .../chats/feed/types/RenderEncryptedFile.kt | 2 +- .../chats/feed/types/RenderRegularTextNote.kt | 2 +- .../chats/privateDM/IncognitoBadge.kt | 2 +- .../chats/privateDM/dal/ChatroomFeedFilter.kt | 2 +- .../privateDM/dal/ChatroomFeedViewModel.kt | 2 +- .../chats/privateDM/header/ChatroomHeader.kt | 2 +- .../privateDM/header/RoomNameOnlyDisplay.kt | 2 +- .../privateDM/send/ChatNewMessageViewModel.kt | 4 +- .../publicChannels/dal/ChannelFeedFilter.kt | 2 +- .../publicChannels/ephemChat/ChannelView.kt | 2 +- .../ephemChat/EphemeralChatScreen.kt | 2 +- .../nip28PublicChat/ChannelView.kt | 2 +- .../PublicChatChannelScreen.kt | 2 +- .../nip53LiveActivities/ChannelView.kt | 2 +- .../LiveActivityChannelScreen.kt | 2 +- .../header/LongLiveActivityChannelHeader.kt | 2 +- .../header/ShortLiveActivityChannelHeader.kt | 2 +- .../send/ChannelNewMessageViewModel.kt | 4 +- .../chats/rooms/ChatroomHeaderCompose.kt | 4 +- .../rooms/dal/ChatroomListKnownFeedFilter.kt | 2 +- .../rooms/dal/ChatroomListNewFeedFilter.kt | 2 +- .../DMsFromUserFilterSubAssembler.kt | 2 +- .../rooms/datasource/FilterNip04DMsFromMe.kt | 2 +- .../rooms/datasource/FilterNip04DMsToMe.kt | 2 +- .../FollowingEphemeralChatSubAssembler.kt | 2 +- .../FollowingPublicChatSubAssembler.kt | 2 +- .../chats/utils/DisplayReplyingToNote.kt | 2 +- .../loggedIn/communities/CommunityScreen.kt | 2 +- .../communities/NewCommunityNoteButton.kt | 2 +- .../communities/dal/CommunityFeedFilter.kt | 4 +- .../communities/dal/CommunityFeedViewModel.kt | 2 +- .../dal/CommunityModerationFeedFilter.kt | 4 +- .../dal/CommunityModerationFeedViewModel.kt | 2 +- .../datasource/CommunityFilterAssembler.kt | 2 +- .../CommunityFilterAssemblerSubscription.kt | 2 +- .../loggedIn/discover/ChannelCardCompose.kt | 2 +- ...yFollowsSetsAndLiveStreamsSubAssembler2.kt | 2 +- ...yLongFormClassifiedsAndDVMSubAssembler1.kt | 2 +- ...yPublicChatsAndCommunitiesSubAssembler3.kt | 2 +- .../DiscoverLongFormFeedFilter.kt | 2 +- .../discover/nip23LongForm/LongFormCard.kt | 2 +- .../nip28Chats/DiscoverChatFeedFilter.kt | 2 +- .../RenderPublicChatChannelThumb.kt | 4 +- .../DiscoverFollowSetsFeedFilter.kt | 2 +- .../discover/nip51FollowSets/FollowSetCard.kt | 2 +- .../DiscoverLiveFeedFilter.kt | 2 +- .../nip53LiveActivities/LiveActivityCard.kt | 4 +- .../nip72Communities/CommunityCard.kt | 4 +- .../DiscoverCommunityFeedFilter.kt | 2 +- .../loggedIn/discover/nip90DVMs/DVMCard.kt | 2 +- .../nip90DVMs/DiscoverNIP89FeedFilter.kt | 2 +- .../nip99Classifieds/ClassifiedsThumb.kt | 2 +- .../DiscoverMarketplaceFeedFilter.kt | 2 +- .../nip99Classifieds/NewProductScreen.kt | 2 +- .../nip99Classifieds/NewProductViewModel.kt | 4 +- .../drafts/dal/DraftEventsFeedFilter.kt | 2 +- .../dvms/DvmContentDiscoveryScreen.kt | 4 +- .../NIP90ContentDiscoveryResponseFilter.kt | 2 +- .../followPacks/feed/FollowPackFeedScreen.kt | 2 +- .../FollowPackFeedConversationsFeedFilter.kt | 4 +- ...ollowPackFeedConversationsFeedViewModel.kt | 2 +- .../dal/FollowPackFeedNewThreadFeedFilter.kt | 4 +- .../FollowPackFeedNewThreadFeedViewModel.kt | 2 +- .../feed/dal/FollowPackMembersFeedFilter.kt | 4 +- .../dal/FollowPackMembersUserFeedViewModel.kt | 2 +- .../FollowPackFeedFilterAssembler.kt | 2 +- ...llowPackFeedFilterAssemblerSubscription.kt | 2 +- .../loggedIn/geohash/GeoHashPostScreen.kt | 2 +- .../loggedIn/geohash/dal/GeoHashFeedFilter.kt | 2 +- .../loggedIn/hashtag/HashtagPostScreen.kt | 2 +- .../loggedIn/hashtag/dal/HashtagFeedFilter.kt | 2 +- .../loggedIn/home/ShortNotePostScreen.kt | 2 +- .../loggedIn/home/ShortNotePostViewModel.kt | 4 +- .../loggedIn/home/VoiceReplyViewModel.kt | 2 +- .../home/dal/HomeConversationsFeedFilter.kt | 2 +- .../loggedIn/home/dal/HomeLiveFilter.kt | 2 +- .../home/dal/HomeNewThreadFeedFilter.kt | 2 +- .../HomeOutboxEventsEoseManager.kt | 2 +- .../loggedIn/lists/display/PeopleListView.kt | 2 +- .../lists/display/ShowUserSuggestions.kt | 2 +- .../lists/display/lists/PeopleListScreen.kt | 4 +- .../display/lists/PeopleListViewModel.kt | 4 +- .../lists/display/packs/FollowPackScreen.kt | 4 +- .../display/packs/FollowPackViewModel.kt | 4 +- .../loggedIn/lists/list/PeopleListItem.kt | 2 +- .../FollowListAndPackAndUserScreen.kt | 2 +- .../FollowListAndPackAndUserView.kt | 2 +- .../notifications/CardFeedContentState.kt | 4 +- .../loggedIn/notifications/CardFeedState.kt | 4 +- .../notifications/NotificationSummaryState.kt | 4 +- .../dal/NotificationFeedFilter.kt | 4 +- .../notifications/donations/ZapTheDevsCard.kt | 2 +- .../publicMessages/NewPublicMessageScreen.kt | 2 +- .../NewPublicMessageViewModel.kt | 4 +- .../screen/loggedIn/profile/ProfileScreen.kt | 2 +- .../profile/bookmarks/BookmarkTabHeader.kt | 2 +- .../dal/UserProfileBookmarksFeedFilter.kt | 4 +- .../dal/UserProfileBookmarksFeedViewModel.kt | 2 +- .../dal/UserProfileConversationsFeedFilter.kt | 4 +- .../UserProfileConversationsFeedViewModel.kt | 2 +- .../datasource/FilterUserProfileFollowers.kt | 2 +- .../datasource/FilterUserProfileMedia.kt | 2 +- .../datasource/FilterUserProfilePosts.kt | 2 +- .../FilterUserProfileZapReceived.kt | 2 +- .../datasource/UserProfileFilterAssembler.kt | 2 +- .../UserProfileFilterAssemblerSubscription.kt | 2 +- .../profile/followers/FollowersTabHeader.kt | 2 +- .../profile/followers/TabFollowers.kt | 2 +- .../dal/UserProfileFollowersFeedFilter.kt | 2 +- .../UserProfileFollowersUserFeedViewModel.kt | 2 +- .../profile/follows/FollowTabHeader.kt | 2 +- .../loggedIn/profile/follows/TabFollows.kt | 2 +- .../dal/UserProfileFollowsFeedFilter.kt | 2 +- .../UserProfileFollowsUserFeedViewModel.kt | 2 +- .../profile/gallery/GalleryCardCompose.kt | 2 +- .../loggedIn/profile/gallery/GalleryThumb.kt | 2 +- .../profile/gallery/QuickActionGallery.kt | 2 +- .../dal/UserProfileGalleryFeedFilter.kt | 4 +- .../dal/UserProfileGalleryFeedViewModel.kt | 2 +- .../profile/hashtags/FollowedTagsTabHeader.kt | 2 +- .../profile/hashtags/TabFollowedTags.kt | 2 +- .../header/DisplayFollowUnfollowButton.kt | 2 +- .../profile/header/DisplayLNAddress.kt | 2 +- .../profile/header/DrawAdditionalInfo.kt | 2 +- .../loggedIn/profile/header/DrawBanner.kt | 2 +- .../loggedIn/profile/header/MessageButton.kt | 2 +- .../loggedIn/profile/header/ProfileActions.kt | 2 +- .../loggedIn/profile/header/ProfileHeader.kt | 2 +- .../profile/header/UserProfileDropDownMenu.kt | 2 +- .../UserAppRecommendationsFeedViewModel.kt | 2 +- ...UserProfileAppRecommendationsFeedFilter.kt | 4 +- .../loggedIn/profile/header/apps/WatchApp.kt | 2 +- .../profile/header/badges/DisplayBadges.kt | 6 +- .../mutual/dal/UserProfileMutualFeedFilter.kt | 6 +- .../dal/UserProfileMutualFeedViewModel.kt | 2 +- .../dal/UserProfileNewThreadFeedFilter.kt | 6 +- .../dal/UserProfileNewThreadsFeedViewModel.kt | 2 +- .../loggedIn/profile/relays/RelayFeedView.kt | 2 +- .../profile/relays/RelayFeedViewModel.kt | 4 +- .../profile/relays/RelaysTabHeader.kt | 2 +- .../loggedIn/profile/relays/TabRelays.kt | 2 +- .../profile/reports/ReportsTabHeader.kt | 2 +- .../loggedIn/profile/reports/TabReports.kt | 2 +- .../reports/WatchReportsAndUpdateFeed.kt | 2 +- .../dal/UserProfileReportFeedViewModel.kt | 2 +- .../dal/UserProfileReportsFeedFilter.kt | 4 +- .../loggedIn/profile/zaps/LnZapFeedState.kt | 2 +- .../loggedIn/profile/zaps/TabReceivedZaps.kt | 2 +- .../profile/zaps/WatchZapsAndUpdateFeed.kt | 2 +- .../loggedIn/profile/zaps/ZapTabHeader.kt | 2 +- .../zaps/dal/UserProfileZapsFeedFilter.kt | 4 +- .../zaps/dal/UserProfileZapsFeedViewModel.kt | 2 +- .../ui/screen/loggedIn/qrcode/ShowQRScreen.kt | 2 +- .../loggedIn/redirect/LoadRedirectScreen.kt | 2 +- .../relays/common/BasicRelaySetupInfo.kt | 2 +- .../loggedIn/report/ReportNoteDialog.kt | 2 +- .../settings/dal/HiddenAccountsFeedFilter.kt | 2 +- .../settings/dal/SpammerAccountsFeedFilter.kt | 2 +- .../loggedIn/threadview/ThreadFeedView.kt | 2 +- .../threadview/dal/LevelFeedViewModel.kt | 2 +- .../threadview/dal/ThreadFeedFilter.kt | 3 +- .../FilterEventsInThreadForRoot.kt | 4 +- .../FilterMissingEventsForThread.kt | 3 +- .../ui/screen/loggedIn/video/VideoScreen.kt | 2 +- .../loggedIn/video/dal/VideoFeedFilter.kt | 5 +- .../VideoOutboxEventsFilterSubAssembler.kt | 2 +- .../amethyst/NewMessageTaggerKeyParseTest.kt | 2 +- .../zaps/UserProfileZapsFeedFilterTest.kt | 2 +- .../commons/threading/Threading.android.kt | 37 ++ .../util/PlatformNumberFormatter.android.kt | 29 ++ .../amethyst/commons/model/IAccount.kt | 89 +++++ .../amethyst/commons}/model/Note.kt | 36 +- .../commons/model/cache/ICacheProvider.kt | 77 ++++ .../model/nip56Reports/UserReportCache.kt | 15 +- .../model/trustedAssertions/UserCardsCache.kt | 33 +- .../amethyst/commons/threading/Threading.kt | 36 ++ .../amethyst/commons/util/EmojiUtils.kt | 134 +++++++ .../amethyst/commons/util/IterableUtils.kt | 26 ++ .../commons/util/PlatformNumberFormatter.kt | 29 ++ .../commons/threading/Threading.jvm.kt | 32 ++ .../util/PlatformNumberFormatter.jvm.kt | 29 ++ 503 files changed, 1675 insertions(+), 888 deletions(-) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt create mode 100644 commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/threading/Threading.android.kt create mode 100644 commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/util/PlatformNumberFormatter.android.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/IAccount.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/Note.kt (96%) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ICacheProvider.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/nip56Reports/UserReportCache.kt (88%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/trustedAssertions/UserCardsCache.kt (84%) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/threading/Threading.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/EmojiUtils.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/IterableUtils.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/PlatformNumberFormatter.kt create mode 100644 commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/threading/Threading.jvm.kt create mode 100644 commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/util/PlatformNumberFormatter.jvm.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index c38e80510..50518c135 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -21,6 +21,11 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.IAccount +import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.commons.richtext.RichTextParser diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index a654dc436..e330f2427 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt index 16d1a7626..1bdfa1ceb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt index 8b66d2ce1..b11e58241 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import android.content.res.Resources import androidx.core.os.ConfigurationCompat import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt index 902a9015b..b20a36ddd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import android.util.LruCache import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.ui.note.njumpLink diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt index 336610ae9..fce3e9256 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.amethyst.ui.dal.ListChange diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Constants.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Constants.kt index f6486e2be..88b8d2e2c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Constants.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Constants.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer object Constants { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt index f9afce5bc..68ee0e9d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LargeSoftCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LargeSoftCache.kt index fb6377c42..ea9d1c2a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LargeSoftCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LargeSoftCache.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.quartz.utils.cache.CacheOperations import java.lang.ref.WeakReference import java.util.concurrent.ConcurrentSkipListMap diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LargeSoftCacheAddressExt.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LargeSoftCacheAddressExt.kt index 42a84bf44..9a22b1984 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LargeSoftCacheAddressExt.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LargeSoftCacheAddressExt.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.utils.cache.CacheCollectors diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 28b05c3f2..b33c1fe15 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -23,6 +23,9 @@ package com.vitorpamplona.amethyst.model import android.util.LruCache import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MediaAspectRatioCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MediaAspectRatioCache.kt index b4b199f9d..6c19286dc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MediaAspectRatioCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MediaAspectRatioCache.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import android.util.LruCache interface MutableMediaAspectRatioCache { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MiniFhir.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MiniFhir.kt index 1e56a62a8..a1d8f37c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MiniFhir.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MiniFhir.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Stable import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonTypeInfo diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ParticipantListBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ParticipantListBuilder.kt index 8e1badf18..7c6de1329 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ParticipantListBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ParticipantListBuilder.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.quartz.nip01Core.core.HexKey class ParticipantListBuilder { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt index 03ba05a3d..ea0745e47 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.quartz.nip01Core.core.Address diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadLevelCalculator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadLevelCalculator.kt index 8b066ce09..793cd699b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadLevelCalculator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadLevelCalculator.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt index cfd116e51..8cdadf40d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.R import kotlinx.serialization.Serializable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt index d1d4303a7..ada6df929 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt index b2e43b270..16aaaa512 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import android.util.LruCache import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.service.previews.UrlPreview diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt deleted file mode 100644 index aa694f42d..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ /dev/null @@ -1,352 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.model - -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.model.nip56Reports.UserReportCache -import com.vitorpamplona.amethyst.model.trustedAssertions.UserCardsCache -import com.vitorpamplona.amethyst.ui.note.toShortDisplay -import com.vitorpamplona.quartz.lightning.Lud06 -import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists -import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent -import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag -import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser -import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent -import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile -import com.vitorpamplona.quartz.nip19Bech32.toNpub -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent -import com.vitorpamplona.quartz.utils.DualCase -import com.vitorpamplona.quartz.utils.Hex -import com.vitorpamplona.quartz.utils.containsAny -import kotlinx.coroutines.flow.MutableStateFlow -import java.math.BigDecimal - -interface UserDependencies - -@Stable -class User( - val pubkeyHex: String, - val nip65RelayListNote: Note, - val dmRelayListNote: Note, -) { - private var reports: UserReportCache? = null - private var cards: UserCardsCache? = null - - // private var deps = ScatterMap, UserDependencies>() - - var info: UserMetadata? = null - - var latestMetadata: MetadataEvent? = null - var latestMetadataRelay: NormalizedRelayUrl? = null - var latestContactList: ContactListEvent? = null - - var zaps = mapOf() - private set - - var relaysBeingUsed = mapOf() - private set - - var flowSet: UserFlowSet? = null - - fun pubkey() = Hex.decode(pubkeyHex) - - fun pubkeyNpub() = pubkey().toNpub() - - fun pubkeyDisplayHex() = pubkeyNpub().toShortDisplay() - - fun dmInboxRelayList() = dmRelayListNote.event as? ChatMessageRelayListEvent - - fun authorRelayList() = nip65RelayListNote.event as? AdvertisedRelayListEvent - - fun toNProfile() = NProfile.create(pubkeyHex, relayHints()) - - fun outboxRelays() = authorRelayList()?.writeRelaysNorm() - - fun relayHints() = authorRelayList()?.writeRelaysNorm()?.take(3) ?: listOfNotNull(latestMetadataRelay) - - fun inboxRelays() = authorRelayList()?.readRelaysNorm() - - fun dmInboxRelays() = dmInboxRelayList()?.relays()?.ifEmpty { null } ?: inboxRelays() - - fun bestRelayHint() = authorRelayList()?.writeRelaysNorm()?.firstOrNull() ?: latestMetadataRelay - - fun toPTag() = PTag(pubkeyHex, bestRelayHint()) - - fun toNostrUri() = "nostr:${toNProfile()}" - - fun toBestShortFirstName(): String { - val fullName = toBestDisplayName() - - val names = fullName.split(' ') - - val firstName = - if (names[0].length <= 3) { - // too short. Remove Dr. - "${names[0]} ${names.getOrNull(1) ?: ""}" - } else { - names[0] - } - - return firstName - } - - fun toBestDisplayName(): String = info?.bestName() ?: pubkeyDisplayHex() - - fun nip05(): String? = info?.nip05 - - fun profilePicture(): String? = info?.picture - - fun updateContactList(event: ContactListEvent) { - if (event.id == latestContactList?.id) return - - val oldContactListEvent = latestContactList - latestContactList = event - - // Update following of the current user - flowSet?.follows?.invalidateData() - - // Update Followers of the past user list - // Update Followers of the new contact list - (oldContactListEvent)?.unverifiedFollowKeySet()?.forEach { - LocalCache - .getUserIfExists(it) - ?.flowSet - ?.followers - ?.invalidateData() - } - (latestContactList)?.unverifiedFollowKeySet()?.forEach { - LocalCache - .getUserIfExists(it) - ?.flowSet - ?.followers - ?.invalidateData() - } - } - - fun addZap( - zapRequest: Note, - zap: Note?, - ) { - if (zaps[zapRequest] == null) { - zaps = zaps + Pair(zapRequest, zap) - flowSet?.zaps?.invalidateData() - } - } - - fun removeZap(zapRequestOrZapEvent: Note) { - if (zaps.containsKey(zapRequestOrZapEvent)) { - zaps = zaps.minus(zapRequestOrZapEvent) - flowSet?.zaps?.invalidateData() - } else if (zaps.containsValue(zapRequestOrZapEvent)) { - zaps = zaps.filter { it.value != zapRequestOrZapEvent } - flowSet?.zaps?.invalidateData() - } - } - - fun zappedAmount(): BigDecimal { - var amount = BigDecimal.ZERO - zaps.forEach { - val itemValue = (it.value?.event as? LnZapEvent)?.amount - if (itemValue != null) { - amount += itemValue - } - } - - return amount - } - - fun addRelayBeingUsed( - relay: NormalizedRelayUrl, - eventTime: Long, - ) { - val here = relaysBeingUsed[relay] - if (here == null) { - relaysBeingUsed = relaysBeingUsed + Pair(relay, RelayInfo(relay, eventTime, 1)) - } else { - if (eventTime > here.lastEvent) { - here.lastEvent = eventTime - } - here.counter++ - } - - flowSet?.usedRelays?.invalidateData() - } - - fun updateUserInfo( - newUserInfo: UserMetadata, - latestMetadata: MetadataEvent, - ) { - info = newUserInfo - info?.tags = latestMetadata.tags.toImmutableListOfLists() - info?.cleanBlankNames() - - if (newUserInfo.lud16.isNullOrBlank()) { - info?.lud06?.let { - if (it.lowercase().startsWith("lnurl")) { - info?.lud16 = Lud06().toLud16(it) - } - } - } - - flowSet?.metadata?.invalidateData() - } - - fun isFollowing(user: User): Boolean = latestContactList?.isTaggedUser(user.pubkeyHex) ?: false - - fun transientFollowCount(): Int? = latestContactList?.unverifiedFollowKeySet()?.size - - fun transientFollowerCount(): Int = LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false } - - fun reportsOrNull(): UserReportCache? = reports - - fun reports(): UserReportCache = reports ?: UserReportCache().also { reports = it } - - // fun reportsOrNull(): UserReports? = deps[UserReports::class] as? UserReports - - // fun reports(): UserReports = deps.getOrPut(UserReports::class) { UserReports() } as UserReports - - fun cardsOrNull(): UserCardsCache? = cards - - fun cards(): UserCardsCache = cards ?: UserCardsCache().also { cards = it } - - fun containsAny(hiddenWordsCase: List): Boolean { - if (hiddenWordsCase.isEmpty()) return false - - if (toBestDisplayName().containsAny(hiddenWordsCase)) { - return true - } - - if (profilePicture()?.containsAny(hiddenWordsCase) == true) { - return true - } - - if (info?.banner?.containsAny(hiddenWordsCase) == true) { - return true - } - - if (info?.about?.containsAny(hiddenWordsCase) == true) { - return true - } - - if (info?.lud06?.containsAny(hiddenWordsCase) == true) { - return true - } - - if (info?.lud16?.containsAny(hiddenWordsCase) == true) { - return true - } - - if (info?.nip05?.containsAny(hiddenWordsCase) == true) { - return true - } - - return false - } - - fun anyNameStartsWith(username: String): Boolean = info?.anyNameStartsWith(username) ?: false - - @Synchronized - fun createOrDestroyFlowSync(create: Boolean) { - if (create) { - if (flowSet == null) { - flowSet = UserFlowSet(this) - } - } else { - if (flowSet != null && flowSet?.isInUse() == false) { - flowSet = null - } - } - } - - fun flow(): UserFlowSet { - if (flowSet == null) { - createOrDestroyFlowSync(true) - } - return flowSet!! - } - - fun clearFlow() { - if (flowSet != null && flowSet?.isInUse() == false) { - createOrDestroyFlowSync(false) - } - } -} - -@Stable -class UserFlowSet( - u: User, -) { - // Observers line up here. - val metadata = UserBundledRefresherFlow(u) - val follows = UserBundledRefresherFlow(u) - val followers = UserBundledRefresherFlow(u) - val usedRelays = UserBundledRefresherFlow(u) - val zaps = UserBundledRefresherFlow(u) - val statuses = UserBundledRefresherFlow(u) - - fun isInUse(): Boolean = - metadata.hasObservers() || - follows.hasObservers() || - followers.hasObservers() || - usedRelays.hasObservers() || - zaps.hasObservers() || - statuses.hasObservers() -} - -@Immutable -data class RelayInfo( - val url: NormalizedRelayUrl, - var lastEvent: Long, - var counter: Long, -) - -@Stable -class UserBundledRefresherFlow( - val user: User, -) { - val stateFlow = MutableStateFlow(UserState(user)) - - fun invalidateData() { - stateFlow.tryEmit(UserState(user)) - } - - fun hasObservers() = stateFlow.subscriptionCount.value > 0 -} - -@Immutable -class UserState( - val user: User, -) - -fun Set.toHexSet() = mapTo(LinkedHashSet(size)) { it.pubkeyHex } - -fun Set.toSortedHexes() = map { it.pubkeyHex }.sorted() - -fun List.toHexes() = map { it.pubkeyHex } - -fun List.toHexSet() = mapTo(LinkedHashSet(size)) { it.pubkeyHex } - -fun List.toSortedHexes() = map { it.pubkeyHex }.sorted() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt index 0126df2fe..1f73d36a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.accountsCache +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import android.content.ContentResolver import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AccountSettings diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListDecryptionCache.kt index ca8e73549..90f2dc39c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListDecryptionCache.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.edits +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt index b67f31d19..4f2160ff0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.edits +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt index 12a28673d..51a638065 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.emphChat +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListDecryptionCache.kt index 60ff2f80f..87676ea32 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListDecryptionCache.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.emphChat +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent import com.vitorpamplona.quartz.experimental.ephemChat.list.roomSet import com.vitorpamplona.quartz.experimental.ephemChat.list.rooms diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt index 3899d0aef..20b818887 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.emphChat +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/LocalRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/LocalRelayListState.kt index 20917ae6c..260046260 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/LocalRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/LocalRelayListState.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.localRelays +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountHomeRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountHomeRelayState.kt index 89403654e..8a5222bf6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountHomeRelayState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountHomeRelayState.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip01UserMetadata +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt index 65f76c500..e541de1ab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip01UserMetadata +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/NotificationInboxRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/NotificationInboxRelayState.kt index 0729475dc..0b5166480 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/NotificationInboxRelayState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/NotificationInboxRelayState.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip01UserMetadata +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState import kotlinx.coroutines.CoroutineScope diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt index d64ec40bd..a831e786a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt @@ -20,9 +20,14 @@ */ package com.vitorpamplona.amethyst.model.nip01UserMetadata +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + +import com.vitorpamplona.amethyst.commons.model.UserState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.utils.Log diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/DeclaredFollowsPerOutboxRelay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/DeclaredFollowsPerOutboxRelay.kt index f2934b113..6f0c8f650 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/DeclaredFollowsPerOutboxRelay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/DeclaredFollowsPerOutboxRelay.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip02FollowLists +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxRelayLoader import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxOrProxyRelays.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxOrProxyRelays.kt index 13d29c44f..0ca41acab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxOrProxyRelays.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxOrProxyRelays.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip02FollowLists +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListReusedOutboxOrProxyRelays.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListReusedOutboxOrProxyRelays.kt index 2f5751936..da5d81f78 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListReusedOutboxOrProxyRelays.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListReusedOutboxOrProxyRelays.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip02FollowLists +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt index 921c8d353..d88442bdf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip02FollowLists +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/Kind3FollowListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/Kind3FollowListState.kt index 46f8a0cba..f377abc4c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/Kind3FollowListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/Kind3FollowListState.kt @@ -20,11 +20,16 @@ */ package com.vitorpamplona.amethyst.model.nip02FollowLists +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.UserState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/IncomingOtsEventVerifier.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/IncomingOtsEventVerifier.kt index 84f0b77d3..be0cdb538 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/IncomingOtsEventVerifier.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/IncomingOtsEventVerifier.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip03Timestamp +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache import kotlinx.coroutines.CoroutineScope diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt index d941acebf..7572f071b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip03Timestamp +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/TorAwareOkHttpOtsResolverBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/TorAwareOkHttpOtsResolverBuilder.kt index b86a8c77b..e1f33bb7b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/TorAwareOkHttpOtsResolverBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/TorAwareOkHttpOtsResolverBuilder.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip03Timestamp +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder import com.vitorpamplona.quartz.nip03Timestamp.okhttp.OkHttpBitcoinExplorer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/LoadRelayInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/LoadRelayInfo.kt index 4ed3eb50d..c84cd6fe9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/LoadRelayInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/LoadRelayInfo.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip11RelayInfo +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.produceState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11CachedRetriever.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11CachedRetriever.kt index dbd59899f..8fea6bbbc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11CachedRetriever.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11CachedRetriever.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip11RelayInfo +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import android.util.LruCache import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11Retriever.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11Retriever.kt index fd4bab976..2bc324f11 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11Retriever.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11Retriever.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip11RelayInfo +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.toHttp import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/RetrieveResult.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/RetrieveResult.kt index bdc158317..8d203c006 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/RetrieveResult.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/RetrieveResult.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip11RelayInfo +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation import com.vitorpamplona.quartz.utils.TimeUtils diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmInboxRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmInboxRelayState.kt index 6ffe95928..3123283a1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmInboxRelayState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmInboxRelayState.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip17Dms +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt index ba18f9850..c19363987 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip17Dms +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip18Reposts/RepostAction.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip18Reposts/RepostAction.kt index 4d5d1276d..adaa18854 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip18Reposts/RepostAction.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip18Reposts/RepostAction.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.model.nip18Reposts -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip25Reactions/ReactionAction.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip25Reactions/ReactionAction.kt index bf580d23c..7e6fffed8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip25Reactions/ReactionAction.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip25Reactions/ReactionAction.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip25Reactions -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt index 75fa41af7..5636e1632 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.model.nip28PublicChats import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.note.toShortDisplay import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListDecryptionCache.kt index a0a54ff93..036c97e12 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListDecryptionCache.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip28PublicChats +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent import com.vitorpamplona.quartz.nip28PublicChat.list.channelSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt index f52bd4687..1b8668a44 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip28PublicChats +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt index 52d0f1aee..4be970327 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip30CustomEmojis +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedAddresses import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip38UserStatuses/UserStatusAction.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip38UserStatuses/UserStatusAction.kt index 834e2437f..69e7bfd52 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip38UserStatuses/UserStatusAction.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip38UserStatuses/UserStatusAction.kt @@ -20,7 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip38UserStatuses -import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt index 8b5990b65..98e7ed9e1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip47WalletConnect +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentQueryState import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BookmarkListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BookmarkListState.kt index 6606229ec..2f5d96d62 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BookmarkListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BookmarkListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists -import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt index b20482350..8abcb2196 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.service.checkNotInMainThread diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt index f4db82512..f8f7d831f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleListDecryptionCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListDecryptionCache.kt index a3f3df679..a7614341f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListDecryptionCache.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListState.kt index 3fc015f51..9b07abdfa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListDecryptionCache.kt index 73ba12906..a10cb5c4b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListDecryptionCache.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListState.kt index 388e78d4c..28d9876b0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListCard.kt index 25f818e72..340ba533b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListCard.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.geohashLists +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + data class GeohashListCard( val relays: List, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListDecryptionCache.kt index 4ea18aadb..752c1b2c2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListDecryptionCache.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.geohashLists -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListState.kt index dc7eaa534..bcd6a4afd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.geohashLists +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent import com.vitorpamplona.quartz.utils.Log diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt index 6e73b068a..e1bd8c05a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt index 8a1341376..ea7908fd1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent import com.vitorpamplona.quartz.utils.Log diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListDecryptionCache.kt index 13d1ce9d4..78d371097 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListDecryptionCache.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListState.kt index 738ec1c6d..a239b5d8c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListState.kt @@ -20,11 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkList.kt index 74333236f..712e7e74a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkList.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Stable import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt index b8686c695..a30a4e764 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.anyNotNullEvent import com.vitorpamplona.amethyst.model.eventIdSet import com.vitorpamplona.amethyst.model.events diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListDecryptionCache.kt index 90314fc4a..80f342b14 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListDecryptionCache.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.muteList +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListState.kt index 6ad8b31a6..40d64aec8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.muteList +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt index 59ab65117..1c2078fa9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt @@ -20,11 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.peopleList +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.anyNotNullEvent import com.vitorpamplona.amethyst.model.eventIdSet import com.vitorpamplona.amethyst.model.events diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleList.kt index c0f1af20a..13c4ce988 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleList.kt @@ -20,8 +20,13 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.peopleList +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListDecryptionCache.kt index 30ab6b599..ef84acbb2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListDecryptionCache.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.peopleList +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache import com.vitorpamplona.quartz.nip51Lists.muteList.mutedUserIdSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt index 77e6d0a54..2ec777a93 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt @@ -20,11 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.peopleList +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.anyNotNullEvent import com.vitorpamplona.amethyst.model.eventIdSet import com.vitorpamplona.amethyst.model.events diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListDecryptionCache.kt index 2f4fff3cd..fc4fc80ae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListDecryptionCache.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListState.kt index d50c88368..60ef944bb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayLists/GenericRelayListCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayLists/GenericRelayListCache.kt index e62fd6a8a..db48ab376 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayLists/GenericRelayListCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayLists/GenericRelayListCache.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.relayLists -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayLists/RelayListCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayLists/RelayListCard.kt index bf12514c4..da7e5d0e5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayLists/RelayListCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayLists/RelayListCard.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.relayLists +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListDecryptionCache.kt index bf88a3752..617a91ccf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListDecryptionCache.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.searchRelays +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListState.kt index 3d5b3919f..2d3bbff1c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListState.kt @@ -20,11 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.searchRelays +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.DefaultSearchRelayList import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListDecryptionCache.kt index 276b47abb..d45560807 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListDecryptionCache.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListState.kt index 96250d6f5..81830bf7c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt index d595164c2..ae7c16f9d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.model.nip53LiveActivities import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.note.toShortDisplay import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/ReportAction.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/ReportAction.kt index 1dfc00f11..0809a5119 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/ReportAction.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/ReportAction.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip56Reports -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip56Reports.ReportType diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/Nip65RelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/Nip65RelayListState.kt index bdeecd489..cba2e7721 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/Nip65RelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/Nip65RelayListState.kt @@ -20,11 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip65RelayList +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.Constants import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListDecryptionCache.kt index 2ea70cb3a..642ff0ae8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListDecryptionCache.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip72Communities +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt index 3b025c79a..e2d801a61 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt @@ -20,11 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip72Communities +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt index 6a69cc8e2..1455f2976 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip78AppSpecific +import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.AccountSyncedSettingsInternal import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.core.JsonMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip96FileStorage/FileStorageServerListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip96FileStorage/FileStorageServerListState.kt index 2d78464d1..9a4b921bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip96FileStorage/FileStorageServerListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip96FileStorage/FileStorageServerListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip96FileStorage +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent import kotlinx.coroutines.CoroutineScope diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt index de9d963e0..a2de6b5bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nipB7Blossom +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/CreatedAtComparator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/CreatedAtComparator.kt index c13a4be33..cd4228f92 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/CreatedAtComparator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/CreatedAtComparator.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.observables -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note object CreatedAtComparator : Comparator { override fun compare( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/LatestByKindAndAuthor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/LatestByKindAndAuthor.kt index c8b43ab5d..27b60d00d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/LatestByKindAndAuthor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/LatestByKindAndAuthor.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.observables -import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.Event import kotlinx.coroutines.flow.MutableStateFlow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/LatestByKindWithETag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/LatestByKindWithETag.kt index 057b71030..895c5aed6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/LatestByKindWithETag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/LatestByKindWithETag.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.observables +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent import kotlinx.coroutines.flow.MutableStateFlow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountPreferenceStores.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountPreferenceStores.kt index 28d856840..de669056d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountPreferenceStores.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountPreferenceStores.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.preferences +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.Preferences diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountSecretsEncryptedStores.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountSecretsEncryptedStores.kt index 7f42b168f..2255bf22e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountSecretsEncryptedStores.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountSecretsEncryptedStores.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.preferences +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.stringPreferencesKey import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/DataStoreExt.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/DataStoreExt.kt index 18f9eac67..626fa8d28 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/DataStoreExt.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/DataStoreExt.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.preferences +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/EncryptedDataStore.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/EncryptedDataStore.kt index 6c8daf2b2..f4ce0e86d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/EncryptedDataStore.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/EncryptedDataStore.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.preferences +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/KeyStoreEncryption.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/KeyStoreEncryption.kt index eca8de687..5fcb34b73 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/KeyStoreEncryption.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/KeyStoreEncryption.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.preferences +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import android.os.Build import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/TorSharedPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/TorSharedPreferences.kt index 0944a4360..a95121165 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/TorSharedPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/TorSharedPreferences.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.preferences +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import android.content.Context import androidx.compose.runtime.Stable import androidx.datastore.preferences.core.booleanPreferencesKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt index d4c88552e..65d9f2419 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.preferences +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import android.content.Context import androidx.appcompat.app.AppCompatDelegate import androidx.compose.runtime.Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UpdatablePropertyFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UpdatablePropertyFlow.kt index 5daabe519..5773ba92d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UpdatablePropertyFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UpdatablePropertyFlow.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.preferences +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/EmptyRoleBasedHttpClientBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/EmptyRoleBasedHttpClientBuilder.kt index 982083af5..a7a0c25e0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/EmptyRoleBasedHttpClientBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/EmptyRoleBasedHttpClientBuilder.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.privacyOptions +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import okhttp3.OkHttpClient import java.net.InetSocketAddress diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/IRoleBasedHttpClientBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/IRoleBasedHttpClientBuilder.kt index 0b75bb3d6..676495876 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/IRoleBasedHttpClientBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/IRoleBasedHttpClientBuilder.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.privacyOptions +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import okhttp3.OkHttpClient interface IRoleBasedHttpClientBuilder { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/RoleBasedHttpClientBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/RoleBasedHttpClientBuilder.kt index 7d7f61ec6..647939c94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/RoleBasedHttpClientBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/RoleBasedHttpClientBuilder.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.privacyOptions +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow import com.vitorpamplona.amethyst.ui.tor.TorType diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/SingleRoleBasedHttpClientBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/SingleRoleBasedHttpClientBuilder.kt index 6c3e85304..6d632dfdb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/SingleRoleBasedHttpClientBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/SingleRoleBasedHttpClientBuilder.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.privacyOptions +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import okhttp3.OkHttpClient import java.net.InetSocketAddress diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt index 9219c2b92..01a2b82c1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.model.privateChats import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NotesGatherer -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NotesGatherer +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.amethyst.ui.dal.ListChange import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt index 6a81dcf37..51d62b621 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.privateChats -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt index c9434c50b..645f6ef0b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.serverList +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.nip02FollowLists.Kind3FollowListState import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt index a9e0ff6d9..7ac69e56f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.serverList +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxOrProxyRelays diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithIndexRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithIndexRelayListsState.kt index 37cc66c47..f026f6927 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithIndexRelayListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithIndexRelayListsState.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.serverList +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxOrProxyRelays diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithSearchRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithSearchRelayListsState.kt index 825a6d98f..2b676ff42 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithSearchRelayListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithSearchRelayListsState.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.serverList +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxOrProxyRelays diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedServerListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedServerListState.kt index fb011d83e..2a1ac20bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedServerListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedServerListState.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.serverList +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.nip96FileStorage.FileStorageServerListState import com.vitorpamplona.amethyst.model.nipB7Blossom.BlossomServerListState import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt index be26a1f19..d6ca458a4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.serverList +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt index 16cf45525..0166c4816 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds +import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedDecryptionCaches.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedDecryptionCaches.kt index 22cc69c34..770973a13 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedDecryptionCaches.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedDecryptionCaches.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.muteList.MuteListDecryptionCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt index 17b3d2bf6..403e450f2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.ALL_FOLLOWS import com.vitorpamplona.amethyst.model.ALL_USER_FOLLOWS import com.vitorpamplona.amethyst.model.AROUND_ME diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedFlowsType.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedFlowsType.kt index 150fa4c99..b7d296455 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedFlowsType.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedFlowsType.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.FlowCollector diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavFilter.kt index 2ec3d0a43..3a6d40678 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavFilter.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilter.kt index 4cbff4957..fe2d3c196 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilter.kt @@ -20,4 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + interface IFeedTopNavPerRelayFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilterSet.kt index 5f922fbeb..acc4c695e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilterSet.kt @@ -20,4 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + interface IFeedTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/MergedTopFeedAuthorListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/MergedTopFeedAuthorListsState.kt index 0728886f2..2d266b774 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/MergedTopFeedAuthorListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/MergedTopFeedAuthorListsState.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxLoaderState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxLoaderState.kt index a2980535b..581dce286 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxLoaderState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxLoaderState.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.unknown.UnknownTopNavPerRelayFilterSet import kotlinx.coroutines.CoroutineScope diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt index 5b1b6f843..844c224f7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds -import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.model.Constants import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt index cd6675eca..da1471e7c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.allFollows +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.CommunityRelayLoader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByProxyTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByProxyTopNavFilter.kt index ac271a340..d3ae11da3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByProxyTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByProxyTopNavFilter.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.allFollows +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt index e451c3129..69351963c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.allFollows +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.serverList.MergedFollowListsState import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilter.kt index af5595c04..71346da77 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilter.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.allFollows +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilterSet.kt index 032b4590c..33e9be694 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilterSet.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.allFollows +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByOutboxTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByOutboxTopNavFilter.kt index b3bf7538e..521b6419d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByOutboxTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByOutboxTopNavFilter.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByProxyTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByProxyTopNavFilter.kt index 6064ce5b2..3f614020f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByProxyTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByProxyTopNavFilter.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsFeedFlow.kt index 24a26e3f7..36165e204 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsFeedFlow.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.serverList.MergedFollowListsState import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/Kind3UserFollowsFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/Kind3UserFollowsFeedFlow.kt index ebf0967b8..c1ed9719c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/Kind3UserFollowsFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/Kind3UserFollowsFeedFlow.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.nip02FollowLists.Kind3FollowListState import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeExpander.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeExpander.kt index 028931a18..d783b0bea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeExpander.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeExpander.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.fonfon.kgeohash.GeoHash fun compute50kmLine(geoHash: GeoHash): List { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt index a0c66edc0..7b3d6a354 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter import com.vitorpamplona.amethyst.service.location.LocationState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavFilter.kt index 6dce8b3cb..ce972e38c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavFilter.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilter.kt index 219c10719..f159ebbc1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilter.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilterSet.kt index 85564aac7..dca520ed7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilterSet.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalFeedFlow.kt index 3aa72e1f8..1d9e8cc24 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalFeedFlow.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.global +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavFilter.kt index 2b961af8e..ad833830d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavFilter.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.global +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilter.kt index 772a298dc..abc66908a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilter.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.global +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilterSet.kt index 2e30e5dc5..fa9faba4c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilterSet.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.global +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavFilter.kt index 47235adc2..f838a927f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavFilter.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.hashtag +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilter.kt index 8c5a0406f..8ce3574df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilter.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.hashtag +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilterSet.kt index 35c5481b0..bf02de7b0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilterSet.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.hashtag +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/NoteFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/NoteFeedFlow.kt index 60a132e31..13bf07015 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/NoteFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/NoteFeedFlow.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased -import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.model.topNavFeeds.FeedDecryptionCaches import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavFilter.kt index 57e2e3425..1d58491ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavFilter.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.CommunityRelayLoader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilter.kt index dd532c3e9..5a9c9129e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilter.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter class AllCommunitiesTopNavPerRelayFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilterSet.kt index 061c40e5a..476a9a810 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilterSet.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavFilter.kt index 96ae9d520..479fe0aa5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavFilter.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByProxyTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByProxyTopNavFilter.kt index 4489c55ee..04bba033f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByProxyTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByProxyTopNavFilter.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilter.kt index abd72b1ec..87648ab77 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilter.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilterSet.kt index 8809e7496..231fe5169 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilterSet.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavFilter.kt index 8a4c3bb4d..9fb08bed1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavFilter.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilter.kt index 0c6ba0610..365be26c4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilter.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter import com.vitorpamplona.quartz.nip01Core.core.Address diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilterSet.kt index c376579fa..bc4b71603 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilterSet.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavFilter.kt index 76028ddbe..f042111da 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavFilter.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByProxyTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByProxyTopNavFilter.kt index 36eea59a9..f3898f01b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByProxyTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByProxyTopNavFilter.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilter.kt index b56f83feb..5636079a2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilter.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilterSet.kt index 69d4c1029..d7d142889 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilterSet.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownFeedFlow.kt index 075f449ca..35d9443c4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownFeedFlow.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.unknown +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter import kotlinx.coroutines.flow.FlowCollector diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavFilter.kt index c20e5793f..e87515a19 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavFilter.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.unknown +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavPerRelayFilterSet.kt index 556f596fc..bbcbfa87b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavPerRelayFilterSet.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.unknown +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet object UnknownTopNavPerRelayFilterSet : IFeedTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/AccountsTorStateConnector.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/AccountsTorStateConnector.kt index 6748f1df1..9102c4f00 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/AccountsTorStateConnector.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/AccountsTorStateConnector.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.torState +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.CoroutineScope diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayEvaluation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayEvaluation.kt index 1041e69cc..6205e3a74 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayEvaluation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayEvaluation.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.torState +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.ui.tor.TorType import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isLocalHost diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelaySettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelaySettings.kt index b0dc31d59..50cb79dbb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelaySettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelaySettings.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.torState +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.ui.tor.TorType data class TorRelaySettings( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt index 6ec0711f8..56b60479f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.torState +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow import com.vitorpamplona.amethyst.ui.tor.TorType diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListDecryptionCache.kt index c35dfbaac..e21f97957 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListDecryptionCache.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.model.trustedAssertions +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User + + import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent import com.vitorpamplona.quartz.experimental.trustedAssertions.list.serviceProviderSet import com.vitorpamplona.quartz.experimental.trustedAssertions.list.serviceProviders diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListState.kt index 758747bb9..1ad0c874d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.trustedAssertions +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent import com.vitorpamplona.quartz.experimental.trustedAssertions.list.tags.ProviderTypes import com.vitorpamplona.quartz.experimental.trustedAssertions.list.tags.ServiceProviderTag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt index 42adc194f..fe31f0f5a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt @@ -23,10 +23,10 @@ package com.vitorpamplona.amethyst.service import android.content.Context import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt index 1452776d5..a592e9749 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt @@ -26,9 +26,9 @@ import androidx.core.content.ContextCompat import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendDMNotification import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendZapNotification import com.vitorpamplona.amethyst.ui.note.showAmount diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt index a91a08a84..4d191464e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.service.relayClient +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.EventCollector diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt index ff2b8411a..8540f32ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relays.EOSEAccountKey import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt index 3b5583f38..8daa532c0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountObservers.kt index d24717964..a27671530 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountObservers.kt @@ -24,8 +24,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.User import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/drafts/AccountDraftsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/drafts/AccountDraftsEoseManager.kt index 0487a503c..63d48ff95 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/drafts/AccountDraftsEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/drafts/AccountDraftsEoseManager.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.drafts -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt index 4693c9e74..1280b6ac3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.BundledUpdate import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.IEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/FilterFindFollowMetadataForKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/FilterFindFollowMetadataForKey.kt index afd76dec4..be61bf554 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/FilterFindFollowMetadataForKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/FilterFindFollowMetadataForKey.kt @@ -20,12 +20,12 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Constants import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList import com.vitorpamplona.amethyst.model.DefaultSearchRelayList import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt index 4b7c90f8c..c4da16643 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromInboxRelaysManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromInboxRelaysManager.kt index c4dd64399..2e901816d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromInboxRelaysManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromInboxRelaysManager.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromRandomRelaysManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromRandomRelaysManager.kt index 34074f380..6198600d7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromRandomRelaysManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromRandomRelaysManager.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt index 7771c04a4..dde939631 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip59GiftWraps -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt index 1add909d1..cad055057 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt @@ -24,9 +24,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.ChannelState -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt index db3d6bb25..b11c8b81c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.NoteEventLoaderSubAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers.EventWatcherSubAssembler diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssemblerSubscription.kt index d0ffe095e..9d29e0a8f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssemblerSubscription.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventObservers.kt index 50cdad965..6ea96fbe3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventObservers.kt @@ -24,9 +24,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt index ebea6dd10..c62cada62 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders -import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState import com.vitorpamplona.quartz.nip01Core.core.Address diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt index d76b06042..9c38810dd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders -import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt index ff3f946a6..948abb47f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToAddresses.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToAddresses.kt index 5e5b5343b..80be5df1d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToAddresses.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToAddresses.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers -import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToNotes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToNotes.kt index dd76df8f6..477dbafe6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToNotes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToNotes.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt index fbb130e33..082148491 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc import android.annotation.SuppressLint import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt index dc4240828..1817971a3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders.UserOutboxFinderSubAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers.UserCardsSubAssembler diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssemblerSubscription.kt index 7e1450331..e63a31d0e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssemblerSubscription.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user import android.annotation.SuppressLint import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt index d40d4fb69..4116ff41c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt @@ -25,12 +25,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.UserState import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.NoteState -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserOutboxFinderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserOutboxFinderSubAssembler.kt index 76108f540..0e2baeb13 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserOutboxFinderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserOutboxFinderSubAssembler.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.BaseEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows.pickRelaysToLoadUsers import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt index 2f634dbcf..66f27b124 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt index 21431db86..991815b9d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.toHexSet import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt index 5b2fc6337..0c5146654 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.toHexSet import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt index d3dd7e145..0c1a76916 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.BaseEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByEvent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByEvent.kt index 5401aeaa1..2cf8dac3b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByEvent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByEvent.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies -import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingEvents import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.potentialRelaysToFindEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt index 0473c743b..bceb9c3d1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.service.relays import androidx.collection.LruCache -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relays.MutableTime import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index 8387333de..8e05d7c18 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -73,8 +73,8 @@ import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.viewmodel.compose.viewModel import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.richtext.RichTextParser -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.playback.composable.VideoView import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt index 0018215d9..13402c08d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt @@ -31,10 +31,10 @@ import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt index 40ce38396..710352eeb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.ui.actions import androidx.compose.runtime.Immutable -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt index f0c758bf3..20a377827 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt @@ -54,8 +54,8 @@ import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ReusableZapButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ReusableZapButton.kt index 5e1dbae50..b1eee5f3a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ReusableZapButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ReusableZapButton.kt @@ -45,8 +45,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index c0ca8c5a0..16e23e73a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -62,6 +62,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em import com.vitorpamplona.amethyst.commons.compose.produceCachedState import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.richtext.Base64Segment import com.vitorpamplona.amethyst.commons.richtext.BechSegment import com.vitorpamplona.amethyst.commons.richtext.CashuSegment @@ -85,8 +87,6 @@ import com.vitorpamplona.amethyst.commons.richtext.VideoSegment import com.vitorpamplona.amethyst.commons.richtext.WithdrawSegment import com.vitorpamplona.amethyst.model.HashtagIcon import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon import com.vitorpamplona.amethyst.service.CachedRichTextParser import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt index 540e2d2fd..c4cda359b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt @@ -50,7 +50,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt index 7bb60c2c3..336b1b6ce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt @@ -33,10 +33,10 @@ import androidx.compose.ui.unit.dp import com.halilibo.richtext.ui.MediaRenderer import com.halilibo.richtext.ui.string.InlineContent import com.halilibo.richtext.ui.string.RichTextString +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.HashtagIcon -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.components.DisplayFullNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/ToastManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/ToastManager.kt index 4e10e8ea3..3377c9dd6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/ToastManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/ToastManager.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.components.toasts -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.ui.components.toasts.multiline.MultiErrorToastMsg import com.vitorpamplona.amethyst.ui.components.toasts.multiline.UserBasedErrorMessage import kotlinx.coroutines.flow.MutableStateFlow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt index 5950caf61..203878a30 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt @@ -42,8 +42,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeToMessage diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiErrorToastMsg.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiErrorToastMsg.kt index 7bf27518d..c487a7e3f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiErrorToastMsg.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiErrorToastMsg.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.components.toasts.multiline import androidx.compose.runtime.Immutable -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.ui.components.toasts.ToastMsg import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt index a296c5dc6..798586a3c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt @@ -31,8 +31,8 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DefaultFeedOrder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DefaultFeedOrder.kt index 72f1e65b8..dc2cec049 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DefaultFeedOrder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DefaultFeedOrder.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.dal -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.Card val DefaultFeedOrder: Comparator = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt index cf62c2cf5..363e367cb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.feeds import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.BundledInsert import com.vitorpamplona.amethyst.service.BundledUpdate import com.vitorpamplona.amethyst.service.checkNotInMainThread diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt index 283735f7a..fcb224691 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.feeds import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.BasicBundledInsert import com.vitorpamplona.amethyst.service.BasicBundledUpdate import com.vitorpamplona.amethyst.service.checkNotInMainThread diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedState.kt index c6e831f75..49ec6b74d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedState.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.feeds import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.MutableStateFlow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt index cda6c74fb..341240e05 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt @@ -57,8 +57,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPicture import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt index fb16001a6..f5982f646 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt @@ -92,8 +92,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserContactCardsFollowerCount import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt index 374c68d92..c0d5578b1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.navigation.routes +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt index 163011d9c..f401e5060 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt @@ -57,7 +57,7 @@ import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.ui.components.LoadingAnimation diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt index d472fa4b8..0281a36ef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt @@ -36,7 +36,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlockReportChecker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlockReportChecker.kt index bdcfa1d94..91e57fa3f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlockReportChecker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlockReportChecker.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/DisplayAuthorBanner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/DisplayAuthorBanner.kt index 81de3a9ea..81dbf2aa8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/DisplayAuthorBanner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/DisplayAuthorBanner.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.note import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.note.elements.BannerImage import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.SimpleImageBorder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Gallery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Gallery.kt index 3bbf5b382..e68bc3dd2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Gallery.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Gallery.kt @@ -35,7 +35,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.navigation.routes.routeForUser diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt index ca5886d93..f626fdb03 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt @@ -32,10 +32,10 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index 100db2569..5b4905fb2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -63,9 +63,9 @@ import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.CachedRichTextParser import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserContactCardsScore import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPicture diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt index 48ad80fbf..87e46cc26 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt @@ -51,9 +51,9 @@ import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons import com.vitorpamplona.amethyst.commons.hashtags.Tunestr -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserNip05 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 6d152f6ca..c33ef3c27 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -53,8 +53,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.produceCachedStateAsync -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelPicture import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeCommunityApprovalNeedStatus diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt index 070403e87..220e4c33a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -77,9 +77,9 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Popup import androidx.core.graphics.ColorUtils import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeEditDraftTo import com.vitorpamplona.amethyst.ui.painterRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt index 263b563c7..505096383 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt @@ -71,9 +71,9 @@ import androidx.compose.ui.window.Popup import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteZaps import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt index a18b3f221..6f08d5334 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt @@ -26,9 +26,9 @@ import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 279a3414f..7c9e5fd2e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -99,8 +99,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteReactionCount import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteReactions diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt index 7b21446da..94d766a1b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt @@ -40,7 +40,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.RelayInfo +import com.vitorpamplona.amethyst.commons.model.RelayInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ButtonBorder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt index 6fe9311f2..f36c21c7d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt @@ -50,7 +50,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt index 43bbc572c..eace08cc7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt @@ -50,7 +50,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt index d3392b4f4..e0ce4bdab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt @@ -34,8 +34,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt index 819b366c1..c2c21aae1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt @@ -74,8 +74,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder +import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.service.firstFullChar import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEventAndMapNotNull import com.vitorpamplona.amethyst.ui.components.AnimatedBorderTextCornerRadius diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt index f5670dac8..5122faacb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt @@ -31,7 +31,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenUser import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowing import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index 06a62f2f9..161daffd3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -45,8 +45,8 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserContactCardsScore import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt index 0c8a4a4ef..8429bc2c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt @@ -34,8 +34,8 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.lifecycle.LifecycleOwner -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.service.tts.TextToSpeechHelper diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/WatchNoteEvent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/WatchNoteEvent.kt index d6f82836b..ddc84af33 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/WatchNoteEvent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/WatchNoteEvent.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteHasEvent import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt index 019455527..aa6204e3d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt @@ -64,9 +64,9 @@ import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.ui.components.TextSpinner import com.vitorpamplona.amethyst.ui.components.TitleExplainer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt index 27374f6a0..343efdce0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt @@ -39,9 +39,9 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserAboutMe import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/InvoiceRequest.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/InvoiceRequest.kt index 59baf599e..2027a4826 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/InvoiceRequest.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/InvoiceRequest.kt @@ -52,7 +52,7 @@ import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons import com.vitorpamplona.amethyst.commons.hashtags.Lightning -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/Notifying.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/Notifying.kt index 519ebea28..f46752530 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/Notifying.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/Notifying.kt @@ -37,7 +37,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUser import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/ShowUserSuggestionList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/ShowUserSuggestionList.kt index 95b6cdca9..45e52219e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/ShowUserSuggestionList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/ShowUserSuggestionList.kt @@ -39,8 +39,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.searchCommand.UserSearchDataSourceSubscription import com.vitorpamplona.amethyst.ui.note.AboutDisplay import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/UserSuggestionState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/UserSuggestionState.kt index cad37791d..e895d2b0c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/UserSuggestionState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/UserSuggestionState.kt @@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.ui.note.creators.userSuggestions import androidx.compose.runtime.Stable import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.searchCommand.SearchQueryState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/IZapField.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/IZapField.kt index 9a85f6178..88a9e1c44 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/IZapField.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/IZapField.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.note.creators.zapsplits import androidx.compose.runtime.MutableState import androidx.compose.ui.text.input.TextFieldValue -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User interface IZapField { val forwardZapTo: MutableState> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/SplitConversor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/SplitConversor.kt index b5e8f9967..b4d34e140 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/SplitConversor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/SplitConversor.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.note.creators.zapsplits -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup import kotlin.math.round diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt index 7d63d9c9e..0af785fd0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt @@ -29,8 +29,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.blur import androidx.compose.ui.layout.ContentScale import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserBanner import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.note.BaseUserPicture diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt index e60b4fc4b..384c7ce45 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt @@ -26,7 +26,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.text.style.TextOverflow -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.components.ClickableTextColor import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt index b0300c708..8e864a6ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt @@ -33,7 +33,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.text.style.TextOverflow import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.components.ClickableTextColor import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt index 5cc1d6b3e..0ac4ea33a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt @@ -30,7 +30,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.components.buildLinkString import com.vitorpamplona.amethyst.ui.note.LoadOts import com.vitorpamplona.amethyst.ui.note.timeAgoNoDot diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt index 102cc6f4d..12db8096e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt @@ -55,8 +55,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteReplies import com.vitorpamplona.amethyst.ui.components.ClickableTextColor import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt index a15f4c944..7275b9922 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt @@ -40,8 +40,8 @@ import androidx.compose.ui.text.AnnotatedString import androidx.core.content.ContextCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserBookmarks import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserFollows import com.vitorpamplona.amethyst.ui.actions.EditPostView diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt index b8fa57650..76dd023ac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt @@ -32,7 +32,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextOverflow import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUser import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt index 9e224b999..6c565784d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt @@ -31,7 +31,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.note.timeAgo import com.vitorpamplona.amethyst.ui.note.timeAgoShort import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index 1a99000a7..d227e9a5f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -33,10 +33,10 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.MediaCompressor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt index 6f5d1e758..865d473db 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt @@ -48,7 +48,7 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt index 85074bd44..b1086dd68 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt @@ -55,8 +55,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.richtext.RichTextParser -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.ClickableTextPrimary import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt index cb37c17cc..52aa7c46a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt @@ -41,8 +41,8 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.playback.composable.LoadThumbAndThenVideoView import com.vitorpamplona.amethyst.service.playback.composable.VideoView import com.vitorpamplona.amethyst.service.playback.composable.WaveformData diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Badge.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Badge.kt index 7b770f447..30e000371 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Badge.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Badge.kt @@ -44,8 +44,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.UserPicture diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChannelMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChannelMessage.kt index 9709ac890..3818cb4c1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChannelMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChannelMessage.kt @@ -29,7 +29,7 @@ import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessage.kt index 3c75950cd..85f98b592 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessage.kt @@ -31,7 +31,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessageEncryptedFile.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessageEncryptedFile.kt index 0b2094448..ee12d7bb8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessageEncryptedFile.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessageEncryptedFile.kt @@ -32,7 +32,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt index bbae48b24..b05fb29d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt @@ -38,8 +38,8 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt index 115431784..f294957d8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt @@ -60,9 +60,9 @@ import androidx.compose.ui.unit.sp import androidx.core.content.ContextCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.components.RichTextViewer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Emoji.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Emoji.kt index 0e03df161..24bc5b5bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Emoji.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Emoji.kt @@ -46,7 +46,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt index 0705aea28..9d5ad038e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt @@ -25,11 +25,11 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.layout.ContentScale +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.RichTextParser -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt index 7b3ea345f..e0e1026ff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt @@ -26,10 +26,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.layout.ContentScale import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent import com.vitorpamplona.amethyst.commons.richtext.MediaLocalImage import com.vitorpamplona.amethyst.commons.richtext.MediaLocalVideo -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.LoadNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt index c79454a9d..ddba83c13 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt @@ -41,8 +41,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEventAndMap import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt index 52ba58254..2ddbb08d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt @@ -40,8 +40,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.components.ClickableUrl import com.vitorpamplona.amethyst.ui.components.SensitivityWarning diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt index f20b58ed8..eec42d61e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt @@ -38,8 +38,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.ui.components.ClickableTextPrimary diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/InteractiveStory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/InteractiveStory.kt index 08afdcf5b..ecf2df323 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/InteractiveStory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/InteractiveStory.kt @@ -37,8 +37,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt index 88ccdf5cb..7426b3d63 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt @@ -45,9 +45,9 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.playback.composable.VideoView import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt index fb8822831..4b6480890 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt @@ -29,7 +29,7 @@ import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LoadLiveActivityChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt index 9a4461dac..1e1ee0020 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt @@ -36,7 +36,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.BaseUserPicture diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MedicalData.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MedicalData.kt index e8606d59e..134b5bd57 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MedicalData.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MedicalData.kt @@ -39,10 +39,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Bundle import com.vitorpamplona.amethyst.model.FhirElementDatabase import com.vitorpamplona.amethyst.model.LensSpecification -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Patient import com.vitorpamplona.amethyst.model.Practitioner import com.vitorpamplona.amethyst.model.Prism diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt index 734c2fde2..b94dc1389 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90Status.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90Status.kt index f4ce7b415..73839665f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90Status.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90Status.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.note.types import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip90Dvms.NIP90StatusEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt index 8d818b56c..3975b6aaf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt @@ -47,8 +47,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.ui.components.ShowMoreButton import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.UserCompose diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PictureDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PictureDisplay.kt index 6b0f27e35..496656ec8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PictureDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PictureDisplay.kt @@ -35,8 +35,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PinList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PinList.kt index afc871c08..56c522aed 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PinList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PinList.kt @@ -45,7 +45,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.components.ShowMoreButton import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt index bf0a07f68..e617a686f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt @@ -32,8 +32,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt index 583a22ab7..b12fe3f97 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt @@ -35,7 +35,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PublicMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PublicMessage.kt index 78f0fedd1..f054f1282 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PublicMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PublicMessage.kt @@ -33,8 +33,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.ui.components.RenderUserAsClickableText import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Reaction.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Reaction.kt index eb2a23085..dad53adef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Reaction.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Reaction.kt @@ -25,7 +25,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt index df050701a..425061ab2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt @@ -45,7 +45,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.RelayListCard import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserRelayIntoList import com.vitorpamplona.amethyst.ui.components.ShowMoreButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderPostApproval.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderPostApproval.kt index 3a3f33615..7828fb641 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderPostApproval.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderPostApproval.kt @@ -34,7 +34,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.note.NoteCompose diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Report.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Report.kt index 5a861de6a..0977b6311 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Report.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Report.kt @@ -27,7 +27,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.NoteCompose diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt index 8432deaa2..200324c06 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt @@ -33,8 +33,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt index efa188f01..3e9e73eeb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt @@ -44,7 +44,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.ui.actions.EditPostView import com.vitorpamplona.amethyst.ui.components.GenericLoadable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt index 3b4eedba0..5694fe9e8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt @@ -49,8 +49,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.net.toUri import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.countToHumanReadableBytes import com.vitorpamplona.amethyst.ui.components.ShowMoreButton import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt index d102e605a..b95999e11 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt @@ -44,8 +44,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.countToHumanReadableBytes import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.components.GenericLoadable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt index b8c26a7be..ac780446d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt @@ -42,11 +42,11 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.RichTextParser -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VideoDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VideoDisplay.kt index ab73587d6..888435d37 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VideoDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VideoDisplay.kt @@ -25,11 +25,11 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.layout.ContentScale +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.RichTextParser -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt index 0a0210fb0..62a65891f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt @@ -53,7 +53,7 @@ import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi import androidx.media3.ui.PlayerView import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.service.playback.composable.GetVideoController import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState import com.vitorpamplona.amethyst.service.playback.composable.WaveformData diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Wiki.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Wiki.kt index e6f53b753..1fd46ebde 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Wiki.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Wiki.kt @@ -37,7 +37,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index 0ba80943e..85b99c659 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt index 6cf7aea7d..b7cda7519 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt @@ -24,11 +24,11 @@ import android.content.Context import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.model.ALL_FOLLOWS import com.vitorpamplona.amethyst.model.ALL_USER_FOLLOWS import com.vitorpamplona.amethyst.model.AROUND_ME import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS import com.vitorpamplona.amethyst.service.checkNotInMainThread diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedState.kt index a1c984997..56998320f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedState.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.MutableStateFlow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt index b31b33843..9abd56ac1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt @@ -25,8 +25,8 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.BundledUpdate import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.dal.FeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt index 10982b6c8..7f722adaf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedContentState import com.vitorpamplona.amethyst.ui.feeds.FeedContentState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 8264d3cd5..03b225c84 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -40,15 +40,15 @@ import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.GenericBaseCache import com.vitorpamplona.amethyst.commons.compose.GenericBaseCacheAsync +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.UiSettingsFlow import com.vitorpamplona.amethyst.model.UrlCachedPreviewer -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt index 0bf7a6cb9..658ef4f31 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.privateChats.ChatroomList import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/ArticleListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/ArticleListView.kt index 2280da6c1..bf04e8d76 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/ArticleListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/ArticleListView.kt @@ -31,7 +31,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt index 78cc44273..1b99f3fa6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt @@ -37,7 +37,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.AnnotatedString import androidx.core.content.ContextCompat import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.actions.EditPostView import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.components.GenericLoadable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/PostListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/PostListView.kt index 93562c8ef..2088876e1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/PostListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/PostListView.kt @@ -31,7 +31,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/ArticleBookmarkListManagementScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/ArticleBookmarkListManagementScreen.kt index ec36b977e..793296a0c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/ArticleBookmarkListManagementScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/ArticleBookmarkListManagementScreen.kt @@ -36,7 +36,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt index 14bb6478d..0a8525835 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt @@ -36,7 +36,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPrivateFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPrivateFeedFilter.kt index 85cdc1d77..764872f0b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPrivateFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPrivateFeedFilter.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.dal +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.FeedFilter class BookmarkPrivateFeedFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPublicFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPublicFeedFilter.kt index 34a3962fb..6a1dd0213 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPublicFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPublicFeedFilter.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.dal +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.FeedFilter class BookmarkPublicFeedFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt index d411332a1..7f5941f24 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt @@ -30,7 +30,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt index f06c49cff..04371a8f0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt @@ -42,7 +42,7 @@ import androidx.compose.ui.Alignment.Companion.CenterStart import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt index 2a6f7e060..6530d6f8b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt @@ -25,7 +25,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.note.timeAgoShort import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Font12SP diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt index bd2a75c42..c13e7f11a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt @@ -25,8 +25,8 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/NewDateOrSubjectDivisor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/NewDateOrSubjectDivisor.kt index b7c23d1e7..d0364a129 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/NewDateOrSubjectDivisor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/NewDateOrSubjectDivisor.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.note.dateFormatter import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts.ChatDivisor import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChangeChannelMetadataNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChangeChannelMetadataNote.kt index 84e9a7d29..b78224194 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChangeChannelMetadataNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChangeChannelMetadataNote.kt @@ -24,7 +24,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt index 34c7600c2..18f55cbd2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt @@ -48,8 +48,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Constants -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderDraftEvent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderDraftEvent.kt index 99a5d2031..de4b34c48 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderDraftEvent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderDraftEvent.kt @@ -24,7 +24,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.ui.graphics.Color -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.ObserveDraftEvent import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt index f48d0b3af..79703d3be 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt @@ -30,11 +30,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.RichTextParser -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt index f704649b7..f9457b866 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/IncognitoBadge.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/IncognitoBadge.kt index a7f7444e8..dd1e4beca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/IncognitoBadge.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/IncognitoBadge.kt @@ -25,7 +25,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.theme.IncognitoIconModifier import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedFilter.kt index 5dfd03295..a8c9cd27c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedFilter.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.dal +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.ChangesFlowFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt index dd6676f11..7710250a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt @@ -24,8 +24,8 @@ import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.ChangesFlowFilter import com.vitorpamplona.amethyst.ui.dal.ListChange import com.vitorpamplona.amethyst.ui.feeds.FeedContentState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/ChatroomHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/ChatroomHeader.kt index 8f062f1b1..513062c35 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/ChatroomHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/ChatroomHeader.kt @@ -31,7 +31,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures import com.vitorpamplona.amethyst.ui.note.UsernameDisplay diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt index e5f0a008f..c741d2f1b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt @@ -29,7 +29,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserShortName import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index c2ce04d5e..f1aac6ce7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -32,10 +32,10 @@ import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt index db66af206..0e345b8dc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.ChangesFlowFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt index 2a8adefca..dafd65425 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt @@ -30,7 +30,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt index 70d6d7f45..d0b62168c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt @@ -24,7 +24,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt index 89f03d6b4..9cdd0c712 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt @@ -30,7 +30,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt index c5796b7ae..4b7da5aa6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt @@ -24,7 +24,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt index 5c14da323..d11109d47 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt @@ -29,7 +29,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt index 67ce16968..db194010d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt @@ -25,7 +25,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.service.OnlineChecker import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt index 97fa81a62..1eea430e7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt @@ -40,8 +40,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt index b66031baa..dca0c690e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt @@ -36,8 +36,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 4d3a5b6b6..f4e784e93 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -33,12 +33,12 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index 86d975767..a1c07fa7c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt @@ -41,8 +41,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt index 4a786a408..d63a4463e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.replace import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt index 5e9426ee1..c3ae917a4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.replace import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt index 5a5eae521..2620d9c94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsFromMe.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsFromMe.kt index e32888b58..c5f2f4236 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsFromMe.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsFromMe.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToMe.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToMe.kt index 2488bed77..0b96093d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToMe.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToMe.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt index 4a733cce0..2d9cea248 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt index 405a22c9a..298bdeed0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/DisplayReplyingToNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/DisplayReplyingToNote.kt index cecbca8e0..6ac23bb2b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/DisplayReplyingToNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/DisplayReplyingToNote.kt @@ -36,7 +36,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.ChatroomMessageCompose diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt index e1d8cc2fe..9704c09e0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt @@ -44,7 +44,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/NewCommunityNoteButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/NewCommunityNoteButton.kt index 12e14fd8d..aec9a2d97 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/NewCommunityNoteButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/NewCommunityNoteButton.kt @@ -27,7 +27,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedFilter.kt index 0e6b9efad..119d13b08 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedFilter.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.dal +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedViewModel.kt index abd71e4ae..973a200ad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.ui.screen.FeedViewModel class CommunityFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedFilter.kt index 114fb2f67..dd5f5bf5e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedFilter.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.dal +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedViewModel.kt index 14b423fac..8d8a2fbcf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.ui.screen.FeedViewModel class CommunityModerationFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssembler.kt index 531a781bd..187154ca2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource -import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssemblerSubscription.kt index ed4f4f3f1..bc19a824f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssemblerSubscription.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt index 100ab2740..52402fc1d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt @@ -25,7 +25,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.CheckHiddenFeedWatchBlockAndReport diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsSetsAndLiveStreamsSubAssembler2.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsSetsAndLiveStreamsSubAssembler2.kt index eacc12b13..c1cb7c1ef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsSetsAndLiveStreamsSubAssembler2.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsSetsAndLiveStreamsSubAssembler2.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.makeFollowSetsFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryLongFormClassifiedsAndDVMSubAssembler1.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryLongFormClassifiedsAndDVMSubAssembler1.kt index 0ba4eb8ed..affd4e77f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryLongFormClassifiedsAndDVMSubAssembler1.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryLongFormClassifiedsAndDVMSubAssembler1.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.makeLongFormFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryPublicChatsAndCommunitiesSubAssembler3.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryPublicChatsAndCommunitiesSubAssembler3.kt index c5a38b8f7..296374d91 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryPublicChatsAndCommunitiesSubAssembler3.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryPublicChatsAndCommunitiesSubAssembler3.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.makePublicChatsFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt index 1e9464341..cda62f410 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.filterIntoSet import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormCard.kt index 317275f2e..779cb8deb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormCard.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.types.LongFormHeader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt index 745f31a8c..fd46c64d4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt index 0d4c93a03..a58549ba8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt @@ -41,9 +41,9 @@ import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import coil3.compose.AsyncImagePainter import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.ParticipantListBuilder -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByProxyTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt index 0f29d1b81..70e816d26 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.filterIntoSet import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt index 529f49992..7a7649b8b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt @@ -38,8 +38,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt index 856de6e36..f19dd82bb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByProxyTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt index 58ec5f2a1..43a08ecfa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt @@ -41,10 +41,10 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ParticipantListBuilder -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt index e1089042f..3ec7ecda0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt @@ -43,10 +43,10 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ParticipantListBuilder -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt index 9ab2c6f8b..f064c8f04 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.mapNotNullIntoSet import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.FilterByListParams diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt index 34a00ce15..56136b282 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt @@ -42,7 +42,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt index 5c3e8ef37..c476adadc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.amethyst.model.filterIntoSet import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt index df2a99c3a..a927e4ae8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt @@ -44,7 +44,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/DiscoverMarketplaceFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/DiscoverMarketplaceFeedFilter.kt index 65b26e890..c58b517e7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/DiscoverMarketplaceFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/DiscoverMarketplaceFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.filterIntoSet import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.FilterByListParams diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt index 4128694c3..e2183c242 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt @@ -47,7 +47,7 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index 90bf27713..cd1a6a429 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -33,10 +33,10 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.MediaCompressor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/dal/DraftEventsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/dal/DraftEventsFeedFilter.kt index 21364ee0e..fc5ec00bb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/dal/DraftEventsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/dal/DraftEventsFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.dal +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.filterIntoSet import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt index 80826edec..2dde71441 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt @@ -50,8 +50,8 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap import com.vitorpamplona.amethyst.ui.components.LoadNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryResponseFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryResponseFilter.kt index 5e1102248..00a039916 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryResponseFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryResponseFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.dal +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.observables.CreatedAtComparator import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.FilterByListParams diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt index ecefa80a5..082e9c81b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt @@ -54,7 +54,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.lifecycle.viewmodel.compose.viewModel import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedFilter.kt index 4895b9f7b..610ec1620 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedFilter.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows.AllUserFollowsByOutboxTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows.AllUserFollowsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedViewModel.kt index 3851b9dfb..f8ea4ecc8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.ui.screen.FeedViewModel class FollowPackFeedConversationsFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt index ac8b756a2..6facf7555 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.filterIntoSet import com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows.AllUserFollowsByOutboxTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows.AllUserFollowsByProxyTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedViewModel.kt index 74b9159a6..3c1041fec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.ui.screen.FeedViewModel class FollowPackFeedNewThreadFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersFeedFilter.kt index 2329eca2c..8aab5aaca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersFeedFilter.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache.checkGetOrCreateUser -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersUserFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersUserFeedViewModel.kt index 2eff19754..faf545117 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersUserFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersUserFeedViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel class FollowPackMembersUserFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterAssembler.kt index ef3f1a91a..1b3e1e636 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterAssembler.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.datasource import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterAssemblerSubscription.kt index c30204f15..d61fa1e26 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterAssemblerSubscription.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.datasourc import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt index 03597a7b6..dba994980 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt index 60418c3aa..080f29df6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.dal +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt index 669cc05fc..b1c0b99d3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt index f961365e3..ce733de72 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.dal +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 504c723be..580f9f1fb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -56,7 +56,7 @@ import androidx.compose.ui.unit.dp import androidx.core.util.Consumer import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.RecordVoiceButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 652cc425f..f8dc572fd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -36,10 +36,10 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState.EmojiMedia import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.CompressorQuality diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt index faa5d677a..fcf3d073e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt @@ -28,7 +28,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.service.uploads.UploadingState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeConversationsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeConversationsFeedFilter.kt index ad9757837..0f8d97b89 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeConversationsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeConversationsFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt index 62f55d898..773631b62 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt index be69e6a00..c30a5b7c2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.filterIntoSet import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt index 6646fa1ad..ea175412c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/PeopleListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/PeopleListView.kt index 16560c968..e09cf6bea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/PeopleListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/PeopleListView.kt @@ -35,7 +35,7 @@ import androidx.compose.material3.OutlinedIconButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.UserComposeNoAction import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/ShowUserSuggestions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/ShowUserSuggestions.kt index 1e4f1c93a..c424a7928 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/ShowUserSuggestions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/ShowUserSuggestions.kt @@ -63,8 +63,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.searchCommand.UserSearchDataSourceSubscription import com.vitorpamplona.amethyst.ui.note.AboutDisplay import com.vitorpamplona.amethyst.ui.note.ClearTextIcon diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListScreen.kt index ff72e8bd8..66cbea473 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListScreen.kt @@ -71,9 +71,9 @@ import androidx.core.content.ContextCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListViewModel.kt index 168f8484b..9a54ec8cf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListViewModel.kt @@ -26,9 +26,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import kotlinx.coroutines.Dispatchers diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackScreen.kt index 743fdf8c1..7ab4aca1a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackScreen.kt @@ -64,9 +64,9 @@ import androidx.core.content.ContextCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackViewModel.kt index c51741b07..185bcd754 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackViewModel.kt @@ -26,9 +26,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent import kotlinx.coroutines.Dispatchers diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/PeopleListItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/PeopleListItem.kt index 5c9b84e41..1e0df3812 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/PeopleListItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/PeopleListItem.kt @@ -55,8 +55,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleList import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserScreen.kt index 9333c185f..a63889e48 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserScreen.kt @@ -36,8 +36,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserView.kt index 2332c2559..3d4b7c929 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserView.kt @@ -37,7 +37,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt index 8dc2fd2f4..77f44f552 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt @@ -24,11 +24,11 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.BundledInsert import com.vitorpamplona.amethyst.service.BundledUpdate import com.vitorpamplona.amethyst.service.checkNotInMainThread diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedState.kt index 776841efc..161a186c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedState.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji import com.vitorpamplona.amethyst.ui.feeds.LoadedFeedState import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt index daa23cac1..ad2a9c318 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt @@ -25,10 +25,10 @@ import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel import com.patrykandpatrick.vico.core.common.data.ExtraStore import com.patrykandpatrick.vico.core.common.data.MutableExtraStore +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.BundledInsert import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.note.showAmountInteger diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt index fac9b7661..82589c3ed 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.dal +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.filterIntoSet import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt index 4f7d249be..9aa38037e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt @@ -45,8 +45,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.ReusableZapButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt index 1643a6798..dc6039197 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt @@ -56,7 +56,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt index 19ba54057..ad2647e31 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt @@ -33,10 +33,10 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.MediaCompressor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt index 71eb5802c..90f02b507 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt @@ -58,8 +58,8 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.IntSize import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenUser import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/BookmarkTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/BookmarkTabHeader.kt index 730d1dc3e..d4668fc8a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/BookmarkTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/BookmarkTabHeader.kt @@ -24,7 +24,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserBookmarkCount import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedFilter.kt index a094f7821..bea1a9c71 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedFilter.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.dal +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedViewModel.kt index 8a84b7086..d54bcafea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.screen.FeedViewModel class UserProfileBookmarksFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedFilter.kt index 8ecaead08..e48a3bf1d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedFilter.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.conversations.dal +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedViewModel.kt index 3b276e1a7..36d0bf04f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.conversations.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.screen.FeedViewModel class UserProfileConversationsFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileFollowers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileFollowers.kt index 42d758ae4..bb038bc52 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileFollowers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileFollowers.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMedia.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMedia.kt index 96e2b5a89..49d812fa5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMedia.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMedia.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt index dbbd86c0e..05646d218 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileZapReceived.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileZapReceived.kt index b48a22fb8..686f2380d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileZapReceived.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileZapReceived.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt index 5a217020b..91a9ecbe3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssemblerSubscription.kt index 94af9e02b..ed1c25e4f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssemblerSubscription.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/FollowersTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/FollowersTabHeader.kt index da530a8df..416456e90 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/FollowersTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/FollowersTabHeader.kt @@ -24,7 +24,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserContactCardsFollowerCount import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/TabFollowers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/TabFollowers.kt index d242b9625..cad4dd237 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/TabFollowers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/TabFollowers.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserFollowers import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersFeedFilter.kt index 4d81c18ca..f97ac95ed 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers.dal +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.dal.FeedFilter class UserProfileFollowersFeedFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersUserFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersUserFeedViewModel.kt index 674421db3..84924a850 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersUserFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersUserFeedViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel class UserProfileFollowersUserFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/FollowTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/FollowTabHeader.kt index 58e085517..0a61ccd9e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/FollowTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/FollowTabHeader.kt @@ -24,7 +24,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserFollowCount import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/TabFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/TabFollows.kt index aaa112059..8147d5eb6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/TabFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/TabFollows.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserFollows import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsFeedFilter.kt index 0208a8a55..1bcd90095 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows.dal +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsUserFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsUserFeedViewModel.kt index ea4e65f2a..8b4d3034f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsUserFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsUserFeedViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel class UserProfileFollowsUserFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt index d83168ddb..61927b150 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt @@ -26,7 +26,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.SensitivityWarning diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt index 575d63492..19d479171 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt @@ -45,11 +45,11 @@ import androidx.media3.ui.PlayerView import coil3.compose.AsyncImagePainter import coil3.compose.SubcomposeAsyncImage import coil3.compose.SubcomposeAsyncImageContent +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.richtext.MediaUrlContent import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.RichTextParser.Companion.isVideoUrl -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.playback.composable.GetVideoController import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.GetMediaItem import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/QuickActionGallery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/QuickActionGallery.kt index 778e99afd..8d7e079fa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/QuickActionGallery.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/QuickActionGallery.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.note.QuickActionAlertDialogOneButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt index 63955e745..cd5b4a2c6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.dal +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.amethyst.ui.dal.FilterByListParams diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedViewModel.kt index 93ff86b75..c46cab934 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.screen.FeedViewModel class UserProfileGalleryFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/FollowedTagsTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/FollowedTagsTabHeader.kt index 4c46aa32b..108bd44a8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/FollowedTagsTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/FollowedTagsTabHeader.kt @@ -24,7 +24,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserTagFollowCount import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/TabFollowedTags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/TabFollowedTags.kt index 9730cfbc6..ee4b2aa31 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/TabFollowedTags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/TabFollowedTags.kt @@ -31,7 +31,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserTagFollows import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayFollowUnfollowButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayFollowUnfollowButton.kt index 0bf1d7d7c..6ef8f98b9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayFollowUnfollowButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayFollowUnfollowButton.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowing import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.FollowButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayLNAddress.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayLNAddress.kt index 776bdc8c9..c1a1f8299 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayLNAddress.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayLNAddress.kt @@ -32,7 +32,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.ui.actions.InformationDialog import com.vitorpamplona.amethyst.ui.components.ClickableTextPrimary import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt index 6f4a4dc8b..15c03b951 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt @@ -45,7 +45,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUser import com.vitorpamplona.amethyst.ui.components.ClickableTextPrimary import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawBanner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawBanner.kt index 579c6ae2a..458969a29 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawBanner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawBanner.kt @@ -37,8 +37,8 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.richtext.RichTextParser -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserBanner import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog import com.vitorpamplona.amethyst.ui.painterRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt index 0ec05e877..d0ae678b7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt @@ -28,7 +28,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeToMessage import com.vitorpamplona.amethyst.ui.painterRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileActions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileActions.kt index 0d9c6b30e..1d0f129ef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileActions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileActions.kt @@ -24,7 +24,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenUser import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt index 5c9b35109..f9c6c6576 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt @@ -49,8 +49,8 @@ import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.richtext.RichTextParser -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserProfileDropDownMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserProfileDropDownMenu.kt index c42054030..ba5c21679 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserProfileDropDownMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserProfileDropDownMenu.kt @@ -30,7 +30,7 @@ import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.AnnotatedString import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.ui.note.externalLinkForUser import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/UserAppRecommendationsFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/UserAppRecommendationsFeedViewModel.kt index 2d169e701..34cdb1f6f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/UserAppRecommendationsFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/UserAppRecommendationsFeedViewModel.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.ui.screen.FeedViewModel class UserAppRecommendationsFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/UserProfileAppRecommendationsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/UserProfileAppRecommendationsFeedFilter.kt index 9bfda48aa..eba05ef88 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/UserProfileAppRecommendationsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/UserProfileAppRecommendationsFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/WatchApp.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/WatchApp.kt index b24d30293..4e68eb164 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/WatchApp.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/WatchApp.kt @@ -33,7 +33,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import coil3.compose.AsyncImage -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt index dbccec86d..b0359ef39 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt @@ -33,10 +33,10 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEventAndMapNotNull import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt index 22a90a03e..dd9419b52 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt @@ -20,11 +20,11 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual.dal +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedViewModel.kt index 36dfbc4ce..0da57999f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.screen.FeedViewModel class UserProfileMutualFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt index 3b4b1b727..68c644911 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt @@ -20,11 +20,11 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads.dal +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadsFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadsFeedViewModel.kt index 0a92c613f..2f16bb745 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadsFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadsFeedViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.screen.FeedViewModel class UserProfileNewThreadsFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt index 3ba91b326..eaec8b103 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt @@ -34,7 +34,7 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.RelayInfo +import com.vitorpamplona.amethyst.commons.model.RelayInfo import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt index 81440c8ec..e253497ab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt @@ -25,8 +25,8 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.model.RelayInfo -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.RelayInfo import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelaysTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelaysTabHeader.kt index 4c211d588..8f431e5ef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelaysTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelaysTabHeader.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.relays import androidx.compose.material3.Text import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/TabRelays.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/TabRelays.kt index 2a2a4258a..aeb64b416 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/TabRelays.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/TabRelays.kt @@ -29,7 +29,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/ReportsTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/ReportsTabHeader.kt index f6fc2b3ea..18e83cf67 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/ReportsTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/ReportsTabHeader.kt @@ -24,7 +24,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserReportCount import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/TabReports.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/TabReports.kt index 8701d7e94..2d2f6a4b5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/TabReports.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/TabReports.kt @@ -24,7 +24,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/WatchReportsAndUpdateFeed.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/WatchReportsAndUpdateFeed.kt index 3b27d5b59..538d332a8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/WatchReportsAndUpdateFeed.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/WatchReportsAndUpdateFeed.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports import androidx.compose.runtime.Composable -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserReports import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.dal.UserProfileReportFeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/dal/UserProfileReportFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/dal/UserProfileReportFeedViewModel.kt index b01ab9936..016330ad2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/dal/UserProfileReportFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/dal/UserProfileReportFeedViewModel.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.ui.screen.FeedViewModel class UserProfileReportFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/dal/UserProfileReportsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/dal/UserProfileReportsFeedFilter.kt index b04fa0f4b..4038c3e08 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/dal/UserProfileReportsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/dal/UserProfileReportsFeedFilter.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.dal -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedState.kt index 868c1fc97..131cd24f9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedState.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.MutableStateFlow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/TabReceivedZaps.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/TabReceivedZaps.kt index e51280ff2..3ce69a793 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/TabReceivedZaps.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/TabReceivedZaps.kt @@ -24,7 +24,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.UserProfileZapsFeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/WatchZapsAndUpdateFeed.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/WatchZapsAndUpdateFeed.kt index 4930c1184..911ef4b9f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/WatchZapsAndUpdateFeed.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/WatchZapsAndUpdateFeed.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserZaps import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.UserProfileZapsFeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapTabHeader.kt index c2d73597a..9d9db8147 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapTabHeader.kt @@ -24,7 +24,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserZapAmount import com.vitorpamplona.amethyst.ui.note.showAmountInteger import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsFeedFilter.kt index 06ea5284c..1fe7fd244 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsFeedFilter.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.ZapReqResponse import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsFeedViewModel.kt index 40ef049d6..e472d934a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsFeedViewModel.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.LnZapFeedViewModel class UserProfileZapsFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRScreen.kt index 89da19efa..52081b6f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRScreen.kt @@ -49,7 +49,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.DisplayNIP05 import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt index bb6e918c1..0ddb9c1b4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt @@ -33,7 +33,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt index a5a5560ff..18f738ec2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStat import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/report/ReportNoteDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/report/ReportNoteDialog.kt index f671ea691..67616f4b8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/report/ReportNoteDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/report/ReportNoteDialog.kt @@ -59,7 +59,7 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.components.TextSpinner import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenAccountsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenAccountsFeedFilter.kt index 38d5541ae..45b4e650c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenAccountsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenAccountsFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.dal +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CancellationException diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedFilter.kt index 542fa1f92..adb19f890 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.dal +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.dal.FeedFilter class SpammerAccountsFeedFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index e3f5068b9..569a73e11 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -73,9 +73,9 @@ import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeCommunityApprovalNeedStatus import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid import com.vitorpamplona.amethyst.ui.components.GenericLoadable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/LevelFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/LevelFeedViewModel.kt index 3d006fffb..cd7f11636 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/LevelFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/LevelFeedViewModel.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.ThreadLevelCalculator import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.feeds.FeedState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedFilter.kt index f1ae4c13a..4691b802f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedFilter.kt @@ -21,9 +21,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LevelSignature -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ThreadAssembler import com.vitorpamplona.amethyst.model.ThreadLevelCalculator import com.vitorpamplona.amethyst.ui.dal.FeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterEventsInThreadForRoot.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterEventsInThreadForRoot.kt index c0cabe9ff..04f46b7a8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterEventsInThreadForRoot.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterEventsInThreadForRoot.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt index 8e0ad15d1..4b865c4e5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt @@ -20,7 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies -import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.ThreadAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingAddressables import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingEvents diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt index 16a001a81..17cb3aa64 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt @@ -51,7 +51,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt index 6646dac18..7f1a0cf26 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt @@ -20,11 +20,12 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.dal +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.filterIntoSet import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt index ef53e0037..0c6e0413d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt index 216691ae0..e8940388f 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache.getOrCreateAddressableNoteInternal -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.actions.Dao import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.quartz.nip01Core.core.Address diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/zaps/UserProfileZapsFeedFilterTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/zaps/UserProfileZapsFeedFilterTest.kt index 6e2f58188..4d16bb3c0 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/zaps/UserProfileZapsFeedFilterTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/zaps/UserProfileZapsFeedFilterTest.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.zaps -import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.UserProfileZapsFeedFilter import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent diff --git a/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/threading/Threading.android.kt b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/threading/Threading.android.kt new file mode 100644 index 000000000..9c1e2d7aa --- /dev/null +++ b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/threading/Threading.android.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.threading + +import android.os.Looper + +/** + * Android implementation of checkNotInMainThread. + * Uses Looper to detect if running on main thread. + */ +actual fun checkNotInMainThread() { + // BuildConfig check removed - commons doesn't have BuildConfig + // Enable this check in debug builds at the app level if needed + if (isMainThread()) { + throw OnMainThreadException("It should not be in the MainThread") + } +} + +private fun isMainThread() = Looper.myLooper() == Looper.getMainLooper() diff --git a/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/util/PlatformNumberFormatter.android.kt b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/util/PlatformNumberFormatter.android.kt new file mode 100644 index 000000000..e97a28225 --- /dev/null +++ b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/util/PlatformNumberFormatter.android.kt @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.util + +import java.text.NumberFormat + +actual class PlatformNumberFormatter { + private val formatter = NumberFormat.getInstance() + + actual fun format(value: Long): String = formatter.format(value) +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/IAccount.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/IAccount.kt new file mode 100644 index 000000000..bbd7805d4 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/IAccount.kt @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.model + +import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent +import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent +import com.vitorpamplona.quartz.nip47WalletConnect.Request +import com.vitorpamplona.quartz.nip47WalletConnect.Response +import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.utils.DualCase + +/** + * Interface for NIP-47 wallet connect signer state. + * Used by Note.kt for checking NWC payment status. + */ +interface INwcSignerState { + suspend fun decryptResponse(event: LnZapPaymentResponseEvent): Response? + + suspend fun decryptRequest(event: LnZapPaymentRequestEvent): Request? + + fun isNIP47Author(pubKey: String?): Boolean +} + +/** + * Interface for private zap decryption cache. + * Used by Note.kt for checking private zap status. + */ +interface IPrivateZapsDecryptionCache { + fun cachedPrivateZap(event: LnZapRequestEvent): LnZapPrivateEvent? + + suspend fun decryptPrivateZap(event: LnZapRequestEvent): LnZapPrivateEvent? +} + +/** + * Hidden content settings for filtering notes. + * Used by Note.isHiddenFor() to check if content should be hidden. + */ +data class LiveHiddenUsers( + val showSensitiveContent: Boolean?, + val hiddenWordsCase: List, + val hiddenUsersHashCodes: Set, + val spammersHashCodes: Set, +) + +/** + * Interface for account operations needed by Note.kt. + * Abstracts Android-specific Account class for use in commons. + */ +interface IAccount { + /** NIP-47 wallet connect state for payment verification */ + val nip47SignerState: INwcSignerState + + /** Private zaps decryption cache */ + val privateZapsDecryptionCache: IPrivateZapsDecryptionCache + + /** Current user's profile */ + fun userProfile(): User + + /** Whether account has write permissions */ + fun isWriteable(): Boolean + + /** Current user's public key */ + val pubKey: String + + /** Content filter settings */ + val showSensitiveContent: Boolean? + val hiddenWordsCase: List + val hiddenUsersHashCodes: Set + val spammersHashCodes: Set +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt index dd32a2985..49af94c47 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt @@ -18,16 +18,15 @@ * 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.model +package com.vitorpamplona.amethyst.commons.model import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState -import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState -import com.vitorpamplona.amethyst.service.checkNotInMainThread -import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji -import com.vitorpamplona.amethyst.service.replace -import com.vitorpamplona.amethyst.ui.note.toShortDisplay +import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider +import com.vitorpamplona.amethyst.commons.threading.checkNotInMainThread +import com.vitorpamplona.amethyst.commons.util.firstFullCharOrEmoji +import com.vitorpamplona.amethyst.commons.util.replace +import com.vitorpamplona.amethyst.commons.util.toShortDisplay import com.vitorpamplona.quartz.experimental.bounties.addedRewardValue import com.vitorpamplona.quartz.experimental.bounties.hasAdditionalReward import com.vitorpamplona.quartz.lightning.LnInvoiceUtil @@ -110,6 +109,7 @@ class AddressableNote( @Stable open class Note( val idHex: String, + private val cacheProvider: ICacheProvider? = null, ) : NotesGatherer { // These fields are only available after the Text Note event is received. // They are immutable after that. @@ -200,11 +200,11 @@ open class Note( val communityPostRelays = when (noteEvent) { is CommunityDefinitionEvent -> noteEvent.relayUrls().ifEmpty { null }?.toSet() - is IsInPublicChatChannel -> LocalCache.getAnyChannel(this)?.relays() + is IsInPublicChatChannel -> cacheProvider?.getAnyChannel(this)?.relays() else -> null } - if (!communityPostRelays.isNullOrEmpty()) return communityPostRelays.firstOrNull() + if (!communityPostRelays.isNullOrEmpty()) return (communityPostRelays as? Collection)?.firstOrNull() val currentOutbox = author?.outboxRelays()?.toSet() @@ -469,7 +469,7 @@ open class Note( private suspend fun isPaidByCalculation( zapPayments: List>, afterTimeInSeconds: Long, - account: Account, + account: IAccount, ): Boolean { if (zapPayments.isEmpty()) { return false @@ -497,7 +497,7 @@ open class Note( option: Int?, user: User, afterTimeInSeconds: Long, - account: Account, + account: IAccount, zapEvents: Map, ): Boolean { if (zapEvents.isEmpty()) { @@ -555,7 +555,7 @@ open class Note( suspend fun isZappedBy( user: User, afterTimeInSeconds: Long, - account: Account, + account: IAccount, ): Boolean { val first = isZappedByCalculation(null, user, afterTimeInSeconds, account, zaps) if (first) return true @@ -569,7 +569,7 @@ open class Note( option: Int?, user: User, afterTimeInSeconds: Long, - account: Account, + account: IAccount, ): Boolean = isZappedByCalculation(option, user, afterTimeInSeconds, account, zaps) fun getReactionBy(user: User): String? = @@ -615,7 +615,7 @@ open class Note( startAmount: BigDecimal, paidInvoiceSet: LinkedHashSet, zapPayments: List>, - signerState: NwcSignerState, + signerState: INwcSignerState, ): BigDecimal { if (zapPayments.isEmpty()) { return startAmount @@ -643,7 +643,7 @@ open class Note( private suspend fun processZapAmountFromResponse( paymentRequest: Note, paymentResponse: Note?, - signerState: NwcSignerState, + signerState: INwcSignerState, ): InvoiceAmount? { val nwcRequest = paymentRequest.event as? LnZapPaymentRequestEvent val nwcResponse = paymentResponse?.event as? LnZapPaymentResponseEvent @@ -667,7 +667,7 @@ open class Note( private suspend fun processZapAmountFromResponse( nwcRequest: LnZapPaymentRequestEvent, nwcResponse: LnZapPaymentResponseEvent, - signerState: NwcSignerState, + signerState: INwcSignerState, ): InvoiceAmount? { // if we can decrypt the reply return signerState.decryptResponse(nwcResponse)?.let { noteEvent -> @@ -701,7 +701,7 @@ open class Note( } } - suspend fun zappedAmountWithNWCPayments(signerState: NwcSignerState): BigDecimal { + suspend fun zappedAmountWithNWCPayments(signerState: INwcSignerState): BigDecimal { if (zapPayments.isEmpty()) { return zapsAmount } @@ -829,7 +829,7 @@ open class Note( zapsAmount = BigDecimal.ZERO } - fun isHiddenFor(accountChoices: HiddenUsersState.LiveHiddenUsers): Boolean { + fun isHiddenFor(accountChoices: LiveHiddenUsers): Boolean { val thisEvent = event ?: return false val hash = thisEvent.pubKey.hashCode() diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ICacheProvider.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ICacheProvider.kt new file mode 100644 index 000000000..ab9fb6a6d --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ICacheProvider.kt @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.model.cache + +import com.vitorpamplona.quartz.nip01Core.core.HexKey + +/** + * Cache provider interface for accessing cached Notes, Users, and Channels. + * + * This abstraction allows Note and User models to access the cache without + * direct coupling to LocalCache singleton. Platform-specific implementations + * (Android LocalCache, Desktop DesktopLocalCache) implement this interface. + * + * Benefits: + * - Dependency injection instead of singleton coupling + * - Testable (can mock for unit tests) + * - Platform-agnostic model layer + */ +interface ICacheProvider { + /** + * Gets a channel by Note reference. + * Used for resolving relay hints for channel messages. + * + * @param note The note to look up channel for + * @return The channel if found, null otherwise + */ + fun getAnyChannel(note: Any?): IChannel? + + /** + * Gets a User by public key hex. + * Used for updating follower counts and user relationships. + * + * @param pubkey The user's public key in hex format + * @return The User if exists in cache, null otherwise + */ + fun getUserIfExists(pubkey: HexKey): Any? + + /** + * Counts users matching a predicate. + * Used for calculating follower counts. + * + * @param predicate Filter function for counting users + * @return Count of users matching the predicate + */ + fun countUsers(predicate: (String, Any) -> Boolean): Int +} + +/** + * Minimal channel interface for relay resolution. + * Full channel implementations (PublicChatChannel, LiveActivitiesChannel) + * implement this interface. + */ +interface IChannel { + /** + * Gets the relay URLs for this channel. + * @return List of relay URLs or null if none configured + */ + fun relays(): List? +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/UserReportCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip56Reports/UserReportCache.kt similarity index 88% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/UserReportCache.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip56Reports/UserReportCache.kt index c6064dfb8..f078c4c83 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/UserReportCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip56Reports/UserReportCache.kt @@ -18,12 +18,11 @@ * 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.model.nip56Reports +package com.vitorpamplona.amethyst.commons.model.nip56Reports -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.UserDependencies -import com.vitorpamplona.amethyst.service.relays.EOSERelayList +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.UserDependencies import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip56Reports.ReportType @@ -34,12 +33,6 @@ import kotlinx.coroutines.flow.update class UserReportCache : UserDependencies { val receivedReportsByAuthor = MutableStateFlow(mapOf>()) - /** - * This assembler saves the EOSE per user key. That EOSE includes their metadata, etc - * and reports, but only from trusted accounts (follows of all logged in users). - */ - var latestEOSEs: EOSERelayList = EOSERelayList() - fun addReport(note: Note) { val author = note.author ?: return diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/UserCardsCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/trustedAssertions/UserCardsCache.kt similarity index 84% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/UserCardsCache.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/trustedAssertions/UserCardsCache.kt index abb6edf9a..0a1d46074 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/UserCardsCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/trustedAssertions/UserCardsCache.kt @@ -18,12 +18,12 @@ * 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.model.trustedAssertions +package com.vitorpamplona.amethyst.commons.model.trustedAssertions -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.UserDependencies -import com.vitorpamplona.amethyst.service.relays.EOSERelayList +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.UserDependencies +import com.vitorpamplona.amethyst.commons.util.PlatformNumberFormatter import com.vitorpamplona.quartz.experimental.relationshipStatus.ContactCardEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -32,17 +32,10 @@ import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update -import java.text.NumberFormat class UserCardsCache : UserDependencies { val receivedCards = MutableStateFlow(mapOf()) - /** - * This assembler saves the EOSE per user key. That EOSE includes their metadata, etc - * and reports, but only from trusted accounts (follows of all logged in users). - */ - var latestEOSEs: EOSERelayList = EOSERelayList() - fun addCard(note: AddressableNote) { val author = note.author ?: return @@ -109,7 +102,7 @@ class UserCardsCache : UserDependencies { (it?.note?.event as? ContactCardEvent)?.rank() }.flowOn(Dispatchers.IO) - val formatter = NumberFormat.getInstance() + private val formatter = PlatformNumberFormatter() fun followerCountStrFlow(trustProviderList: TrustProviderListState) = combineTransform(receivedCards, trustProviderList.liveUserFollowerCount) { cards, provider -> @@ -137,9 +130,21 @@ class UserCardsCache : UserDependencies { val value = (it?.note?.event as? ContactCardEvent)?.followerCount() if (value != null && value > 0) { - formatter.format(value) + formatter.format(value.toLong()) } else { "--" } }.flowOn(Dispatchers.IO) } + +// Placeholder for TrustProviderListState - will be properly extracted later +// For now, this allows UserCardsCache to compile +interface TrustProviderListState { + val liveUserRankProvider: kotlinx.coroutines.flow.StateFlow + val liveUserFollowerCount: kotlinx.coroutines.flow.StateFlow +} + +// Placeholder for ServiceProviderTag +interface ServiceProviderTag { + val pubkey: String +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/threading/Threading.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/threading/Threading.kt new file mode 100644 index 000000000..8892ff2a6 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/threading/Threading.kt @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.threading + +/** + * Checks that the current code is not running on the main thread. + * Throws an exception in debug builds if called from the main thread. + * + * Platform-specific: Android uses Looper, Desktop may use different mechanism or no-op. + */ +expect fun checkNotInMainThread() + +/** + * Exception thrown when code expected to run on a background thread is executed on the main thread. + */ +class OnMainThreadException( + str: String, +) : RuntimeException(str) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/EmojiUtils.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/EmojiUtils.kt new file mode 100644 index 000000000..8b677f5ed --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/EmojiUtils.kt @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.util + +import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder +import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists + +fun String.isUTF16Char(pos: Int): Boolean = Character.charCount(this.codePointAt(pos)) == 2 + +fun String.firstFullCharOld(): String { + return when (this.length) { + 0, + 1, + -> return this + 2, + 3, + -> return if (isUTF16Char(0)) this.take(2) else this.take(1) + else -> { + val first = isUTF16Char(0) + val second = isUTF16Char(2) + if (first && second) { + this.take(4) + } else if (first) { + this.take(2) + } else { + this.take(1) + } + } + } +} + +fun String.firstFullChar(): String { + var isInJoin = false + var hasHadSecondChance = false + var start = 0 + var previousCharLength = 0 + var next: Int + var codePoint: Int + + var i = 0 + + while (i < this.length) { + codePoint = codePointAt(i) + + // Skips if it starts with the join char 0x200D + if (codePoint == 0x200D && previousCharLength == 0) { + next = offsetByCodePoints(i, 1) + start = next + } else { + // If join, searches for the next char + if (codePoint == 0xFE0F) { + } else if (codePoint == 0x200D) { + isInJoin = true + } else { + // stops when two chars are not joined together + if (previousCharLength > 0 && !isInJoin) { + if (Character.charCount(codePoint) == 1 || hasHadSecondChance) { + break + } else { + hasHadSecondChance = true + } + } else { + hasHadSecondChance = false + } + + isInJoin = false + } + + // next char to evaluate + next = offsetByCodePoints(i, 1) + previousCharLength += (next - i) + } + + i = next + } + + // if ends in join, then seachers backwards until a char is found. + if (isInJoin) { + i = previousCharLength - 1 + while (i > 0) { + if (this[i].code == 0x200D) { + previousCharLength -= 1 + } else { + break + } + + i -= 1 + } + } + + return substring(start, start + previousCharLength) +} + +fun String.firstFullCharOrEmoji(tags: ImmutableListOfLists): String { + if (length <= 2) { + return firstFullChar() + } + + if (this[0] == ':') { + // makes sure an emoji exists + val emojiParts = this.split(":", limit = 3) + if (emojiParts.size >= 2) { + val emojiName = emojiParts[1] + val emojiUrl = tags.lists.firstOrNull { it.size > 1 && it[1] == emojiName }?.getOrNull(2) + if (emojiUrl != null) { + return ":$emojiName:$emojiUrl" + } + } + } + + if (EmojiCoder.isCoded(this)) { + return EmojiCoder.cropToFirstMessage(this) + } + + return firstFullChar() +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/IterableUtils.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/IterableUtils.kt new file mode 100644 index 000000000..eef13c149 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/IterableUtils.kt @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.util + +fun Iterable.replace( + old: T, + new: T, +): List = map { if (it == old) new else it } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/PlatformNumberFormatter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/PlatformNumberFormatter.kt new file mode 100644 index 000000000..3691fc1f8 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/PlatformNumberFormatter.kt @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.util + +/** + * Platform-agnostic number formatter. + * Uses NumberFormat on JVM platforms, custom formatting elsewhere. + */ +expect class PlatformNumberFormatter() { + fun format(value: Long): String +} diff --git a/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/threading/Threading.jvm.kt b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/threading/Threading.jvm.kt new file mode 100644 index 000000000..8140c47be --- /dev/null +++ b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/threading/Threading.jvm.kt @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.threading + +/** + * Desktop JVM implementation of checkNotInMainThread. + * Currently a no-op as Desktop Compose doesn't have the same main thread restrictions as Android. + * Could be enhanced to check Swing EDT if needed. + */ +actual fun checkNotInMainThread() { + // No-op for Desktop - different threading model + // Could check for Swing EDT with: javax.swing.SwingUtilities.isEventDispatchThread() + // but Compose Desktop doesn't have the same restrictions as Android +} diff --git a/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/util/PlatformNumberFormatter.jvm.kt b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/util/PlatformNumberFormatter.jvm.kt new file mode 100644 index 000000000..e97a28225 --- /dev/null +++ b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/util/PlatformNumberFormatter.jvm.kt @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.util + +import java.text.NumberFormat + +actual class PlatformNumberFormatter { + private val formatter = NumberFormat.getInstance() + + actual fun format(value: Long): String = formatter.format(value) +} From 3750c7e26e857634d365dcf6e7eae72a6d4e2afd Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Thu, 1 Jan 2026 07:32:44 +0200 Subject: [PATCH 022/137] fixes --- .../LabeledBookmarkListsState.kt | 8 +- .../nip51Lists/peopleList/FollowListsState.kt | 8 +- .../nip51Lists/peopleList/PeopleListsState.kt | 8 +- .../user/watchers/UserCardsSubAssembler.kt | 2 +- .../user/watchers/UserReportsSubAssembler.kt | 2 +- .../amethyst/commons/model/User.kt | 355 ++++++++++++++++++ 6 files changed, 369 insertions(+), 14 deletions(-) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt index a30a4e764..7859c198a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt @@ -24,11 +24,11 @@ import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.anyNotNullEvent -import com.vitorpamplona.amethyst.model.eventIdSet -import com.vitorpamplona.amethyst.model.events +import com.vitorpamplona.amethyst.commons.model.anyNotNullEvent +import com.vitorpamplona.amethyst.commons.model.eventIdSet +import com.vitorpamplona.amethyst.commons.model.events import com.vitorpamplona.amethyst.model.filter -import com.vitorpamplona.amethyst.model.updateFlow +import com.vitorpamplona.amethyst.commons.model.updateFlow import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.update import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt index 1c2078fa9..2bc5b7e87 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt @@ -25,11 +25,11 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.anyNotNullEvent -import com.vitorpamplona.amethyst.model.eventIdSet -import com.vitorpamplona.amethyst.model.events +import com.vitorpamplona.amethyst.commons.model.anyNotNullEvent +import com.vitorpamplona.amethyst.commons.model.eventIdSet +import com.vitorpamplona.amethyst.commons.model.events import com.vitorpamplona.amethyst.model.filter -import com.vitorpamplona.amethyst.model.updateFlow +import com.vitorpamplona.amethyst.commons.model.updateFlow import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.update diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt index 2ec777a93..b145b602e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt @@ -25,11 +25,11 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.anyNotNullEvent -import com.vitorpamplona.amethyst.model.eventIdSet -import com.vitorpamplona.amethyst.model.events +import com.vitorpamplona.amethyst.commons.model.anyNotNullEvent +import com.vitorpamplona.amethyst.commons.model.eventIdSet +import com.vitorpamplona.amethyst.commons.model.events import com.vitorpamplona.amethyst.model.filter -import com.vitorpamplona.amethyst.model.updateFlow +import com.vitorpamplona.amethyst.commons.model.updateFlow import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.update diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt index 991815b9d..9222d1bc8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.toHexSet +import com.vitorpamplona.amethyst.commons.model.toHexSet import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState import com.vitorpamplona.amethyst.service.relays.MutableTime diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt index 0c5146654..e0c0da81f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.toHexSet +import com.vitorpamplona.amethyst.commons.model.toHexSet import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState import com.vitorpamplona.amethyst.service.relays.MutableTime diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt new file mode 100644 index 000000000..08e1af1fd --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt @@ -0,0 +1,355 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.model + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider +import com.vitorpamplona.amethyst.commons.model.nip56Reports.UserReportCache +import com.vitorpamplona.amethyst.commons.model.trustedAssertions.UserCardsCache +import com.vitorpamplona.amethyst.commons.util.toShortDisplay +import com.vitorpamplona.quartz.lightning.Lud06 +import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser +import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile +import com.vitorpamplona.quartz.nip19Bech32.toNpub +import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent +import com.vitorpamplona.quartz.utils.DualCase +import com.vitorpamplona.quartz.utils.Hex +import com.vitorpamplona.quartz.utils.containsAny +import kotlinx.coroutines.flow.MutableStateFlow +import java.math.BigDecimal + +interface UserDependencies + +@Stable +class User( + val pubkeyHex: String, + val nip65RelayListNote: Note, + val dmRelayListNote: Note, + private val cacheProvider: ICacheProvider? = null, +) { + private var reports: UserReportCache? = null + private var cards: UserCardsCache? = null + + // private var deps = ScatterMap, UserDependencies>() + + var info: UserMetadata? = null + + var latestMetadata: MetadataEvent? = null + var latestMetadataRelay: NormalizedRelayUrl? = null + var latestContactList: ContactListEvent? = null + + var zaps = mapOf() + private set + + var relaysBeingUsed = mapOf() + private set + + var flowSet: UserFlowSet? = null + + fun pubkey() = Hex.decode(pubkeyHex) + + fun pubkeyNpub() = pubkey().toNpub() + + fun pubkeyDisplayHex() = pubkeyNpub().toShortDisplay() + + fun dmInboxRelayList() = dmRelayListNote.event as? ChatMessageRelayListEvent + + fun authorRelayList() = nip65RelayListNote.event as? AdvertisedRelayListEvent + + fun toNProfile() = NProfile.create(pubkeyHex, relayHints()) + + fun outboxRelays() = authorRelayList()?.writeRelaysNorm() + + fun relayHints() = authorRelayList()?.writeRelaysNorm()?.take(3) ?: listOfNotNull(latestMetadataRelay) + + fun inboxRelays() = authorRelayList()?.readRelaysNorm() + + fun dmInboxRelays() = dmInboxRelayList()?.relays()?.ifEmpty { null } ?: inboxRelays() + + fun bestRelayHint() = authorRelayList()?.writeRelaysNorm()?.firstOrNull() ?: latestMetadataRelay + + fun toPTag() = PTag(pubkeyHex, bestRelayHint()) + + fun toNostrUri() = "nostr:${toNProfile()}" + + fun toBestShortFirstName(): String { + val fullName = toBestDisplayName() + + val names = fullName.split(' ') + + val firstName = + if (names[0].length <= 3) { + // too short. Remove Dr. + "${names[0]} ${names.getOrNull(1) ?: ""}" + } else { + names[0] + } + + return firstName + } + + fun toBestDisplayName(): String = info?.bestName() ?: pubkeyDisplayHex() + + fun nip05(): String? = info?.nip05 + + fun profilePicture(): String? = info?.picture + + fun updateContactList(event: ContactListEvent) { + if (event.id == latestContactList?.id) return + + val oldContactListEvent = latestContactList + latestContactList = event + + // Update following of the current user + flowSet?.follows?.invalidateData() + + // Update Followers of the past user list + // Update Followers of the new contact list + (oldContactListEvent)?.unverifiedFollowKeySet()?.forEach { + (cacheProvider?.getUserIfExists(it) as? User) + ?.flowSet + ?.followers + ?.invalidateData() + } + (latestContactList)?.unverifiedFollowKeySet()?.forEach { + (cacheProvider?.getUserIfExists(it) as? User) + ?.flowSet + ?.followers + ?.invalidateData() + } + } + + fun addZap( + zapRequest: Note, + zap: Note?, + ) { + if (zaps[zapRequest] == null) { + zaps = zaps + Pair(zapRequest, zap) + flowSet?.zaps?.invalidateData() + } + } + + fun removeZap(zapRequestOrZapEvent: Note) { + if (zaps.containsKey(zapRequestOrZapEvent)) { + zaps = zaps.minus(zapRequestOrZapEvent) + flowSet?.zaps?.invalidateData() + } else if (zaps.containsValue(zapRequestOrZapEvent)) { + zaps = zaps.filter { it.value != zapRequestOrZapEvent } + flowSet?.zaps?.invalidateData() + } + } + + fun zappedAmount(): BigDecimal { + var amount = BigDecimal.ZERO + zaps.forEach { + val itemValue = (it.value?.event as? LnZapEvent)?.amount + if (itemValue != null) { + amount += itemValue + } + } + + return amount + } + + fun addRelayBeingUsed( + relay: NormalizedRelayUrl, + eventTime: Long, + ) { + val here = relaysBeingUsed[relay] + if (here == null) { + relaysBeingUsed = relaysBeingUsed + Pair(relay, RelayInfo(relay, eventTime, 1)) + } else { + if (eventTime > here.lastEvent) { + here.lastEvent = eventTime + } + here.counter++ + } + + flowSet?.usedRelays?.invalidateData() + } + + fun updateUserInfo( + newUserInfo: UserMetadata, + latestMetadata: MetadataEvent, + ) { + info = newUserInfo + info?.tags = latestMetadata.tags.toImmutableListOfLists() + info?.cleanBlankNames() + + if (newUserInfo.lud16.isNullOrBlank()) { + info?.lud06?.let { + if (it.lowercase().startsWith("lnurl")) { + info?.lud16 = Lud06().toLud16(it) + } + } + } + + flowSet?.metadata?.invalidateData() + } + + fun isFollowing(user: User): Boolean = latestContactList?.isTaggedUser(user.pubkeyHex) ?: false + + fun transientFollowCount(): Int? = latestContactList?.unverifiedFollowKeySet()?.size + + fun transientFollowerCount(): Int = + cacheProvider?.countUsers { _, it -> + (it as? User)?.latestContactList?.isTaggedUser(pubkeyHex) ?: false + } ?: 0 + + fun reportsOrNull(): UserReportCache? = reports + + fun reports(): UserReportCache = reports ?: UserReportCache().also { reports = it } + + // fun reportsOrNull(): UserReports? = deps[UserReports::class] as? UserReports + + // fun reports(): UserReports = deps.getOrPut(UserReports::class) { UserReports() } as UserReports + + fun cardsOrNull(): UserCardsCache? = cards + + fun cards(): UserCardsCache = cards ?: UserCardsCache().also { cards = it } + + fun containsAny(hiddenWordsCase: List): Boolean { + if (hiddenWordsCase.isEmpty()) return false + + if (toBestDisplayName().containsAny(hiddenWordsCase)) { + return true + } + + if (profilePicture()?.containsAny(hiddenWordsCase) == true) { + return true + } + + if (info?.banner?.containsAny(hiddenWordsCase) == true) { + return true + } + + if (info?.about?.containsAny(hiddenWordsCase) == true) { + return true + } + + if (info?.lud06?.containsAny(hiddenWordsCase) == true) { + return true + } + + if (info?.lud16?.containsAny(hiddenWordsCase) == true) { + return true + } + + if (info?.nip05?.containsAny(hiddenWordsCase) == true) { + return true + } + + return false + } + + fun anyNameStartsWith(username: String): Boolean = info?.anyNameStartsWith(username) ?: false + + @Synchronized + fun createOrDestroyFlowSync(create: Boolean) { + if (create) { + if (flowSet == null) { + flowSet = UserFlowSet(this) + } + } else { + if (flowSet != null && flowSet?.isInUse() == false) { + flowSet = null + } + } + } + + fun flow(): UserFlowSet { + if (flowSet == null) { + createOrDestroyFlowSync(true) + } + return flowSet!! + } + + fun clearFlow() { + if (flowSet != null && flowSet?.isInUse() == false) { + createOrDestroyFlowSync(false) + } + } +} + +@Stable +class UserFlowSet( + u: User, +) { + // Observers line up here. + val metadata = UserBundledRefresherFlow(u) + val follows = UserBundledRefresherFlow(u) + val followers = UserBundledRefresherFlow(u) + val usedRelays = UserBundledRefresherFlow(u) + val zaps = UserBundledRefresherFlow(u) + val statuses = UserBundledRefresherFlow(u) + + fun isInUse(): Boolean = + metadata.hasObservers() || + follows.hasObservers() || + followers.hasObservers() || + usedRelays.hasObservers() || + zaps.hasObservers() || + statuses.hasObservers() +} + +@Immutable +data class RelayInfo( + val url: NormalizedRelayUrl, + var lastEvent: Long, + var counter: Long, +) + +@Stable +class UserBundledRefresherFlow( + val user: User, +) { + val stateFlow = MutableStateFlow(UserState(user)) + + fun invalidateData() { + stateFlow.tryEmit(UserState(user)) + } + + fun hasObservers() = stateFlow.subscriptionCount.value > 0 +} + +@Immutable +class UserState( + val user: User, +) + +fun Set.toHexSet() = mapTo(LinkedHashSet(size)) { it.pubkeyHex } + +fun Set.toSortedHexes() = map { it.pubkeyHex }.sorted() + +fun List.toHexes() = map { it.pubkeyHex } + +fun List.toHexSet() = mapTo(LinkedHashSet(size)) { it.pubkeyHex } + +fun List.toSortedHexes() = map { it.pubkeyHex }.sorted() From 4bc7e8644838331a44bab2d528f8572f4571c12c Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Thu, 1 Jan 2026 07:55:31 +0200 Subject: [PATCH 023/137] fixes imports --- .../vitorpamplona/amethyst/model/Account.kt | 18 +++++--- .../vitorpamplona/amethyst/model/Channel.kt | 2 +- .../nip02FollowLists/Kind3FollowListState.kt | 2 - .../nip47WalletConnect/NwcSignerState.kt | 9 ++-- .../model/nip51Lists/HiddenUsersState.kt | 43 ++++++++++--------- .../model/nip51Lists/peopleList/PeopleList.kt | 2 - .../TrustProviderListState.kt | 7 +-- .../amethyst/ui/dal/FilterByListParams.kt | 6 +-- .../ui/screen/loggedIn/AccountViewModel.kt | 3 +- .../amethyst/commons/model/IAccount.kt | 21 ++++----- .../TrustProviderListState.kt | 33 ++++++++++++++ .../model/trustedAssertions/UserCardsCache.kt | 12 ------ .../nip57Zaps/IPrivateZapsDecryptionCache.kt | 31 +++++++++++++ .../quartz/nip57Zaps/PrivateZapCache.kt | 6 +-- 14 files changed, 125 insertions(+), 70 deletions(-) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/trustedAssertions/TrustProviderListState.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/IPrivateZapsDecryptionCache.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 50518c135..689777b3e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -108,6 +108,7 @@ import com.vitorpamplona.quartz.experimental.bounties.BountyAddValueEvent import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.utils.DualCase import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryReadingStateEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent import com.vitorpamplona.quartz.experimental.interactiveStories.image @@ -237,14 +238,21 @@ class Account( val cache: LocalCache, val client: INostrClient, val scope: CoroutineScope, -) { +) : IAccount { private var userProfileCache: User? = null - fun userProfile(): User = userProfileCache ?: cache.getOrCreateUser(signer.pubKey).also { userProfileCache = it } + override fun userProfile(): User = userProfileCache ?: cache.getOrCreateUser(signer.pubKey).also { userProfileCache = it } + + // IAccount interface properties + override val pubKey: String get() = signer.pubKey + override val showSensitiveContent: Boolean? get() = hiddenUsers.flow.value.showSensitiveContent + override val hiddenWordsCase: List get() = hiddenUsers.flow.value.hiddenWordsCase + override val hiddenUsersHashCodes: Set get() = hiddenUsers.flow.value.hiddenUsersHashCodes + override val spammersHashCodes: Set get() = hiddenUsers.flow.value.spammersHashCodes val userMetadata = UserMetadataState(signer, cache, scope, settings) - val nip47SignerState = NwcSignerState(signer, nwcFilterAssembler, cache, scope, settings) + override val nip47SignerState = NwcSignerState(signer, nwcFilterAssembler, cache, scope, settings) val nip65RelayList = Nip65RelayListState(signer, cache, scope, settings) val localRelayList = LocalRelayListState(signer, cache, scope, settings) @@ -340,7 +348,7 @@ class Account( val allFollows = MergedFollowListsState(kind3FollowList, peopleLists, followLists, hashtagList, geohashList, communityList, scope) val privateDMDecryptionCache = PrivateDMCache(signer) - val privateZapsDecryptionCache = PrivateZapCache(signer) + override val privateZapsDecryptionCache = PrivateZapCache(signer) val draftsDecryptionCache = DraftEventCache(signer) val chatroomList = cache.getOrCreateChatroomList(signer.pubKey) @@ -423,7 +431,7 @@ class Account( val liveNotificationFollowListsPerRelay = OutboxLoaderState(liveNotificationFollowLists, cache, scope).flow - fun isWriteable(): Boolean = settings.isWriteable() + override fun isWriteable(): Boolean = settings.isWriteable() suspend fun updateWarnReports(warnReports: Boolean): Boolean { if (settings.updateWarnReports(warnReports)) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt index fce3e9256..1e3444774 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt @@ -22,9 +22,9 @@ package com.vitorpamplona.amethyst.model import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NotesGatherer import com.vitorpamplona.amethyst.commons.model.User - import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.amethyst.ui.dal.ListChange diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/Kind3FollowListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/Kind3FollowListState.kt index f377abc4c..2df798ef6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/Kind3FollowListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/Kind3FollowListState.kt @@ -24,9 +24,7 @@ import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User - import androidx.compose.runtime.Immutable -import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.UserState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt index 98e7ed9e1..18d19a409 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.model.nip47WalletConnect +import com.vitorpamplona.amethyst.commons.model.INwcSignerState import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache @@ -66,7 +67,7 @@ class NwcSignerState( val cache: LocalCache, val scope: CoroutineScope, val settings: AccountSettings, -) { +) : INwcSignerState { /** * Derives a NIP-47 signer from the zap payment request in settings. * If there's no valid configuration, it defaults to the main signer. @@ -112,7 +113,7 @@ class NwcSignerState( fun hasWalletConnectSetup(): Boolean = settings.zapPaymentRequest.value != null - fun isNIP47Author(pubkeyHex: String?): Boolean = nip47Signer.value.pubKey == pubkeyHex + override fun isNIP47Author(pubkeyHex: String?): Boolean = nip47Signer.value.pubKey == pubkeyHex /** * Decrypts a NIP-47 payment request using the current signer. @@ -120,7 +121,7 @@ class NwcSignerState( * @param nwcRequest the NIP-47 payment request event to decrypt * @return the decrypted request or null if not set up or decryption fails */ - suspend fun decryptRequest(nwcRequest: LnZapPaymentRequestEvent): Request? { + override suspend fun decryptRequest(nwcRequest: LnZapPaymentRequestEvent): Request? { if (!hasWalletConnectSetup()) return null return zapPaymentRequestDecryptionCache.value.decryptRequest(nwcRequest) } @@ -131,7 +132,7 @@ class NwcSignerState( * @param nwsResponse the NIP-47 payment response event to decrypt * @return the decrypted response or null if not set up or decryption fails */ - suspend fun decryptResponse(nwsResponse: LnZapPaymentResponseEvent): Response? { + override suspend fun decryptResponse(nwsResponse: LnZapPaymentResponseEvent): Response? { if (!hasWalletConnectSetup()) return null return zapPaymentResponseDecryptionCache.value.decryptResponse(nwsResponse) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt index 8abcb2196..841576a3d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.model.nip51Lists import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User @@ -52,33 +53,25 @@ class HiddenUsersState( ) { var transientHiddenUsers: MutableStateFlow> = MutableStateFlow(setOf()) - @Immutable - class LiveHiddenUsers( - val hiddenUsers: Set, - val spammers: Set, - val hiddenWords: Set, - val showSensitiveContent: Boolean?, - ) { - // speeds up isHidden calculations - val hiddenUsersHashCodes = hiddenUsers.mapTo(HashSet()) { it.hashCode() } - val spammersHashCodes = spammers.mapTo(HashSet()) { it.hashCode() } - val hiddenWordsCase = hiddenWords.map { DualCase(it.lowercase(), it.uppercase()) } - - fun isUserHidden(userHex: HexKey) = hiddenUsers.contains(userHex) || spammers.contains(userHex) - } - suspend fun assembleLiveHiddenUsers( blockList: List, muteList: List, transientHiddenUsers: Set, showSensitiveContent: Boolean?, - ): LiveHiddenUsers = - LiveHiddenUsers( - hiddenUsers = blockList.mapNotNullTo(mutableSetOf()) { if (it is UserTag) it.pubKey else null } + muteList.mapNotNull { if (it is UserTag) it.pubKey else null }, - hiddenWords = blockList.mapNotNullTo(mutableSetOf()) { if (it is WordTag) it.word else null } + muteList.mapNotNull { if (it is WordTag) it.word else null }, - spammers = transientHiddenUsers, + ): LiveHiddenUsers { + val hiddenUsers = blockList.mapNotNullTo(mutableSetOf()) { if (it is UserTag) it.pubKey else null } + muteList.mapNotNull { if (it is UserTag) it.pubKey else null } + val hiddenWords = blockList.mapNotNullTo(mutableSetOf()) { if (it is WordTag) it.word else null } + muteList.mapNotNull { if (it is WordTag) it.word else null } + + return LiveHiddenUsers( showSensitiveContent = showSensitiveContent, + hiddenWordsCase = hiddenWords.map { DualCase(it.lowercase(), it.uppercase()) }, + hiddenUsersHashCodes = hiddenUsers.mapTo(HashSet()) { it.hashCode() }, + spammersHashCodes = transientHiddenUsers.mapTo(HashSet()) { it.hashCode() }, + hiddenUsers = hiddenUsers, + spammers = transientHiddenUsers, + hiddenWords = hiddenWords, ) + } val flow: StateFlow = combineTransform( @@ -102,7 +95,15 @@ class HiddenUsersState( .stateIn( scope, SharingStarted.Eagerly, - LiveHiddenUsers(emptySet(), emptySet(), emptySet(), null), + LiveHiddenUsers( + showSensitiveContent = null, + hiddenWordsCase = emptyList(), + hiddenUsersHashCodes = emptySet(), + spammersHashCodes = emptySet(), + hiddenUsers = emptySet(), + spammers = emptySet(), + hiddenWords = emptySet(), + ), ) fun resetTransientUsers() { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleList.kt index 13c4ce988..4de01330b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleList.kt @@ -24,9 +24,7 @@ import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User - import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.commons.model.User import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListState.kt index 1ad0c874d..aad3082bd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListState.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.model.trustedAssertions import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.trustedAssertions.TrustProviderListState as ITrustProviderListState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent @@ -49,7 +50,7 @@ class TrustProviderListState( val decryptionCache: TrustProviderListDecryptionCache, val scope: CoroutineScope, val settings: AccountSettings, -) { +) : ITrustProviderListState { // Creates a long-term reference for this note so that the GC doesn't collect the note it self val trustProviderListNote = cache.getOrCreateAddressableNote(getTrustProviderListAddress()) @@ -79,7 +80,7 @@ class TrustProviderListState( ) @OptIn(ExperimentalCoroutinesApi::class) - val liveUserRankProvider: StateFlow = + override val liveUserRankProvider: StateFlow = liveTrustProviderList .map { it.firstOrNull { it.service == ProviderTypes.rank } @@ -97,7 +98,7 @@ class TrustProviderListState( ) @OptIn(ExperimentalCoroutinesApi::class) - val liveUserFollowerCount: StateFlow = + override val liveUserFollowerCount: StateFlow = liveTrustProviderList .map { tagList -> tagList.firstOrNull { it.service == ProviderTypes.followerCount } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt index d7b26ea33..88ebbd8b4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.dal -import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState +import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter @@ -36,7 +36,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils class FilterByListParams( val isHiddenList: Boolean, val followLists: IFeedTopNavFilter?, - val hiddenLists: HiddenUsersState.LiveHiddenUsers, + val hiddenLists: LiveHiddenUsers, val now: Long = TimeUtils.oneMinuteFromNow(), ) { fun isNotHidden(userHex: String) = !(hiddenLists.hiddenUsers.contains(userHex) || hiddenLists.spammers.contains(userHex)) @@ -87,7 +87,7 @@ class FilterByListParams( fun create( followLists: IFeedTopNavFilter?, - hiddenUsers: HiddenUsersState.LiveHiddenUsers, + hiddenUsers: LiveHiddenUsers, ): FilterByListParams = FilterByListParams( isHiddenList = followLists is MutedAuthorsByOutboxTopNavFilter || followLists is MutedAuthorsByProxyTopNavFilter, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 03b225c84..ff076ee83 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -51,6 +51,7 @@ import com.vitorpamplona.amethyst.model.UiSettingsFlow import com.vitorpamplona.amethyst.model.UrlCachedPreviewer import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.observables.CreatedAtComparator @@ -341,7 +342,7 @@ class AccountViewModel( fun isNoteAcceptable( note: Note, - accountChoices: HiddenUsersState.LiveHiddenUsers, + accountChoices: LiveHiddenUsers, followUsers: Set, ): NoteComposeReportState { checkNotInMainThread() diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/IAccount.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/IAccount.kt index bbd7805d4..2b2a4510e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/IAccount.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/IAccount.kt @@ -24,8 +24,7 @@ import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent import com.vitorpamplona.quartz.nip47WalletConnect.Request import com.vitorpamplona.quartz.nip47WalletConnect.Response -import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.IPrivateZapsDecryptionCache import com.vitorpamplona.quartz.utils.DualCase /** @@ -40,16 +39,6 @@ interface INwcSignerState { fun isNIP47Author(pubKey: String?): Boolean } -/** - * Interface for private zap decryption cache. - * Used by Note.kt for checking private zap status. - */ -interface IPrivateZapsDecryptionCache { - fun cachedPrivateZap(event: LnZapRequestEvent): LnZapPrivateEvent? - - suspend fun decryptPrivateZap(event: LnZapRequestEvent): LnZapPrivateEvent? -} - /** * Hidden content settings for filtering notes. * Used by Note.isHiddenFor() to check if content should be hidden. @@ -59,7 +48,13 @@ data class LiveHiddenUsers( val hiddenWordsCase: List, val hiddenUsersHashCodes: Set, val spammersHashCodes: Set, -) + // Raw sets for amethyst-specific usage + val hiddenUsers: Set = emptySet(), + val spammers: Set = emptySet(), + val hiddenWords: Set = emptySet(), +) { + fun isUserHidden(userHex: String) = hiddenUsers.contains(userHex) || spammers.contains(userHex) +} /** * Interface for account operations needed by Note.kt. diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/trustedAssertions/TrustProviderListState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/trustedAssertions/TrustProviderListState.kt new file mode 100644 index 000000000..67410df63 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/trustedAssertions/TrustProviderListState.kt @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.model.trustedAssertions + +import com.vitorpamplona.quartz.experimental.trustedAssertions.list.tags.ServiceProviderTag +import kotlinx.coroutines.flow.StateFlow + +/** + * Interface for trust provider list state. + * Used by UserCardsCache for accessing user rank and follower count providers. + */ +interface TrustProviderListState { + val liveUserRankProvider: StateFlow + val liveUserFollowerCount: StateFlow +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/trustedAssertions/UserCardsCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/trustedAssertions/UserCardsCache.kt index 0a1d46074..944486ca5 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/trustedAssertions/UserCardsCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/trustedAssertions/UserCardsCache.kt @@ -136,15 +136,3 @@ class UserCardsCache : UserDependencies { } }.flowOn(Dispatchers.IO) } - -// Placeholder for TrustProviderListState - will be properly extracted later -// For now, this allows UserCardsCache to compile -interface TrustProviderListState { - val liveUserRankProvider: kotlinx.coroutines.flow.StateFlow - val liveUserFollowerCount: kotlinx.coroutines.flow.StateFlow -} - -// Placeholder for ServiceProviderTag -interface ServiceProviderTag { - val pubkey: String -} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/IPrivateZapsDecryptionCache.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/IPrivateZapsDecryptionCache.kt new file mode 100644 index 000000000..b826afd03 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/IPrivateZapsDecryptionCache.kt @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip57Zaps + +/** + * Interface for private zap decryption cache. + * Used by Note.kt for checking private zap status. + */ +interface IPrivateZapsDecryptionCache { + fun cachedPrivateZap(event: LnZapRequestEvent): LnZapPrivateEvent? + + suspend fun decryptPrivateZap(event: LnZapRequestEvent): LnZapPrivateEvent? +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/PrivateZapCache.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/PrivateZapCache.kt index 9e8ede206..e8f9ec77e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/PrivateZapCache.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/PrivateZapCache.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.caches.DecryptCache class PrivateZapCache( signer: NostrSigner, -) { +) : IPrivateZapsDecryptionCache { private val decryptionCache = object : LruCache(1000) { override fun create(key: LnZapRequestEvent): PrivateZapDecryptCache? { @@ -43,9 +43,9 @@ class PrivateZapCache( decryptionCache.remove(event) } - fun cachedPrivateZap(event: LnZapRequestEvent): LnZapPrivateEvent? = decryptionCache[event]?.cached() + override fun cachedPrivateZap(event: LnZapRequestEvent): LnZapPrivateEvent? = decryptionCache[event]?.cached() - suspend fun decryptPrivateZap(event: LnZapRequestEvent) = decryptionCache[event]?.decrypt(event) + override suspend fun decryptPrivateZap(event: LnZapRequestEvent) = decryptionCache[event]?.decrypt(event) } class PrivateZapDecryptCache( From 9b330164a7fc80bb77f8e0c5944346859c6f657c Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Thu, 1 Jan 2026 12:41:13 +0200 Subject: [PATCH 024/137] use typealiases as references to reduce files changed --- .../vitorpamplona/amethyst/model/Account.kt | 6 +- .../amethyst/model/AccountSettings.kt | 6 +- .../amethyst/model/AccountSyncedSettings.kt | 6 +- .../model/AccountSyncedSettingsInternal.kt | 6 +- .../amethyst/model/AntiSpamFilter.kt | 6 +- .../vitorpamplona/amethyst/model/Channel.kt | 8 +-- .../vitorpamplona/amethyst/model/Constants.kt | 6 +- .../amethyst/model/HashtagIcon.kt | 6 +- .../amethyst/model/LargeSoftCache.kt | 6 +- .../model/LargeSoftCacheAddressExt.kt | 6 +- .../amethyst/model/LocalCache.kt | 6 +- .../amethyst/model/MediaAspectRatioCache.kt | 6 +- .../vitorpamplona/amethyst/model/MiniFhir.kt | 6 +- .../com/vitorpamplona/amethyst/model/Note.kt | 29 ++++++++++ .../amethyst/model/ParticipantListBuilder.kt | 6 +- .../amethyst/model/ThreadAssembler.kt | 6 +- .../amethyst/model/ThreadLevelCalculator.kt | 6 +- .../amethyst/model/UiSettings.kt | 6 +- .../amethyst/model/UiSettingsFlow.kt | 6 +- .../amethyst/model/UrlCachedPreviewer.kt | 6 +- .../com/vitorpamplona/amethyst/model/User.kt | 29 ++++++++++ .../model/accountsCache/AccountCacheState.kt | 6 +- .../PrivateStorageRelayListDecryptionCache.kt | 6 +- .../edits/PrivateStorageRelayListState.kt | 4 +- .../model/emphChat/EphemeralChatChannel.kt | 6 +- .../EphemeralChatListDecryptionCache.kt | 6 +- .../model/emphChat/EphemeralChatListState.kt | 4 +- .../model/localRelays/LocalRelayListState.kt | 6 +- .../AccountHomeRelayState.kt | 6 +- .../AccountOutboxRelayState.kt | 6 +- .../NotificationInboxRelayState.kt | 6 +- .../nip01UserMetadata/UserMetadataState.kt | 8 +-- .../DeclaredFollowsPerOutboxRelay.kt | 6 +- .../FollowListOutboxOrProxyRelays.kt | 6 +- .../FollowListReusedOutboxOrProxyRelays.kt | 6 +- .../nip02FollowLists/FollowsPerOutboxRelay.kt | 6 +- .../nip02FollowLists/Kind3FollowListState.kt | 8 +-- .../IncomingOtsEventVerifier.kt | 2 +- .../amethyst/model/nip03Timestamp/OtsState.kt | 2 +- .../TorAwareOkHttpOtsResolverBuilder.kt | 6 +- .../model/nip11RelayInfo/LoadRelayInfo.kt | 6 +- .../nip11RelayInfo/Nip11CachedRetriever.kt | 6 +- .../model/nip11RelayInfo/Nip11Retriever.kt | 6 +- .../model/nip11RelayInfo/RetrieveResult.kt | 6 +- .../model/nip17Dms/DmInboxRelayState.kt | 6 +- .../model/nip17Dms/DmRelayListState.kt | 4 +- .../model/nip18Reposts/RepostAction.kt | 2 +- .../model/nip25Reactions/ReactionAction.kt | 4 +- .../nip28PublicChats/PublicChatChannel.kt | 4 +- .../PublicChatListDecryptionCache.kt | 6 +- .../nip28PublicChats/PublicChatListState.kt | 4 +- .../model/nip30CustomEmojis/EmojiPackState.kt | 4 +- .../nip38UserStatuses/UserStatusAction.kt | 6 +- .../nip47WalletConnect/NwcSignerState.kt | 2 +- .../model/nip51Lists/BookmarkListState.kt | 6 +- .../model/nip51Lists/HiddenUsersState.kt | 6 +- .../blockPeopleList/BlockPeopleListState.kt | 4 +- .../BlockedRelayListDecryptionCache.kt | 6 +- .../blockedRelays/BlockedRelayListState.kt | 4 +- .../BroadcastRelayListDecryptionCache.kt | 6 +- .../BroadcastRelayListState.kt | 4 +- .../geohashLists/GeohashListCard.kt | 6 +- .../GeohashListDecryptionCache.kt | 2 +- .../geohashLists/GeohashListState.kt | 4 +- .../HashtagListDecryptionCache.kt | 6 +- .../hashtagLists/HashtagListState.kt | 4 +- .../IndexerRelayListDecryptionCache.kt | 6 +- .../indexerRelays/IndexerRelayListState.kt | 4 +- .../LabeledBookmarkList.kt | 6 +- .../LabeledBookmarkListsState.kt | 4 +- .../muteList/MuteListDecryptionCache.kt | 6 +- .../nip51Lists/muteList/MuteListState.kt | 4 +- .../nip51Lists/peopleList/FollowListsState.kt | 6 +- .../model/nip51Lists/peopleList/PeopleList.kt | 6 +- .../peopleList/PeopleListDecryptionCache.kt | 6 +- .../nip51Lists/peopleList/PeopleListsState.kt | 6 +- .../ProxyRelayListDecryptionCache.kt | 6 +- .../proxyRelays/ProxyRelayListState.kt | 4 +- .../relayLists/GenericRelayListCache.kt | 2 +- .../nip51Lists/relayLists/RelayListCard.kt | 6 +- .../SearchRelayListDecryptionCache.kt | 6 +- .../searchRelays/SearchRelayListState.kt | 4 +- .../TrustedRelayListDecryptionCache.kt | 6 +- .../trustedRelays/TrustedRelayListState.kt | 4 +- .../LiveActivitiesChannel.kt | 4 +- .../model/nip56Reports/ReportAction.kt | 4 +- .../nip65RelayList/Nip65RelayListState.kt | 4 +- .../CommunityListDecryptionCache.kt | 6 +- .../nip72Communities/CommunityListState.kt | 6 +- .../nip78AppSpecific/AppSpecificState.kt | 2 +- .../FileStorageServerListState.kt | 4 +- .../nipB7Blossom/BlossomServerListState.kt | 4 +- .../model/observables/CreatedAtComparator.kt | 4 +- .../observables/LatestByKindAndAuthor.kt | 4 +- .../model/observables/LatestByKindWithETag.kt | 2 +- .../preferences/AccountPreferenceStores.kt | 6 +- .../AccountSecretsEncryptedStores.kt | 6 +- .../model/preferences/DataStoreExt.kt | 6 +- .../model/preferences/EncryptedDataStore.kt | 6 +- .../model/preferences/KeyStoreEncryption.kt | 6 +- .../model/preferences/TorSharedPreferences.kt | 6 +- .../model/preferences/UISharedPreferences.kt | 6 +- .../preferences/UpdatablePropertyFlow.kt | 6 +- .../EmptyRoleBasedHttpClientBuilder.kt | 6 +- .../IRoleBasedHttpClientBuilder.kt | 6 +- .../RoleBasedHttpClientBuilder.kt | 6 +- .../SingleRoleBasedHttpClientBuilder.kt | 6 +- .../amethyst/model/privateChats/Chatroom.kt | 6 +- .../model/privateChats/ChatroomList.kt | 4 +- .../serverList/MergedFollowListsState.kt | 6 +- .../MergedFollowPlusMineRelayListsState.kt | 6 +- ...dFollowPlusMineWithIndexRelayListsState.kt | 6 +- ...FollowPlusMineWithSearchRelayListsState.kt | 6 +- .../model/serverList/MergedServerListState.kt | 6 +- .../serverList/TrustedRelayListsState.kt | 6 +- .../model/topNavFeeds/CommunityRelayLoader.kt | 2 +- .../model/topNavFeeds/FeedDecryptionCaches.kt | 6 +- .../topNavFeeds/FeedTopNavFilterState.kt | 6 +- .../model/topNavFeeds/IFeedFlowsType.kt | 6 +- .../model/topNavFeeds/IFeedTopNavFilter.kt | 6 +- .../topNavFeeds/IFeedTopNavPerRelayFilter.kt | 6 +- .../IFeedTopNavPerRelayFilterSet.kt | 6 +- .../MergedTopFeedAuthorListsState.kt | 6 +- .../model/topNavFeeds/OutboxLoaderState.kt | 6 +- .../model/topNavFeeds/OutboxRelayLoader.kt | 4 +- .../AllFollowsByOutboxTopNavFilter.kt | 6 +- .../AllFollowsByProxyTopNavFilter.kt | 6 +- .../allFollows/AllFollowsFeedFlow.kt | 6 +- .../AllFollowsTopNavPerRelayFilter.kt | 6 +- .../AllFollowsTopNavPerRelayFilterSet.kt | 6 +- .../AllUserFollowsByOutboxTopNavFilter.kt | 6 +- .../AllUserFollowsByProxyTopNavFilter.kt | 6 +- .../allUserFollows/AllUserFollowsFeedFlow.kt | 6 +- .../Kind3UserFollowsFeedFlow.kt | 6 +- .../topNavFeeds/aroundMe/AroundMeExpander.kt | 6 +- .../topNavFeeds/aroundMe/AroundMeFeedFlow.kt | 6 +- .../aroundMe/LocationTopNavFilter.kt | 6 +- .../aroundMe/LocationTopNavPerRelayFilter.kt | 6 +- .../LocationTopNavPerRelayFilterSet.kt | 6 +- .../topNavFeeds/global/GlobalFeedFlow.kt | 6 +- .../topNavFeeds/global/GlobalTopNavFilter.kt | 6 +- .../global/GlobalTopNavPerRelayFilter.kt | 6 +- .../global/GlobalTopNavPerRelayFilterSet.kt | 6 +- .../hashtag/HashtagTopNavFilter.kt | 6 +- .../hashtag/HashtagTopNavPerRelayFilter.kt | 6 +- .../hashtag/HashtagTopNavPerRelayFilterSet.kt | 6 +- .../topNavFeeds/noteBased/NoteFeedFlow.kt | 2 +- .../AllCommunitiesTopNavFilter.kt | 6 +- .../AllCommunitiesTopNavPerRelayFilter.kt | 6 +- .../AllCommunitiesTopNavPerRelayFilterSet.kt | 6 +- .../author/AuthorsByOutboxTopNavFilter.kt | 6 +- .../author/AuthorsByProxyTopNavFilter.kt | 6 +- .../author/AuthorsTopNavPerRelayFilter.kt | 6 +- .../author/AuthorsTopNavPerRelayFilterSet.kt | 6 +- .../community/SingleCommunityTopNavFilter.kt | 6 +- .../SingleCommunityTopNavPerRelayFilter.kt | 6 +- .../SingleCommunityTopNavPerRelayFilterSet.kt | 6 +- .../muted/MutedAuthorsByOutboxTopNavFilter.kt | 6 +- .../muted/MutedAuthorsByProxyTopNavFilter.kt | 6 +- .../muted/MutedAuthorsTopNavPerRelayFilter.kt | 6 +- .../MutedAuthorsTopNavPerRelayFilterSet.kt | 6 +- .../topNavFeeds/unknown/UnknownFeedFlow.kt | 6 +- .../unknown/UnknownTopNavFilter.kt | 6 +- .../unknown/UnknownTopNavPerRelayFilterSet.kt | 6 +- .../torState/AccountsTorStateConnector.kt | 6 +- .../model/torState/TorRelayEvaluation.kt | 6 +- .../model/torState/TorRelaySettings.kt | 6 +- .../amethyst/model/torState/TorRelayState.kt | 6 +- .../TrustProviderListDecryptionCache.kt | 6 +- .../TrustProviderListState.kt | 4 +- .../amethyst/service/ZapPaymentHandler.kt | 4 +- .../EventNotificationConsumer.kt | 2 +- .../relayClient/CacheClientConnector.kt | 2 +- .../PerUserAndFollowListEoseManager.kt | 2 +- .../eoseManagers/PerUserEoseManager.kt | 2 +- .../reqCommand/account/AccountObservers.kt | 2 +- .../drafts/AccountDraftsEoseManager.kt | 2 +- .../AccountFollowsLoaderSubAssembler.kt | 2 +- .../follows/FilterFindFollowMetadataForKey.kt | 2 +- .../metadata/AccountMetadataEoseManager.kt | 2 +- ...NotificationsEoseFromInboxRelaysManager.kt | 2 +- ...otificationsEoseFromRandomRelaysManager.kt | 2 +- .../AccountGiftWrapsEoseManager.kt | 2 +- .../reqCommand/channel/ChannelObservers.kt | 2 +- .../event/EventFinderFilterAssembler.kt | 2 +- .../EventFinderFilterAssemblerSubscription.kt | 2 +- .../reqCommand/event/EventObservers.kt | 6 +- .../loaders/FilterMissingAddressables.kt | 2 +- .../event/loaders/FilterMissingEvents.kt | 4 +- .../watchers/EventWatcherSubAssembler.kt | 4 +- .../FilterRepliesAndReactionsToAddresses.kt | 2 +- .../FilterRepliesAndReactionsToNotes.kt | 2 +- .../NWCFinderFilterAssemblerSubscription.kt | 2 +- .../user/UserFinderFilterAssembler.kt | 2 +- .../UserFinderFilterAssemblerSubscription.kt | 2 +- .../reqCommand/user/UserObservers.kt | 8 +-- .../loaders/UserOutboxFinderSubAssembler.kt | 2 +- .../user/watchers/FilterUserMetadataForKey.kt | 2 +- .../user/watchers/UserCardsSubAssembler.kt | 2 +- .../user/watchers/UserReportsSubAssembler.kt | 2 +- .../user/watchers/UserWatcherSubAssembler.kt | 2 +- .../subassemblies/FilterByEvent.kt | 2 +- .../amethyst/service/relays/EOSE.kt | 35 ++---------- .../amethyst/ui/actions/EditPostView.kt | 2 +- .../amethyst/ui/actions/EditPostViewModel.kt | 4 +- .../amethyst/ui/actions/NewMessageTagger.kt | 6 +- .../amethyst/ui/components/ClickableRoute.kt | 4 +- .../ui/components/ReusableZapButton.kt | 4 +- .../amethyst/ui/components/RichTextViewer.kt | 4 +- .../ui/components/SensitivityWarning.kt | 2 +- .../markdown/MarkdownMediaRenderer.kt | 2 +- .../ui/components/toasts/ToastManager.kt | 2 +- .../components/toasts/multiline/ErrorList.kt | 2 +- .../toasts/multiline/MultiErrorToastMsg.kt | 2 +- .../multiline/MultiUserErrorMessageDialog.kt | 2 +- .../amethyst/ui/dal/DefaultFeedOrder.kt | 2 +- .../ui/feeds/ChannelFeedContentState.kt | 2 +- .../amethyst/ui/feeds/FeedState.kt | 2 +- .../drawer/AccountSwitchBottomSheet.kt | 2 +- .../ui/navigation/drawer/DrawerContent.kt | 2 +- .../ui/navigation/routes/RouteMaker.kt | 4 +- .../navigation/topbars/FeedFilterSpinner.kt | 2 +- .../amethyst/ui/note/BlankNote.kt | 2 +- .../amethyst/ui/note/BlockReportChecker.kt | 2 +- .../amethyst/ui/note/DisplayAuthorBanner.kt | 2 +- .../vitorpamplona/amethyst/ui/note/Gallery.kt | 2 +- .../vitorpamplona/amethyst/ui/note/Loaders.kt | 6 +- .../amethyst/ui/note/MultiSetCompose.kt | 6 +- .../ui/note/NIP05VerificationDisplay.kt | 6 +- .../amethyst/ui/note/NoteCompose.kt | 4 +- .../amethyst/ui/note/NoteQuickActionMenu.kt | 6 +- .../amethyst/ui/note/PollNote.kt | 4 +- .../amethyst/ui/note/PollNoteViewModel.kt | 4 +- .../amethyst/ui/note/ReactionsRow.kt | 4 +- .../amethyst/ui/note/RelayCompose.kt | 2 +- .../amethyst/ui/note/RelayListBox.kt | 2 +- .../amethyst/ui/note/RelayListRow.kt | 2 +- .../amethyst/ui/note/ReplyInformation.kt | 4 +- .../ui/note/UpdateReactionTypeDialog.kt | 2 +- .../amethyst/ui/note/UserCompose.kt | 2 +- .../amethyst/ui/note/UserProfilePicture.kt | 4 +- .../amethyst/ui/note/UsernameDisplay.kt | 4 +- .../amethyst/ui/note/WatchNoteEvent.kt | 2 +- .../amethyst/ui/note/ZapCustomDialog.kt | 4 +- .../amethyst/ui/note/ZapNoteCompose.kt | 4 +- .../note/creators/invoice/InvoiceRequest.kt | 2 +- .../ui/note/creators/notify/Notifying.kt | 2 +- .../userSuggestions/ShowUserSuggestionList.kt | 2 +- .../userSuggestions/UserSuggestionState.kt | 2 +- .../ui/note/creators/zapsplits/IZapField.kt | 2 +- .../note/creators/zapsplits/SplitConversor.kt | 2 +- .../ui/note/elements/DefaultImageHeader.kt | 4 +- .../ui/note/elements/DisplayCommunity.kt | 2 +- .../ui/note/elements/DisplayHashtags.kt | 2 +- .../amethyst/ui/note/elements/DisplayOts.kt | 2 +- .../ui/note/elements/DisplayReward.kt | 2 +- .../amethyst/ui/note/elements/DropDownMenu.kt | 4 +- .../amethyst/ui/note/elements/ForkInfo.kt | 2 +- .../amethyst/ui/note/elements/TimeAgo.kt | 2 +- .../nip22Comments/CommentPostViewModel.kt | 4 +- .../nip22Comments/GenericCommentPostScreen.kt | 2 +- .../amethyst/ui/note/types/AppDefinition.kt | 2 +- .../amethyst/ui/note/types/AudioTrack.kt | 4 +- .../amethyst/ui/note/types/Badge.kt | 4 +- .../amethyst/ui/note/types/ChannelMessage.kt | 2 +- .../amethyst/ui/note/types/ChatMessage.kt | 2 +- .../ui/note/types/ChatMessageEncryptedFile.kt | 2 +- .../amethyst/ui/note/types/Classifieds.kt | 2 +- .../amethyst/ui/note/types/CommunityHeader.kt | 6 +- .../amethyst/ui/note/types/Emoji.kt | 2 +- .../amethyst/ui/note/types/FileHeader.kt | 2 +- .../amethyst/ui/note/types/FileStorage.kt | 2 +- .../amethyst/ui/note/types/FollowList.kt | 2 +- .../amethyst/ui/note/types/Git.kt | 4 +- .../amethyst/ui/note/types/Highlight.kt | 4 +- .../ui/note/types/InteractiveStory.kt | 4 +- .../amethyst/ui/note/types/LiveActivity.kt | 4 +- .../ui/note/types/LiveActivityChatMessage.kt | 2 +- .../amethyst/ui/note/types/LongForm.kt | 2 +- .../amethyst/ui/note/types/MedicalData.kt | 2 +- .../types/NIP90ContentDiscoveryResponse.kt | 2 +- .../amethyst/ui/note/types/NIP90Status.kt | 2 +- .../amethyst/ui/note/types/PeopleList.kt | 4 +- .../amethyst/ui/note/types/PictureDisplay.kt | 2 +- .../amethyst/ui/note/types/PinList.kt | 2 +- .../amethyst/ui/note/types/Poll.kt | 2 +- .../amethyst/ui/note/types/PrivateMessage.kt | 2 +- .../amethyst/ui/note/types/PublicMessage.kt | 4 +- .../amethyst/ui/note/types/Reaction.kt | 2 +- .../amethyst/ui/note/types/RelayList.kt | 2 +- .../ui/note/types/RenderPostApproval.kt | 2 +- .../amethyst/ui/note/types/Report.kt | 2 +- .../amethyst/ui/note/types/Text.kt | 2 +- .../ui/note/types/TextModification.kt | 2 +- .../amethyst/ui/note/types/Torrent.kt | 2 +- .../amethyst/ui/note/types/TorrentComment.kt | 2 +- .../amethyst/ui/note/types/Video.kt | 2 +- .../amethyst/ui/note/types/VideoDisplay.kt | 2 +- .../amethyst/ui/note/types/VoiceTrack.kt | 2 +- .../amethyst/ui/note/types/Wiki.kt | 2 +- .../amethyst/ui/screen/FeedViewModel.kt | 2 +- .../amethyst/ui/screen/TopNavFilterState.kt | 2 +- .../amethyst/ui/screen/UserFeedState.kt | 2 +- .../amethyst/ui/screen/UserFeedViewModel.kt | 2 +- .../loggedIn/AccountFeedContentStates.kt | 2 +- .../ui/screen/loggedIn/AccountViewModel.kt | 6 +- .../loggedIn/DecryptAndIndexProcessor.kt | 2 +- .../bookmarkgroups/display/ArticleListView.kt | 2 +- .../display/BookmarkGroupItemOptions.kt | 2 +- .../bookmarkgroups/display/PostListView.kt | 2 +- .../ArticleBookmarkListManagementScreen.kt | 2 +- .../PostBookmarkListManagementScreen.kt | 2 +- .../dal/BookmarkPrivateFeedFilter.kt | 2 +- .../bookmarks/dal/BookmarkPublicFeedFilter.kt | 2 +- .../loggedIn/chats/feed/ChatFeedView.kt | 2 +- .../loggedIn/chats/feed/ChatMessageCompose.kt | 2 +- .../screen/loggedIn/chats/feed/ChatTimeAgo.kt | 2 +- .../loggedIn/chats/feed/DrawAuthorInfo.kt | 4 +- .../chats/feed/NewDateOrSubjectDivisor.kt | 2 +- .../types/RenderChangeChannelMetadataNote.kt | 2 +- .../feed/types/RenderCreateChannelNote.kt | 2 +- .../chats/feed/types/RenderDraftEvent.kt | 2 +- .../chats/feed/types/RenderEncryptedFile.kt | 2 +- .../chats/feed/types/RenderRegularTextNote.kt | 2 +- .../chats/privateDM/IncognitoBadge.kt | 2 +- .../chats/privateDM/dal/ChatroomFeedFilter.kt | 2 +- .../privateDM/dal/ChatroomFeedViewModel.kt | 2 +- .../chats/privateDM/header/ChatroomHeader.kt | 2 +- .../privateDM/header/RoomNameOnlyDisplay.kt | 2 +- .../privateDM/send/ChatNewMessageViewModel.kt | 4 +- .../publicChannels/dal/ChannelFeedFilter.kt | 2 +- .../publicChannels/ephemChat/ChannelView.kt | 2 +- .../ephemChat/EphemeralChatScreen.kt | 2 +- .../nip28PublicChat/ChannelView.kt | 2 +- .../PublicChatChannelScreen.kt | 2 +- .../nip53LiveActivities/ChannelView.kt | 2 +- .../LiveActivityChannelScreen.kt | 2 +- .../header/LongLiveActivityChannelHeader.kt | 2 +- .../header/ShortLiveActivityChannelHeader.kt | 2 +- .../send/ChannelNewMessageViewModel.kt | 4 +- .../chats/rooms/ChatroomHeaderCompose.kt | 4 +- .../rooms/dal/ChatroomListKnownFeedFilter.kt | 2 +- .../rooms/dal/ChatroomListNewFeedFilter.kt | 2 +- .../DMsFromUserFilterSubAssembler.kt | 2 +- .../rooms/datasource/FilterNip04DMsFromMe.kt | 2 +- .../rooms/datasource/FilterNip04DMsToMe.kt | 2 +- .../FollowingEphemeralChatSubAssembler.kt | 2 +- .../FollowingPublicChatSubAssembler.kt | 2 +- .../chats/utils/DisplayReplyingToNote.kt | 2 +- .../loggedIn/communities/CommunityScreen.kt | 2 +- .../communities/NewCommunityNoteButton.kt | 2 +- .../communities/dal/CommunityFeedFilter.kt | 4 +- .../communities/dal/CommunityFeedViewModel.kt | 2 +- .../dal/CommunityModerationFeedFilter.kt | 4 +- .../dal/CommunityModerationFeedViewModel.kt | 2 +- .../datasource/CommunityFilterAssembler.kt | 2 +- .../CommunityFilterAssemblerSubscription.kt | 2 +- .../loggedIn/discover/ChannelCardCompose.kt | 2 +- ...yFollowsSetsAndLiveStreamsSubAssembler2.kt | 2 +- ...yLongFormClassifiedsAndDVMSubAssembler1.kt | 2 +- ...yPublicChatsAndCommunitiesSubAssembler3.kt | 2 +- .../DiscoverLongFormFeedFilter.kt | 2 +- .../discover/nip23LongForm/LongFormCard.kt | 2 +- .../nip28Chats/DiscoverChatFeedFilter.kt | 2 +- .../RenderPublicChatChannelThumb.kt | 4 +- .../DiscoverFollowSetsFeedFilter.kt | 2 +- .../discover/nip51FollowSets/FollowSetCard.kt | 2 +- .../DiscoverLiveFeedFilter.kt | 2 +- .../nip53LiveActivities/LiveActivityCard.kt | 4 +- .../nip72Communities/CommunityCard.kt | 4 +- .../DiscoverCommunityFeedFilter.kt | 2 +- .../loggedIn/discover/nip90DVMs/DVMCard.kt | 2 +- .../nip90DVMs/DiscoverNIP89FeedFilter.kt | 2 +- .../nip99Classifieds/ClassifiedsThumb.kt | 2 +- .../DiscoverMarketplaceFeedFilter.kt | 2 +- .../nip99Classifieds/NewProductScreen.kt | 2 +- .../nip99Classifieds/NewProductViewModel.kt | 4 +- .../drafts/dal/DraftEventsFeedFilter.kt | 2 +- .../dvms/DvmContentDiscoveryScreen.kt | 4 +- .../NIP90ContentDiscoveryResponseFilter.kt | 2 +- .../followPacks/feed/FollowPackFeedScreen.kt | 2 +- .../FollowPackFeedConversationsFeedFilter.kt | 4 +- ...ollowPackFeedConversationsFeedViewModel.kt | 2 +- .../dal/FollowPackFeedNewThreadFeedFilter.kt | 4 +- .../FollowPackFeedNewThreadFeedViewModel.kt | 2 +- .../feed/dal/FollowPackMembersFeedFilter.kt | 4 +- .../dal/FollowPackMembersUserFeedViewModel.kt | 2 +- .../FollowPackFeedFilterAssembler.kt | 2 +- ...llowPackFeedFilterAssemblerSubscription.kt | 2 +- .../loggedIn/geohash/GeoHashPostScreen.kt | 2 +- .../loggedIn/geohash/dal/GeoHashFeedFilter.kt | 2 +- .../loggedIn/hashtag/HashtagPostScreen.kt | 2 +- .../loggedIn/hashtag/dal/HashtagFeedFilter.kt | 2 +- .../loggedIn/home/ShortNotePostScreen.kt | 2 +- .../loggedIn/home/ShortNotePostViewModel.kt | 4 +- .../loggedIn/home/VoiceReplyViewModel.kt | 2 +- .../home/dal/HomeConversationsFeedFilter.kt | 2 +- .../loggedIn/home/dal/HomeLiveFilter.kt | 2 +- .../home/dal/HomeNewThreadFeedFilter.kt | 2 +- .../HomeOutboxEventsEoseManager.kt | 2 +- .../loggedIn/lists/display/PeopleListView.kt | 2 +- .../lists/display/ShowUserSuggestions.kt | 2 +- .../lists/display/lists/PeopleListScreen.kt | 4 +- .../display/lists/PeopleListViewModel.kt | 4 +- .../lists/display/packs/FollowPackScreen.kt | 4 +- .../display/packs/FollowPackViewModel.kt | 4 +- .../loggedIn/lists/list/PeopleListItem.kt | 2 +- .../FollowListAndPackAndUserScreen.kt | 2 +- .../FollowListAndPackAndUserView.kt | 2 +- .../notifications/CardFeedContentState.kt | 4 +- .../loggedIn/notifications/CardFeedState.kt | 4 +- .../notifications/NotificationSummaryState.kt | 4 +- .../dal/NotificationFeedFilter.kt | 4 +- .../notifications/donations/ZapTheDevsCard.kt | 2 +- .../publicMessages/NewPublicMessageScreen.kt | 2 +- .../NewPublicMessageViewModel.kt | 4 +- .../screen/loggedIn/profile/ProfileScreen.kt | 2 +- .../profile/bookmarks/BookmarkTabHeader.kt | 2 +- .../dal/UserProfileBookmarksFeedFilter.kt | 4 +- .../dal/UserProfileBookmarksFeedViewModel.kt | 2 +- .../dal/UserProfileConversationsFeedFilter.kt | 4 +- .../UserProfileConversationsFeedViewModel.kt | 2 +- .../datasource/FilterUserProfileFollowers.kt | 2 +- .../datasource/FilterUserProfileMedia.kt | 2 +- .../datasource/FilterUserProfilePosts.kt | 2 +- .../FilterUserProfileZapReceived.kt | 2 +- .../datasource/UserProfileFilterAssembler.kt | 2 +- .../UserProfileFilterAssemblerSubscription.kt | 2 +- .../profile/followers/FollowersTabHeader.kt | 2 +- .../profile/followers/TabFollowers.kt | 2 +- .../dal/UserProfileFollowersFeedFilter.kt | 2 +- .../UserProfileFollowersUserFeedViewModel.kt | 2 +- .../profile/follows/FollowTabHeader.kt | 2 +- .../loggedIn/profile/follows/TabFollows.kt | 2 +- .../dal/UserProfileFollowsFeedFilter.kt | 2 +- .../UserProfileFollowsUserFeedViewModel.kt | 2 +- .../profile/gallery/GalleryCardCompose.kt | 2 +- .../loggedIn/profile/gallery/GalleryThumb.kt | 2 +- .../profile/gallery/QuickActionGallery.kt | 2 +- .../dal/UserProfileGalleryFeedFilter.kt | 4 +- .../dal/UserProfileGalleryFeedViewModel.kt | 2 +- .../profile/hashtags/FollowedTagsTabHeader.kt | 2 +- .../profile/hashtags/TabFollowedTags.kt | 2 +- .../header/DisplayFollowUnfollowButton.kt | 2 +- .../profile/header/DisplayLNAddress.kt | 2 +- .../profile/header/DrawAdditionalInfo.kt | 2 +- .../loggedIn/profile/header/DrawBanner.kt | 2 +- .../loggedIn/profile/header/MessageButton.kt | 2 +- .../loggedIn/profile/header/ProfileActions.kt | 2 +- .../loggedIn/profile/header/ProfileHeader.kt | 2 +- .../profile/header/UserProfileDropDownMenu.kt | 2 +- .../UserAppRecommendationsFeedViewModel.kt | 2 +- ...UserProfileAppRecommendationsFeedFilter.kt | 4 +- .../loggedIn/profile/header/apps/WatchApp.kt | 2 +- .../profile/header/badges/DisplayBadges.kt | 6 +- .../mutual/dal/UserProfileMutualFeedFilter.kt | 6 +- .../dal/UserProfileMutualFeedViewModel.kt | 2 +- .../dal/UserProfileNewThreadFeedFilter.kt | 6 +- .../dal/UserProfileNewThreadsFeedViewModel.kt | 2 +- .../loggedIn/profile/relays/RelayFeedView.kt | 2 +- .../profile/relays/RelayFeedViewModel.kt | 4 +- .../profile/relays/RelaysTabHeader.kt | 2 +- .../loggedIn/profile/relays/TabRelays.kt | 2 +- .../profile/reports/ReportsTabHeader.kt | 2 +- .../loggedIn/profile/reports/TabReports.kt | 2 +- .../reports/WatchReportsAndUpdateFeed.kt | 2 +- .../dal/UserProfileReportFeedViewModel.kt | 2 +- .../dal/UserProfileReportsFeedFilter.kt | 4 +- .../loggedIn/profile/zaps/LnZapFeedState.kt | 2 +- .../loggedIn/profile/zaps/TabReceivedZaps.kt | 2 +- .../profile/zaps/WatchZapsAndUpdateFeed.kt | 2 +- .../loggedIn/profile/zaps/ZapTabHeader.kt | 2 +- .../zaps/dal/UserProfileZapsFeedFilter.kt | 4 +- .../zaps/dal/UserProfileZapsFeedViewModel.kt | 2 +- .../ui/screen/loggedIn/qrcode/ShowQRScreen.kt | 2 +- .../loggedIn/redirect/LoadRedirectScreen.kt | 2 +- .../relays/common/BasicRelaySetupInfo.kt | 2 +- .../loggedIn/report/ReportNoteDialog.kt | 2 +- .../settings/dal/HiddenAccountsFeedFilter.kt | 2 +- .../settings/dal/SpammerAccountsFeedFilter.kt | 2 +- .../loggedIn/threadview/ThreadFeedView.kt | 2 +- .../threadview/dal/LevelFeedViewModel.kt | 2 +- .../threadview/dal/ThreadFeedFilter.kt | 4 +- .../FilterEventsInThreadForRoot.kt | 4 +- .../FilterMissingEventsForThread.kt | 4 +- .../ui/screen/loggedIn/video/VideoScreen.kt | 2 +- .../loggedIn/video/dal/VideoFeedFilter.kt | 6 +- .../VideoOutboxEventsFilterSubAssembler.kt | 2 +- .../amethyst/NewMessageTaggerKeyParseTest.kt | 2 +- .../zaps/UserProfileZapsFeedFilterTest.kt | 2 +- .../model/nip56Reports/UserReportCache.kt | 4 ++ .../model/trustedAssertions/UserCardsCache.kt | 4 ++ .../amethyst/commons/relays/EOSERelayList.kt | 56 +++++++++++++++++++ .../amethyst/commons}/relays/MutableTime.kt | 2 +- 494 files changed, 995 insertions(+), 898 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relays/EOSERelayList.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/relays/MutableTime.kt (97%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 689777b3e..264b54cb6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -21,11 +21,11 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.IAccount import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.commons.richtext.RichTextParser diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index e330f2427..cd5dfcd3e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt index 1bdfa1ceb..26251c0f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt index b11e58241..020e3769d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import android.content.res.Resources diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt index b20a36ddd..fb57bb562 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import android.util.LruCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt index 1e3444774..6ff880a6b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.NotesGatherer -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NotesGatherer +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Constants.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Constants.kt index 88b8d2e2c..ee72252ad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Constants.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Constants.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt index 68ee0e9d6..070603f64 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.foundation.layout.padding diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LargeSoftCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LargeSoftCache.kt index ea9d1c2a6..e327bb5dc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LargeSoftCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LargeSoftCache.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.utils.cache.CacheOperations diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LargeSoftCacheAddressExt.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LargeSoftCacheAddressExt.kt index 9a22b1984..a68632e9e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LargeSoftCacheAddressExt.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LargeSoftCacheAddressExt.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip01Core.core.Address diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index b33c1fe15..a9119ec3a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.model import android.util.LruCache import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MediaAspectRatioCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MediaAspectRatioCache.kt index 6c19286dc..7c18720f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MediaAspectRatioCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MediaAspectRatioCache.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import android.util.LruCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MiniFhir.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MiniFhir.kt index a1d8f37c7..c8abb5df9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MiniFhir.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MiniFhir.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt new file mode 100644 index 000000000..d203c59e5 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model + +// Re-export from commons for backwards compatibility +typealias Note = com.vitorpamplona.amethyst.commons.model.Note +typealias NotesGatherer = com.vitorpamplona.amethyst.commons.model.NotesGatherer +typealias AddressableNote = com.vitorpamplona.amethyst.commons.model.AddressableNote +typealias NoteFlowSet = com.vitorpamplona.amethyst.commons.model.NoteFlowSet +typealias NoteBundledRefresherFlow = com.vitorpamplona.amethyst.commons.model.NoteBundledRefresherFlow +typealias NoteState = com.vitorpamplona.amethyst.commons.model.NoteState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ParticipantListBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ParticipantListBuilder.kt index 7c6de1329..ffadedace 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ParticipantListBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ParticipantListBuilder.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt index ea0745e47..6e7ae6263 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadLevelCalculator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadLevelCalculator.kt index 793cd699b..228c66b6f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadLevelCalculator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadLevelCalculator.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt index 8cdadf40d..8742bf246 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt index ada6df929..c30b4b762 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import kotlinx.coroutines.flow.Flow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt index 16aaaa512..f8706735e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import android.util.LruCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt new file mode 100644 index 000000000..bfa68c725 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model + +// Re-export from commons for backwards compatibility +typealias UserDependencies = com.vitorpamplona.amethyst.commons.model.UserDependencies +typealias User = com.vitorpamplona.amethyst.commons.model.User +typealias UserFlowSet = com.vitorpamplona.amethyst.commons.model.UserFlowSet +typealias RelayInfo = com.vitorpamplona.amethyst.commons.model.RelayInfo +typealias UserBundledRefresherFlow = com.vitorpamplona.amethyst.commons.model.UserBundledRefresherFlow +typealias UserState = com.vitorpamplona.amethyst.commons.model.UserState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt index 1f73d36a6..6031ad0a3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.accountsCache -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import android.content.ContentResolver diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListDecryptionCache.kt index 90f2dc39c..a0127c9f0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListDecryptionCache.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.edits -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt index 4f2160ff0..280a11db6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.edits -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt index 51a638065..ab78e2557 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.emphChat -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListDecryptionCache.kt index 87676ea32..e53994812 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListDecryptionCache.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.emphChat -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt index 20b818887..deb29e22f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.emphChat -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/LocalRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/LocalRelayListState.kt index 260046260..f0d33cb59 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/LocalRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/LocalRelayListState.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.localRelays -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.AccountSettings diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountHomeRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountHomeRelayState.kt index 8a5222bf6..5b3359649 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountHomeRelayState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountHomeRelayState.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip01UserMetadata -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt index e541de1ab..32cfa28eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip01UserMetadata -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/NotificationInboxRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/NotificationInboxRelayState.kt index 0b5166480..84a3d7a79 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/NotificationInboxRelayState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/NotificationInboxRelayState.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip01UserMetadata -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt index a831e786a..f471ea257 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt @@ -20,12 +20,12 @@ */ package com.vitorpamplona.amethyst.model.nip01UserMetadata -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.commons.model.UserState +import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/DeclaredFollowsPerOutboxRelay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/DeclaredFollowsPerOutboxRelay.kt index 6f0c8f650..572d2c998 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/DeclaredFollowsPerOutboxRelay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/DeclaredFollowsPerOutboxRelay.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip02FollowLists -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxOrProxyRelays.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxOrProxyRelays.kt index 0ca41acab..8ae4dc37d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxOrProxyRelays.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxOrProxyRelays.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip02FollowLists -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListReusedOutboxOrProxyRelays.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListReusedOutboxOrProxyRelays.kt index da5d81f78..504ada7df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListReusedOutboxOrProxyRelays.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListReusedOutboxOrProxyRelays.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip02FollowLists -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt index d88442bdf..42999f6d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip02FollowLists -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/Kind3FollowListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/Kind3FollowListState.kt index 2df798ef6..798986508 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/Kind3FollowListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/Kind3FollowListState.kt @@ -20,12 +20,12 @@ */ package com.vitorpamplona.amethyst.model.nip02FollowLists -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Immutable -import com.vitorpamplona.amethyst.commons.model.UserState +import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/IncomingOtsEventVerifier.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/IncomingOtsEventVerifier.kt index be0cdb538..f86e1a3fd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/IncomingOtsEventVerifier.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/IncomingOtsEventVerifier.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.model.nip03Timestamp -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt index 7572f071b..f46820c3f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.model.nip03Timestamp -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/TorAwareOkHttpOtsResolverBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/TorAwareOkHttpOtsResolverBuilder.kt index e1f33bb7b..33f967321 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/TorAwareOkHttpOtsResolverBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/TorAwareOkHttpOtsResolverBuilder.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip03Timestamp -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/LoadRelayInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/LoadRelayInfo.kt index c84cd6fe9..a5d0eaab5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/LoadRelayInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/LoadRelayInfo.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip11RelayInfo -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11CachedRetriever.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11CachedRetriever.kt index 8fea6bbbc..1a8eaaa50 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11CachedRetriever.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11CachedRetriever.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip11RelayInfo -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import android.util.LruCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11Retriever.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11Retriever.kt index 2bc324f11..da740ce84 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11Retriever.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11Retriever.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip11RelayInfo -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/RetrieveResult.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/RetrieveResult.kt index 8d203c006..ee9491a4b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/RetrieveResult.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/RetrieveResult.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip11RelayInfo -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmInboxRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmInboxRelayState.kt index 3123283a1..deb665cf1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmInboxRelayState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmInboxRelayState.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip17Dms -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt index c19363987..0c91e4bde 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip17Dms -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip18Reposts/RepostAction.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip18Reposts/RepostAction.kt index adaa18854..4d5d1276d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip18Reposts/RepostAction.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip18Reposts/RepostAction.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.model.nip18Reposts -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip25Reactions/ReactionAction.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip25Reactions/ReactionAction.kt index 7e6fffed8..bf580d23c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip25Reactions/ReactionAction.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip25Reactions/ReactionAction.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip25Reactions -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt index 5636e1632..ba31086bd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.amethyst.model.nip28PublicChats import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.ui.note.toShortDisplay import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListDecryptionCache.kt index 036c97e12..84c75b282 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListDecryptionCache.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip28PublicChats -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt index 1b8668a44..10a9a52dd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip28PublicChats -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt index 4be970327..6182c03b0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip30CustomEmojis -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip38UserStatuses/UserStatusAction.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip38UserStatuses/UserStatusAction.kt index 69e7bfd52..e9916f754 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip38UserStatuses/UserStatusAction.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip38UserStatuses/UserStatusAction.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip38UserStatuses -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt index 18d19a409..fad4ce0b1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.model.nip47WalletConnect import com.vitorpamplona.amethyst.commons.model.INwcSignerState -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BookmarkListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BookmarkListState.kt index 2f5d96d62..a822975c0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BookmarkListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BookmarkListState.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt index 841576a3d..9e509ba51 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists -import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt index f8f7d831f..c4b9e7e67 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleListDecryptionCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListDecryptionCache.kt index a7614341f..2d1ecacc1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListDecryptionCache.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListState.kt index 9b07abdfa..39d639933 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListState.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListDecryptionCache.kt index a10cb5c4b..064bfd46b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListDecryptionCache.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListState.kt index 28d9876b0..4f58858b8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListState.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListCard.kt index 340ba533b..30e5bbeb3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListCard.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.geohashLists -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User data class GeohashListCard( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListDecryptionCache.kt index 752c1b2c2..4ea18aadb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListDecryptionCache.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.geohashLists -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListState.kt index bcd6a4afd..fce1e5880 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListState.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.geohashLists -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt index e1bd8c05a..9cc6f0c7e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt index ea7908fd1..c12ed2de2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListDecryptionCache.kt index 78d371097..59fbbee71 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListDecryptionCache.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListState.kt index a239b5d8c..3aec5f510 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListState.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList import com.vitorpamplona.amethyst.model.LocalCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkList.kt index 712e7e74a..e0e0b7115 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkList.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt index 7859c198a..61c98794f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.commons.model.anyNotNullEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListDecryptionCache.kt index 80f342b14..74ad33385 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListDecryptionCache.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.muteList -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListState.kt index 40d64aec8..534a03dff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListState.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.muteList -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt index 2bc5b7e87..7cd417768 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.peopleList -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.commons.model.anyNotNullEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleList.kt index 4de01330b..9cf1c7655 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleList.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.peopleList -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Stable import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListDecryptionCache.kt index ef84acbb2..bbf1bbcfb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListDecryptionCache.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.peopleList -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt index b145b602e..06a208c4e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.peopleList -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.commons.model.anyNotNullEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListDecryptionCache.kt index fc4fc80ae..fba8dfe86 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListDecryptionCache.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListState.kt index 60ef944bb..a3bfe4e57 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListState.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayLists/GenericRelayListCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayLists/GenericRelayListCache.kt index db48ab376..e62fd6a8a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayLists/GenericRelayListCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayLists/GenericRelayListCache.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.relayLists -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayLists/RelayListCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayLists/RelayListCard.kt index da7e5d0e5..93a4e3030 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayLists/RelayListCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayLists/RelayListCard.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.relayLists -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListDecryptionCache.kt index 617a91ccf..d99d77b32 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListDecryptionCache.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.searchRelays -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListState.kt index 2d3bbff1c..cc6ad368a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListState.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.searchRelays -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.DefaultSearchRelayList import com.vitorpamplona.amethyst.model.LocalCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListDecryptionCache.kt index d45560807..a97a56abe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListDecryptionCache.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListState.kt index 81830bf7c..b4cede81f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListState.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt index ae7c16f9d..1d07e838f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.amethyst.model.nip53LiveActivities import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.ui.note.toShortDisplay import com.vitorpamplona.quartz.nip01Core.core.Address diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/ReportAction.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/ReportAction.kt index 0809a5119..1dfc00f11 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/ReportAction.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/ReportAction.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip56Reports -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip56Reports.ReportType diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/Nip65RelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/Nip65RelayListState.kt index cba2e7721..f961edbf0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/Nip65RelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/Nip65RelayListState.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip65RelayList -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.Constants import com.vitorpamplona.amethyst.model.LocalCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListDecryptionCache.kt index 642ff0ae8..e34c72a4b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListDecryptionCache.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip72Communities -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt index e2d801a61..125dcad1b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip72Communities -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt index 1455f2976..d36b874a9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.model.nip78AppSpecific -import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.AccountSyncedSettingsInternal import com.vitorpamplona.amethyst.model.LocalCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip96FileStorage/FileStorageServerListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip96FileStorage/FileStorageServerListState.kt index 9a4b921bf..6d0612fc4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip96FileStorage/FileStorageServerListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip96FileStorage/FileStorageServerListState.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip96FileStorage -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt index a2de6b5bf..23ddf1da3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nipB7Blossom -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/CreatedAtComparator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/CreatedAtComparator.kt index cd4228f92..c13a4be33 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/CreatedAtComparator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/CreatedAtComparator.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.observables -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note object CreatedAtComparator : Comparator { override fun compare( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/LatestByKindAndAuthor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/LatestByKindAndAuthor.kt index 27b60d00d..dfc9ea1ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/LatestByKindAndAuthor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/LatestByKindAndAuthor.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.observables -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/LatestByKindWithETag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/LatestByKindWithETag.kt index 895c5aed6..c21514186 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/LatestByKindWithETag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/LatestByKindWithETag.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.model.observables -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountPreferenceStores.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountPreferenceStores.kt index de669056d..16f246fdc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountPreferenceStores.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountPreferenceStores.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.preferences -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.datastore.core.DataStore diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountSecretsEncryptedStores.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountSecretsEncryptedStores.kt index 2255bf22e..5e25514c4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountSecretsEncryptedStores.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountSecretsEncryptedStores.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.preferences -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.datastore.preferences.core.PreferenceDataStoreFactory diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/DataStoreExt.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/DataStoreExt.kt index 626fa8d28..486d75054 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/DataStoreExt.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/DataStoreExt.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.preferences -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.datastore.core.DataStore diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/EncryptedDataStore.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/EncryptedDataStore.kt index f4ce0e86d..b57efd277 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/EncryptedDataStore.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/EncryptedDataStore.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.preferences -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.datastore.core.DataStore diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/KeyStoreEncryption.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/KeyStoreEncryption.kt index 5fcb34b73..426122328 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/KeyStoreEncryption.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/KeyStoreEncryption.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.preferences -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import android.os.Build diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/TorSharedPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/TorSharedPreferences.kt index a95121165..8d78be2e6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/TorSharedPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/TorSharedPreferences.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.preferences -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import android.content.Context diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt index 65d9f2419..bf29638a1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.preferences -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import android.content.Context diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UpdatablePropertyFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UpdatablePropertyFlow.kt index 5773ba92d..30e7d254b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UpdatablePropertyFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UpdatablePropertyFlow.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.preferences -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import kotlinx.coroutines.CoroutineScope diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/EmptyRoleBasedHttpClientBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/EmptyRoleBasedHttpClientBuilder.kt index a7a0c25e0..cafb9a503 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/EmptyRoleBasedHttpClientBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/EmptyRoleBasedHttpClientBuilder.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.privacyOptions -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import okhttp3.OkHttpClient diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/IRoleBasedHttpClientBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/IRoleBasedHttpClientBuilder.kt index 676495876..923a6a6ea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/IRoleBasedHttpClientBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/IRoleBasedHttpClientBuilder.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.privacyOptions -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import okhttp3.OkHttpClient diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/RoleBasedHttpClientBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/RoleBasedHttpClientBuilder.kt index 647939c94..b2ce4d961 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/RoleBasedHttpClientBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/RoleBasedHttpClientBuilder.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.privacyOptions -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/SingleRoleBasedHttpClientBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/SingleRoleBasedHttpClientBuilder.kt index 6d632dfdb..4cc97b3d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/SingleRoleBasedHttpClientBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/SingleRoleBasedHttpClientBuilder.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.privacyOptions -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import okhttp3.OkHttpClient diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt index 01a2b82c1..9219c2b92 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.model.privateChats import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.NotesGatherer -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NotesGatherer +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.amethyst.ui.dal.ListChange import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt index 51d62b621..6a81dcf37 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.privateChats -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt index 645f6ef0b..60ecab35f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.serverList -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt index 7ac69e56f..144d66e5e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.serverList -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithIndexRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithIndexRelayListsState.kt index f026f6927..d9a5baee7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithIndexRelayListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithIndexRelayListsState.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.serverList -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithSearchRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithSearchRelayListsState.kt index 2b676ff42..fbf114c56 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithSearchRelayListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithSearchRelayListsState.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.serverList -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedServerListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedServerListState.kt index 2a1ac20bf..b0df8e49e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedServerListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedServerListState.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.serverList -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip96FileStorage.FileStorageServerListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt index d6ca458a4..08e3b7ae2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.serverList -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt index 0166c4816..bcc5ca1b1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds -import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedDecryptionCaches.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedDecryptionCaches.kt index 770973a13..b8d8bc64e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedDecryptionCaches.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedDecryptionCaches.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListDecryptionCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt index 403e450f2..438d49a7e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.ALL_FOLLOWS diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedFlowsType.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedFlowsType.kt index b7d296455..e0cb002d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedFlowsType.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedFlowsType.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import kotlinx.coroutines.flow.Flow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavFilter.kt index 3a6d40678..e0b393c58 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilter.kt index fe2d3c196..1ad010bcf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User interface IFeedTopNavPerRelayFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilterSet.kt index acc4c695e..47281520d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilterSet.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User interface IFeedTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/MergedTopFeedAuthorListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/MergedTopFeedAuthorListsState.kt index 2d266b774..f26abeae7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/MergedTopFeedAuthorListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/MergedTopFeedAuthorListsState.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxLoaderState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxLoaderState.kt index 581dce286..21086ff07 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxLoaderState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxLoaderState.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt index 844c224f7..eea9ec4f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.Constants import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt index da1471e7c..210e37a7a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.allFollows -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByProxyTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByProxyTopNavFilter.kt index d3ae11da3..7346426e8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByProxyTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByProxyTopNavFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.allFollows -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt index 69351963c..2c0915993 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.allFollows -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.serverList.MergedFollowListsState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilter.kt index 71346da77..4bc3ac67b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.allFollows -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilterSet.kt index 33e9be694..e1aa10b63 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilterSet.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.allFollows -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByOutboxTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByOutboxTopNavFilter.kt index 521b6419d..dab91dcd4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByOutboxTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByOutboxTopNavFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByProxyTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByProxyTopNavFilter.kt index 3f614020f..234d7827f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByProxyTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByProxyTopNavFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsFeedFlow.kt index 36165e204..e8304833a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsFeedFlow.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.serverList.MergedFollowListsState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/Kind3UserFollowsFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/Kind3UserFollowsFeedFlow.kt index c1ed9719c..9d65a8d53 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/Kind3UserFollowsFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/Kind3UserFollowsFeedFlow.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip02FollowLists.Kind3FollowListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeExpander.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeExpander.kt index d783b0bea..2df4003e8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeExpander.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeExpander.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.fonfon.kgeohash.GeoHash diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt index 7b3d6a354..4a2adc1e1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavFilter.kt index ce972e38c..906c9ad4c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilter.kt index f159ebbc1..96b494d71 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilterSet.kt index dca520ed7..62b41fbb4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilterSet.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalFeedFlow.kt index 1d9e8cc24..ee3598f96 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalFeedFlow.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.global -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavFilter.kt index ad833830d..becf80f9e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.global -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilter.kt index abc66908a..83c22cb78 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.global -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilterSet.kt index fa9faba4c..ffa6dd324 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilterSet.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.global -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavFilter.kt index f838a927f..82479f03e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.hashtag -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilter.kt index 8ce3574df..faf4ddd18 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.hashtag -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilterSet.kt index bf02de7b0..b20c4ba0f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilterSet.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.hashtag -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/NoteFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/NoteFeedFlow.kt index 13bf07015..60a132e31 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/NoteFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/NoteFeedFlow.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased -import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.topNavFeeds.FeedDecryptionCaches import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavFilter.kt index 1d58491ca..32025dd58 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilter.kt index 5a9c9129e..89630ad58 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilterSet.kt index 476a9a810..ed872dd0c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilterSet.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavFilter.kt index 479fe0aa5..4c5b2afa7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByProxyTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByProxyTopNavFilter.kt index 04bba033f..dee3812c2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByProxyTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByProxyTopNavFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilter.kt index 87648ab77..bc34fca89 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilterSet.kt index 231fe5169..9863462a4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilterSet.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavFilter.kt index 9fb08bed1..48bfbebe2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilter.kt index 365be26c4..1d37ab727 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilterSet.kt index bc4b71603..66fbe8e55 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilterSet.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavFilter.kt index f042111da..e9341f8e7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByProxyTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByProxyTopNavFilter.kt index f3898f01b..dabeba417 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByProxyTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByProxyTopNavFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilter.kt index 5636079a2..f0f70ca4f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilterSet.kt index d7d142889..f40d71441 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilterSet.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownFeedFlow.kt index 35d9443c4..ab87abc1a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownFeedFlow.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.unknown -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavFilter.kt index e87515a19..b1a63ee0c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.unknown -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import androidx.compose.runtime.Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavPerRelayFilterSet.kt index bbcbfa87b..db84c57c1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavPerRelayFilterSet.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.unknown -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/AccountsTorStateConnector.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/AccountsTorStateConnector.kt index 9102c4f00..2ee648a0c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/AccountsTorStateConnector.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/AccountsTorStateConnector.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.torState -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayEvaluation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayEvaluation.kt index 6205e3a74..30831d901 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayEvaluation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayEvaluation.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.torState -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.tor.TorType diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelaySettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelaySettings.kt index 50cb79dbb..290139085 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelaySettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelaySettings.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.torState -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.tor.TorType diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt index 56b60479f..80835db30 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.torState -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListDecryptionCache.kt index e21f97957..3ffc7f6ae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListDecryptionCache.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.trustedAssertions -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListState.kt index aad3082bd..b00c0c70f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListState.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.trustedAssertions -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.commons.model.trustedAssertions.TrustProviderListState as ITrustProviderListState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt index fe31f0f5a..f876d22a5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.service import android.content.Context import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt index a592e9749..3087ef3ec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt @@ -26,7 +26,7 @@ import androidx.core.content.ContextCompat import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendDMNotification diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt index 4d191464e..2d3086dc8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt index 8540f32ca..ff2b8411a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.EOSEAccountKey import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt index 8daa532c0..3b5583f38 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountObservers.kt index a27671530..6afa547d7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountObservers.kt @@ -24,7 +24,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/drafts/AccountDraftsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/drafts/AccountDraftsEoseManager.kt index 63d48ff95..0487a503c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/drafts/AccountDraftsEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/drafts/AccountDraftsEoseManager.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.drafts -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt index 1280b6ac3..978f7f376 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/FilterFindFollowMetadataForKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/FilterFindFollowMetadataForKey.kt index be61bf554..a2d843594 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/FilterFindFollowMetadataForKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/FilterFindFollowMetadataForKey.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Constants import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt index c4da16643..4b7c90f8c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromInboxRelaysManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromInboxRelaysManager.kt index 2e901816d..c4dd64399 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromInboxRelaysManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromInboxRelaysManager.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromRandomRelaysManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromRandomRelaysManager.kt index 6198600d7..34074f380 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromRandomRelaysManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromRandomRelaysManager.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt index dde939631..7771c04a4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip59GiftWraps -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt index cad055057..e66870d7c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt @@ -24,7 +24,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.ChannelState import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt index b11c8b81c..0aacd5cf8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.NoteEventLoaderSubAssembler diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssemblerSubscription.kt index 9d29e0a8f..73ca743a4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssemblerSubscription.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventObservers.kt index 6ea96fbe3..50cdad965 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventObservers.kt @@ -24,9 +24,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.NoteState -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt index c62cada62..ebea6dd10 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders -import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState import com.vitorpamplona.quartz.nip01Core.core.Address diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt index 9c38810dd..b8495362a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt index 948abb47f..ff3f946a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToAddresses.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToAddresses.kt index 80be5df1d..5e5b5343b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToAddresses.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToAddresses.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers -import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToNotes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToNotes.kt index 477dbafe6..dd76df8f6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToNotes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToNotes.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt index 082148491..fbb130e33 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc import android.annotation.SuppressLint import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt index 1817971a3..967c78ddb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssemblerSubscription.kt index e63a31d0e..1fee4ac5f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssemblerSubscription.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user import android.annotation.SuppressLint import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt index 4116ff41c..11c1f728f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt @@ -25,10 +25,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.NoteState -import com.vitorpamplona.amethyst.commons.model.User -import com.vitorpamplona.amethyst.commons.model.UserState +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserOutboxFinderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserOutboxFinderSubAssembler.kt index 0e2baeb13..07864e04b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserOutboxFinderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserOutboxFinderSubAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.BaseEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows.pickRelaysToLoadUsers diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt index 66f27b124..d718bd403 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt index 9222d1bc8..79ac83b98 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.commons.model.toHexSet import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt index e0c0da81f..d23a370a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.commons.model.toHexSet import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt index 0c1a76916..0e5cdea78 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.BaseEoseManager diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByEvent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByEvent.kt index 2cf8dac3b..5401aeaa1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByEvent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByEvent.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies -import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingEvents import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.potentialRelaysToFindEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt index bceb9c3d1..581e3e099 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt @@ -21,38 +21,13 @@ package com.vitorpamplona.amethyst.service.relays import androidx.collection.LruCache -import com.vitorpamplona.amethyst.commons.model.User -import com.vitorpamplona.amethyst.service.relays.MutableTime +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -typealias SincePerRelayMap = MutableMap - -class EOSERelayList { - var relayList: SincePerRelayMap = mutableMapOf() - - fun addOrUpdate( - relayUrl: NormalizedRelayUrl, - time: Long, - ) { - val eose = relayList[relayUrl] - if (eose == null) { - relayList[relayUrl] = MutableTime(time) - } else { - eose.updateIfNewer(time) - } - } - - fun clear() { - relayList = mutableMapOf() - } - - fun since() = relayList - - fun newEose( - relay: NormalizedRelayUrl, - time: Long, - ) = addOrUpdate(relay, time) -} +// Re-export from commons for backwards compatibility +typealias EOSERelayList = com.vitorpamplona.amethyst.commons.relays.EOSERelayList +typealias SincePerRelayMap = com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap +typealias MutableTime = com.vitorpamplona.amethyst.commons.relays.MutableTime open class EOSEByKey( cacheSize: Int = 200, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index 8e05d7c18..a3570e4a1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -73,7 +73,7 @@ import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.viewmodel.compose.viewModel import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.service.playback.composable.VideoView import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt index 13402c08d..66bba4802 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt @@ -31,8 +31,8 @@ import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.uploads.MediaCompressor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt index 710352eeb..40ce38396 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.ui.actions import androidx.compose.runtime.Immutable -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt index 20a377827..f0c758bf3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt @@ -54,8 +54,8 @@ import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ReusableZapButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ReusableZapButton.kt index b1eee5f3a..5e1dbae50 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ReusableZapButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ReusableZapButton.kt @@ -45,8 +45,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 16e23e73a..4951569b2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -62,8 +62,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em import com.vitorpamplona.amethyst.commons.compose.produceCachedState import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.commons.richtext.Base64Segment import com.vitorpamplona.amethyst.commons.richtext.BechSegment import com.vitorpamplona.amethyst.commons.richtext.CashuSegment diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt index c4cda359b..540e2d2fd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt @@ -50,7 +50,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt index 336b1b6ce..222a916a3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt @@ -33,7 +33,7 @@ import androidx.compose.ui.unit.dp import com.halilibo.richtext.ui.MediaRenderer import com.halilibo.richtext.ui.string.InlineContent import com.halilibo.richtext.ui.string.RichTextString -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.HashtagIcon diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/ToastManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/ToastManager.kt index 3377c9dd6..4e10e8ea3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/ToastManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/ToastManager.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.components.toasts -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.components.toasts.multiline.MultiErrorToastMsg import com.vitorpamplona.amethyst.ui.components.toasts.multiline.UserBasedErrorMessage import kotlinx.coroutines.flow.MutableStateFlow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt index 203878a30..5ea2242d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt @@ -42,7 +42,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiErrorToastMsg.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiErrorToastMsg.kt index c487a7e3f..7bf27518d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiErrorToastMsg.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiErrorToastMsg.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.components.toasts.multiline import androidx.compose.runtime.Immutable -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.components.toasts.ToastMsg import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt index 798586a3c..9df88cb8c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt @@ -31,7 +31,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DefaultFeedOrder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DefaultFeedOrder.kt index dc2cec049..72f1e65b8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DefaultFeedOrder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DefaultFeedOrder.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.dal -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.Card val DefaultFeedOrder: Comparator = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt index 363e367cb..7edc16ab3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.feeds import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.service.BundledInsert import com.vitorpamplona.amethyst.service.BundledUpdate diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedState.kt index 49ec6b74d..c6e831f75 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedState.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.feeds import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.MutableStateFlow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt index 341240e05..b77230fc3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt @@ -57,7 +57,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPicture diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt index f5982f646..452944cbf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt @@ -92,7 +92,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserContactCardsFollowerCount diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt index c0d5578b1..247f29c5a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.navigation.routes -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt index f401e5060..163011d9c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt @@ -57,7 +57,7 @@ import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.ui.components.LoadingAnimation diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt index 0281a36ef..d472fa4b8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt @@ -36,7 +36,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlockReportChecker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlockReportChecker.kt index 91e57fa3f..bdcfa1d94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlockReportChecker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlockReportChecker.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/DisplayAuthorBanner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/DisplayAuthorBanner.kt index 81dbf2aa8..81de3a9ea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/DisplayAuthorBanner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/DisplayAuthorBanner.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.note import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.note.elements.BannerImage import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.SimpleImageBorder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Gallery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Gallery.kt index e68bc3dd2..3bbf5b382 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Gallery.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Gallery.kt @@ -35,7 +35,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.navigation.routes.routeForUser diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt index f626fdb03..50b302933 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt @@ -32,9 +32,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index 5b4905fb2..100db2569 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -63,9 +63,9 @@ import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.NoteState -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.CachedRichTextParser import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserContactCardsScore import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPicture diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt index 87e46cc26..48ad80fbf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt @@ -51,9 +51,9 @@ import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons import com.vitorpamplona.amethyst.commons.hashtags.Tunestr -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserNip05 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index c33ef3c27..6d152f6ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -53,8 +53,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.produceCachedStateAsync -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelPicture import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeCommunityApprovalNeedStatus diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt index 220e4c33a..070403e87 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -77,9 +77,9 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Popup import androidx.core.graphics.ColorUtils import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeEditDraftTo import com.vitorpamplona.amethyst.ui.painterRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt index 505096383..a3a31d5f9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt @@ -71,8 +71,8 @@ import androidx.compose.ui.window.Popup import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteZaps diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt index 6f08d5334..dffa2b764 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt @@ -26,8 +26,8 @@ import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 7c9e5fd2e..279a3414f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -99,8 +99,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteReactionCount import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteReactions diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt index 94d766a1b..7b21446da 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt @@ -40,7 +40,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.RelayInfo +import com.vitorpamplona.amethyst.model.RelayInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ButtonBorder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt index f36c21c7d..6fe9311f2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt @@ -50,7 +50,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt index eace08cc7..43bbc572c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt @@ -50,7 +50,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt index e0ce4bdab..d3392b4f4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt @@ -34,8 +34,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt index c2c21aae1..73dbee486 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt @@ -74,7 +74,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder -import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.firstFullChar import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEventAndMapNotNull diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt index 5122faacb..f5670dac8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt @@ -31,7 +31,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenUser import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowing import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index 161daffd3..06a62f2f9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -45,8 +45,8 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserContactCardsScore import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt index 8429bc2c3..0c8a4a4ef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt @@ -34,8 +34,8 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.lifecycle.LifecycleOwner -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.service.tts.TextToSpeechHelper diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/WatchNoteEvent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/WatchNoteEvent.kt index ddc84af33..d6f82836b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/WatchNoteEvent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/WatchNoteEvent.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteHasEvent import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt index aa6204e3d..3cc9df1ef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt @@ -64,8 +64,8 @@ import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.ui.components.TextSpinner diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt index 343efdce0..1373a92a7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt @@ -39,8 +39,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserAboutMe diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/InvoiceRequest.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/InvoiceRequest.kt index 2027a4826..59baf599e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/InvoiceRequest.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/InvoiceRequest.kt @@ -52,7 +52,7 @@ import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons import com.vitorpamplona.amethyst.commons.hashtags.Lightning -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/Notifying.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/Notifying.kt index f46752530..519ebea28 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/Notifying.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/Notifying.kt @@ -37,7 +37,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUser import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/ShowUserSuggestionList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/ShowUserSuggestionList.kt index 45e52219e..e5afc82bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/ShowUserSuggestionList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/ShowUserSuggestionList.kt @@ -39,7 +39,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.searchCommand.UserSearchDataSourceSubscription import com.vitorpamplona.amethyst.ui.note.AboutDisplay diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/UserSuggestionState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/UserSuggestionState.kt index e895d2b0c..922f41638 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/UserSuggestionState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/UserSuggestionState.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.note.creators.userSuggestions import androidx.compose.runtime.Stable import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relayClient.searchCommand.SearchQueryState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/IZapField.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/IZapField.kt index 88a9e1c44..9a85f6178 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/IZapField.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/IZapField.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.note.creators.zapsplits import androidx.compose.runtime.MutableState import androidx.compose.ui.text.input.TextFieldValue -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User interface IZapField { val forwardZapTo: MutableState> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/SplitConversor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/SplitConversor.kt index b4d34e140..b5e8f9967 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/SplitConversor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/SplitConversor.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.note.creators.zapsplits -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup import kotlin.math.round diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt index 0af785fd0..7d63d9c9e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt @@ -29,8 +29,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.blur import androidx.compose.ui.layout.ContentScale import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserBanner import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.note.BaseUserPicture diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt index 384c7ce45..e60b4fc4b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt @@ -26,7 +26,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.text.style.TextOverflow -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.ClickableTextColor import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt index 8e864a6ca..b0300c708 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt @@ -33,7 +33,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.text.style.TextOverflow import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.ClickableTextColor import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt index 0ac4ea33a..5cc1d6b3e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt @@ -30,7 +30,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.buildLinkString import com.vitorpamplona.amethyst.ui.note.LoadOts import com.vitorpamplona.amethyst.ui.note.timeAgoNoDot diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt index 12db8096e..e26577355 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt @@ -55,7 +55,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteReplies import com.vitorpamplona.amethyst.ui.components.ClickableTextColor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt index 7275b9922..a15f4c944 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt @@ -40,8 +40,8 @@ import androidx.compose.ui.text.AnnotatedString import androidx.core.content.ContextCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserBookmarks import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserFollows import com.vitorpamplona.amethyst.ui.actions.EditPostView diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt index 76dd023ac..b8fa57650 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt @@ -32,7 +32,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextOverflow import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUser import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt index 6c565784d..9e224b999 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt @@ -31,7 +31,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.note.timeAgo import com.vitorpamplona.amethyst.ui.note.timeAgoShort import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index d227e9a5f..445887199 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -33,8 +33,8 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt index 865d473db..6f5d1e758 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt @@ -48,7 +48,7 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt index b1086dd68..1d93ba573 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt @@ -55,7 +55,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.ui.components.ClickableTextPrimary import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt index 52aa7c46a..cb37c17cc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt @@ -41,8 +41,8 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.playback.composable.LoadThumbAndThenVideoView import com.vitorpamplona.amethyst.service.playback.composable.VideoView import com.vitorpamplona.amethyst.service.playback.composable.WaveformData diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Badge.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Badge.kt index 30e000371..7b770f447 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Badge.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Badge.kt @@ -44,8 +44,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.UserPicture diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChannelMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChannelMessage.kt index 3818cb4c1..9709ac890 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChannelMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChannelMessage.kt @@ -29,7 +29,7 @@ import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessage.kt index 85f98b592..3c75950cd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessage.kt @@ -31,7 +31,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessageEncryptedFile.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessageEncryptedFile.kt index ee12d7bb8..0b2094448 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessageEncryptedFile.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessageEncryptedFile.kt @@ -32,7 +32,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt index b05fb29d6..48bfa2d6b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt @@ -38,7 +38,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid import com.vitorpamplona.amethyst.ui.components.ZoomableContentView diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt index f294957d8..115431784 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt @@ -60,9 +60,9 @@ import androidx.compose.ui.unit.sp import androidx.core.content.ContextCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.components.RichTextViewer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Emoji.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Emoji.kt index 24bc5b5bc..0e03df161 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Emoji.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Emoji.kt @@ -46,7 +46,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt index 9d5ad038e..f456c26f4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt @@ -25,7 +25,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.layout.ContentScale -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt index e0e1026ff..f91a1d3d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.layout.ContentScale import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent import com.vitorpamplona.amethyst.commons.richtext.MediaLocalImage import com.vitorpamplona.amethyst.commons.richtext.MediaLocalVideo diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt index ddba83c13..5e1c1e1de 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt @@ -41,7 +41,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEventAndMap import com.vitorpamplona.amethyst.ui.components.MyAsyncImage diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt index 2ddbb08d6..52ba58254 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt @@ -40,8 +40,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.components.ClickableUrl import com.vitorpamplona.amethyst.ui.components.SensitivityWarning diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt index eec42d61e..f20b58ed8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt @@ -38,8 +38,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.ui.components.ClickableTextPrimary diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/InteractiveStory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/InteractiveStory.kt index ecf2df323..08afdcf5b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/InteractiveStory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/InteractiveStory.kt @@ -37,8 +37,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt index 7426b3d63..990fd77ad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt @@ -45,8 +45,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.playback.composable.VideoView import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt index 4b6480890..fb8822831 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt @@ -29,7 +29,7 @@ import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LoadLiveActivityChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt index 1e1ee0020..9a4461dac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt @@ -36,7 +36,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.BaseUserPicture diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MedicalData.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MedicalData.kt index 134b5bd57..8deff3805 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MedicalData.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MedicalData.kt @@ -39,7 +39,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Bundle import com.vitorpamplona.amethyst.model.FhirElementDatabase import com.vitorpamplona.amethyst.model.LensSpecification diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt index b94dc1389..734c2fde2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90Status.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90Status.kt index 73839665f..f4ce7b415 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90Status.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90Status.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.note.types import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip90Dvms.NIP90StatusEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt index 3975b6aaf..8d818b56c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt @@ -47,8 +47,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.components.ShowMoreButton import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.UserCompose diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PictureDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PictureDisplay.kt index 496656ec8..a2677bff3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PictureDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PictureDisplay.kt @@ -35,7 +35,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid import com.vitorpamplona.amethyst.ui.components.SensitivityWarning diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PinList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PinList.kt index 56c522aed..afc871c08 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PinList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PinList.kt @@ -45,7 +45,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.ShowMoreButton import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt index e617a686f..dd8d8e2c9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt @@ -32,7 +32,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt index b12fe3f97..583a22ab7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt @@ -35,7 +35,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PublicMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PublicMessage.kt index f054f1282..78f0fedd1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PublicMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PublicMessage.kt @@ -33,8 +33,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.components.RenderUserAsClickableText import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Reaction.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Reaction.kt index dad53adef..eb2a23085 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Reaction.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Reaction.kt @@ -25,7 +25,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt index 425061ab2..df050701a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt @@ -45,7 +45,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.RelayListCard import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserRelayIntoList import com.vitorpamplona.amethyst.ui.components.ShowMoreButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderPostApproval.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderPostApproval.kt index 7828fb641..3a3f33615 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderPostApproval.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderPostApproval.kt @@ -34,7 +34,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.note.NoteCompose diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Report.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Report.kt index 0977b6311..5a861de6a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Report.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Report.kt @@ -27,7 +27,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.NoteCompose diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt index 200324c06..b3672d396 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt @@ -33,7 +33,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.SensitivityWarning diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt index 3e9e73eeb..efa188f01 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt @@ -44,7 +44,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.ui.actions.EditPostView import com.vitorpamplona.amethyst.ui.components.GenericLoadable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt index 5694fe9e8..afddc9844 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt @@ -49,7 +49,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.net.toUri import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.countToHumanReadableBytes import com.vitorpamplona.amethyst.ui.components.ShowMoreButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt index b95999e11..ece2433af 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt @@ -44,7 +44,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.countToHumanReadableBytes import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt index ac780446d..b21a24bec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt @@ -42,7 +42,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VideoDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VideoDisplay.kt index 888435d37..eecd04680 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VideoDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VideoDisplay.kt @@ -25,7 +25,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.layout.ContentScale -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt index 62a65891f..0a0210fb0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt @@ -53,7 +53,7 @@ import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi import androidx.media3.ui.PlayerView import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.playback.composable.GetVideoController import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState import com.vitorpamplona.amethyst.service.playback.composable.WaveformData diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Wiki.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Wiki.kt index 1fd46ebde..e6f53b753 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Wiki.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Wiki.kt @@ -37,7 +37,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index 85b99c659..8ded3eeea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.feeds.FeedContentState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt index b7cda7519..573ad92ee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt @@ -24,7 +24,7 @@ import android.content.Context import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.ALL_FOLLOWS import com.vitorpamplona.amethyst.model.ALL_USER_FOLLOWS import com.vitorpamplona.amethyst.model.AROUND_ME diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedState.kt index 56998320f..a1c984997 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedState.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.MutableStateFlow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt index 9abd56ac1..4c6624a12 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt @@ -25,7 +25,7 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.BundledUpdate import com.vitorpamplona.amethyst.service.checkNotInMainThread diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt index 7f722adaf..31813b631 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedContentState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index ff076ee83..fccc7eba7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -40,9 +40,9 @@ import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.GenericBaseCache import com.vitorpamplona.amethyst.commons.compose.GenericBaseCacheAsync -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AccountSettings diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt index 658ef4f31..7f591df71 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.privateChats.ChatroomList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/ArticleListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/ArticleListView.kt index bf04e8d76..2280da6c1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/ArticleListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/ArticleListView.kt @@ -31,7 +31,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt index 1b99f3fa6..78cc44273 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt @@ -37,7 +37,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.AnnotatedString import androidx.core.content.ContextCompat import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.actions.EditPostView import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.components.GenericLoadable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/PostListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/PostListView.kt index 2088876e1..93562c8ef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/PostListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/PostListView.kt @@ -31,7 +31,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/ArticleBookmarkListManagementScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/ArticleBookmarkListManagementScreen.kt index 793296a0c..ec36b977e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/ArticleBookmarkListManagementScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/ArticleBookmarkListManagementScreen.kt @@ -36,7 +36,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt index 0a8525835..14bb6478d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt @@ -36,7 +36,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPrivateFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPrivateFeedFilter.kt index 764872f0b..489334ea3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPrivateFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPrivateFeedFilter.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.dal -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.dal.FeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPublicFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPublicFeedFilter.kt index 6a1dd0213..561af0e2c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPublicFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPublicFeedFilter.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.dal -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.dal.FeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt index 7f5941f24..d411332a1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt @@ -30,7 +30,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt index 04371a8f0..f06c49cff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt @@ -42,7 +42,7 @@ import androidx.compose.ui.Alignment.Companion.CenterStart import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt index 6530d6f8b..2a6f7e060 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt @@ -25,7 +25,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.note.timeAgoShort import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Font12SP diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt index c13e7f11a..bd2a75c42 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt @@ -25,8 +25,8 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/NewDateOrSubjectDivisor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/NewDateOrSubjectDivisor.kt index d0364a129..b7c23d1e7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/NewDateOrSubjectDivisor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/NewDateOrSubjectDivisor.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.note.dateFormatter import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts.ChatDivisor import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChangeChannelMetadataNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChangeChannelMetadataNote.kt index b78224194..84e9a7d29 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChangeChannelMetadataNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChangeChannelMetadataNote.kt @@ -24,7 +24,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt index 18f55cbd2..1e5393762 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt @@ -48,7 +48,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Constants import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderDraftEvent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderDraftEvent.kt index de4b34c48..99a5d2031 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderDraftEvent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderDraftEvent.kt @@ -24,7 +24,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.ui.graphics.Color -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.ObserveDraftEvent import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt index 79703d3be..1cc768530 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt @@ -30,7 +30,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlVideo diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt index f9457b866..f704649b7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/IncognitoBadge.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/IncognitoBadge.kt index dd1e4beca..a7f7444e8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/IncognitoBadge.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/IncognitoBadge.kt @@ -25,7 +25,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.theme.IncognitoIconModifier import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedFilter.kt index a8c9cd27c..34f8e9ba5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedFilter.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.dal -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.ChangesFlowFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt index 7710250a6..13a3f2616 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt @@ -24,7 +24,7 @@ import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.dal.ChangesFlowFilter import com.vitorpamplona.amethyst.ui.dal.ListChange diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/ChatroomHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/ChatroomHeader.kt index 513062c35..8f062f1b1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/ChatroomHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/ChatroomHeader.kt @@ -31,7 +31,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures import com.vitorpamplona.amethyst.ui.note.UsernameDisplay diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt index c741d2f1b..e5f0a008f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt @@ -29,7 +29,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserShortName import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index f1aac6ce7..66af69f87 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -32,8 +32,8 @@ import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt index 0e345b8dc..822507e72 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt index dafd65425..2a8adefca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt @@ -30,7 +30,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt index d0b62168c..70d6d7f45 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt @@ -24,7 +24,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt index 9cdd0c712..89f03d6b4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt @@ -30,7 +30,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt index 4b7da5aa6..c5796b7ae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt @@ -24,7 +24,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt index d11109d47..5c14da323 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt @@ -29,7 +29,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt index db194010d..67ce16968 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt @@ -25,7 +25,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.OnlineChecker import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt index 1eea430e7..469b001b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt @@ -40,7 +40,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt index dca0c690e..0f496f606 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt @@ -36,7 +36,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index f4e784e93..abc3d01ff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -33,8 +33,8 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Channel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index a1c07fa7c..86d975767 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt @@ -41,8 +41,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt index d63a4463e..4483051ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.replace diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt index c3ae917a4..82a3423d1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.replace import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt index 2620d9c94..5a5eae521 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsFromMe.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsFromMe.kt index c5f2f4236..e32888b58 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsFromMe.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsFromMe.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToMe.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToMe.kt index 0b96093d5..2488bed77 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToMe.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToMe.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt index 2d9cea248..4a733cce0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt index 298bdeed0..405a22c9a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/DisplayReplyingToNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/DisplayReplyingToNote.kt index 6ac23bb2b..cecbca8e0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/DisplayReplyingToNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/DisplayReplyingToNote.kt @@ -36,7 +36,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.ChatroomMessageCompose diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt index 9704c09e0..e1d8cc2fe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt @@ -44,7 +44,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/NewCommunityNoteButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/NewCommunityNoteButton.kt index aec9a2d97..12e14fd8d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/NewCommunityNoteButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/NewCommunityNoteButton.kt @@ -27,7 +27,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedFilter.kt index 119d13b08..f354dd807 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedFilter.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.dal -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedViewModel.kt index 973a200ad..32e32387c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedViewModel.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.screen.FeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedFilter.kt index dd5f5bf5e..cd8b2efd7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedFilter.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.dal -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedViewModel.kt index 8d8a2fbcf..449407ae2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedViewModel.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.screen.FeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssembler.kt index 187154ca2..531a781bd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource -import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssemblerSubscription.kt index bc19a824f..ed4f4f3f1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssemblerSubscription.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt index 52402fc1d..100ab2740 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt @@ -25,7 +25,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.CheckHiddenFeedWatchBlockAndReport diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsSetsAndLiveStreamsSubAssembler2.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsSetsAndLiveStreamsSubAssembler2.kt index c1cb7c1ef..eacc12b13 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsSetsAndLiveStreamsSubAssembler2.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsSetsAndLiveStreamsSubAssembler2.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.makeFollowSetsFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryLongFormClassifiedsAndDVMSubAssembler1.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryLongFormClassifiedsAndDVMSubAssembler1.kt index affd4e77f..0ba4eb8ed 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryLongFormClassifiedsAndDVMSubAssembler1.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryLongFormClassifiedsAndDVMSubAssembler1.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.makeLongFormFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryPublicChatsAndCommunitiesSubAssembler3.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryPublicChatsAndCommunitiesSubAssembler3.kt index 296374d91..c5a38b8f7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryPublicChatsAndCommunitiesSubAssembler3.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryPublicChatsAndCommunitiesSubAssembler3.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.makePublicChatsFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt index cda62f410..050f48e9f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.filterIntoSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormCard.kt index 779cb8deb..317275f2e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormCard.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.types.LongFormHeader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt index fd46c64d4..4e4f9fd7c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt index a58549ba8..027e32d40 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt @@ -41,8 +41,8 @@ import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import coil3.compose.AsyncImagePainter import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt index 70e816d26..40a4a5b12 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.filterIntoSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt index 7a7649b8b..4d12421ef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt @@ -38,7 +38,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap import com.vitorpamplona.amethyst.ui.components.MyAsyncImage diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt index f19dd82bb..c96f13be4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.ParticipantListBuilder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt index 43a08ecfa..4b56a6acb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt @@ -41,8 +41,8 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt index 3ec7ecda0..be38006fa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt @@ -43,8 +43,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt index f064c8f04..6890729d8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.mapNotNullIntoSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt index 56136b282..34a00ce15 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt @@ -42,7 +42,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt index c476adadc..d8d47a564 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.ParticipantListBuilder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt index a927e4ae8..df2a99c3a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt @@ -44,7 +44,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/DiscoverMarketplaceFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/DiscoverMarketplaceFeedFilter.kt index c58b517e7..8153489b3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/DiscoverMarketplaceFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/DiscoverMarketplaceFeedFilter.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.filterIntoSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt index e2183c242..4128694c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt @@ -47,7 +47,7 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index cd1a6a429..b762be777 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -33,8 +33,8 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/dal/DraftEventsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/dal/DraftEventsFeedFilter.kt index fc5ec00bb..b7e17df5d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/dal/DraftEventsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/dal/DraftEventsFeedFilter.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.dal -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.filterIntoSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt index 2dde71441..80826edec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt @@ -50,8 +50,8 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap import com.vitorpamplona.amethyst.ui.components.LoadNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryResponseFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryResponseFilter.kt index 00a039916..24f169ff4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryResponseFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryResponseFilter.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.dal -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.observables.CreatedAtComparator diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt index 082e9c81b..ecefa80a5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt @@ -54,7 +54,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.lifecycle.viewmodel.compose.viewModel import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedFilter.kt index 610ec1620..ffcbc86fb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedFilter.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows.AllUserFollowsByOutboxTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedViewModel.kt index f8ea4ecc8..680c1b089 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedViewModel.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.screen.FeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt index 6facf7555..29fc09689 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.filterIntoSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedViewModel.kt index 3c1041fec..1b4d05062 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedViewModel.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.screen.FeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersFeedFilter.kt index 8aab5aaca..64068dd08 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersFeedFilter.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache.checkGetOrCreateUser import com.vitorpamplona.amethyst.ui.dal.FeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersUserFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersUserFeedViewModel.kt index faf545117..97269984b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersUserFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersUserFeedViewModel.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterAssembler.kt index 1b3e1e636..1f43ad014 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterAssembler.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.datasource import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterAssemblerSubscription.kt index d61fa1e26..c30204f15 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterAssemblerSubscription.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.datasourc import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt index dba994980..03597a7b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt index 080f29df6..0d9e13dd3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.dal -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt index b1c0b99d3..669cc05fc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt index ce733de72..f72519b80 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.dal -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 580f9f1fb..504c723be 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -56,7 +56,7 @@ import androidx.compose.ui.unit.dp import androidx.core.util.Consumer import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.RecordVoiceButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index f8dc572fd..cf9652a1e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -36,8 +36,8 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState.EmojiMedia diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt index fcf3d073e..faa5d677a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt @@ -28,7 +28,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.service.uploads.UploadingState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeConversationsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeConversationsFeedFilter.kt index 0f8d97b89..4c7ba220c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeConversationsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeConversationsFeedFilter.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt index 773631b62..4e3829fda 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.LocalCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt index c30a5b7c2..f71c6510a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.filterIntoSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt index ea175412c..6646fa1ad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/PeopleListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/PeopleListView.kt index e09cf6bea..16560c968 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/PeopleListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/PeopleListView.kt @@ -35,7 +35,7 @@ import androidx.compose.material3.OutlinedIconButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.UserComposeNoAction import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/ShowUserSuggestions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/ShowUserSuggestions.kt index c424a7928..ef7627709 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/ShowUserSuggestions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/ShowUserSuggestions.kt @@ -63,7 +63,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.searchCommand.UserSearchDataSourceSubscription import com.vitorpamplona.amethyst.ui.note.AboutDisplay diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListScreen.kt index 66cbea473..95c5e9885 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListScreen.kt @@ -71,8 +71,8 @@ import androidx.core.content.ContextCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListViewModel.kt index 9a54ec8cf..4c66116eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListViewModel.kt @@ -26,8 +26,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackScreen.kt index 7ab4aca1a..d1eda7039 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackScreen.kt @@ -64,8 +64,8 @@ import androidx.core.content.ContextCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackViewModel.kt index 185bcd754..6d5afad15 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackViewModel.kt @@ -26,8 +26,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/PeopleListItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/PeopleListItem.kt index 1e0df3812..3cd843690 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/PeopleListItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/PeopleListItem.kt @@ -55,7 +55,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleList import com.vitorpamplona.amethyst.ui.components.ClickableBox diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserScreen.kt index a63889e48..9563ac307 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserScreen.kt @@ -36,7 +36,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserView.kt index 3d4b7c929..2332c2559 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserView.kt @@ -37,7 +37,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt index 77f44f552..ddc90aac7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt @@ -24,8 +24,8 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedState.kt index 161a186c3..776841efc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedState.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji import com.vitorpamplona.amethyst.ui.feeds.LoadedFeedState import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt index ad2a9c318..232ce75e8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt @@ -25,8 +25,8 @@ import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel import com.patrykandpatrick.vico.core.common.data.ExtraStore import com.patrykandpatrick.vico.core.common.data.MutableExtraStore -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.BundledInsert diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt index 82589c3ed..fdcd0f3d9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.dal -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.filterIntoSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt index 9aa38037e..cc6c823ab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt @@ -45,7 +45,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.ui.components.LoadNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt index dc6039197..1643a6798 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt @@ -56,7 +56,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt index ad2647e31..060c90c20 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt @@ -33,8 +33,8 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt index 90f02b507..c7fc331cb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt @@ -58,7 +58,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.IntSize import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenUser import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/BookmarkTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/BookmarkTabHeader.kt index d4668fc8a..730d1dc3e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/BookmarkTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/BookmarkTabHeader.kt @@ -24,7 +24,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserBookmarkCount import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedFilter.kt index bea1a9c71..61caaaa53 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedFilter.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.dal -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedViewModel.kt index d54bcafea..fed67ff3e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedViewModel.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.screen.FeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedFilter.kt index e48a3bf1d..0c6d4ac73 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedFilter.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.conversations.dal -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedViewModel.kt index 36d0bf04f..0291559c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedViewModel.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.conversations.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.screen.FeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileFollowers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileFollowers.kt index bb038bc52..b3d8bc27a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileFollowers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileFollowers.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMedia.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMedia.kt index 49d812fa5..848429e4b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMedia.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMedia.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt index 05646d218..946127f67 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileZapReceived.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileZapReceived.kt index 686f2380d..62b5ca358 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileZapReceived.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileZapReceived.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt index 91a9ecbe3..5a217020b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssemblerSubscription.kt index ed1c25e4f..94af9e02b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssemblerSubscription.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/FollowersTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/FollowersTabHeader.kt index 416456e90..da530a8df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/FollowersTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/FollowersTabHeader.kt @@ -24,7 +24,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserContactCardsFollowerCount import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/TabFollowers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/TabFollowers.kt index cad4dd237..d242b9625 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/TabFollowers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/TabFollowers.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserFollowers import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersFeedFilter.kt index f97ac95ed..abd424e28 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersFeedFilter.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers.dal -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersUserFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersUserFeedViewModel.kt index 84924a850..864f60478 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersUserFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersUserFeedViewModel.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/FollowTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/FollowTabHeader.kt index 0a61ccd9e..58e085517 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/FollowTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/FollowTabHeader.kt @@ -24,7 +24,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserFollowCount import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/TabFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/TabFollows.kt index 8147d5eb6..aaa112059 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/TabFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/TabFollows.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserFollows import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsFeedFilter.kt index 1bcd90095..475a56500 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsFeedFilter.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows.dal -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsUserFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsUserFeedViewModel.kt index 8b4d3034f..10d08d985 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsUserFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsUserFeedViewModel.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt index 61927b150..d83168ddb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt @@ -26,7 +26,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.SensitivityWarning diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt index 19d479171..8b0e1db04 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt @@ -45,7 +45,7 @@ import androidx.media3.ui.PlayerView import coil3.compose.AsyncImagePainter import coil3.compose.SubcomposeAsyncImage import coil3.compose.SubcomposeAsyncImageContent -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.commons.richtext.MediaUrlContent import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/QuickActionGallery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/QuickActionGallery.kt index 8d7e079fa..778e99afd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/QuickActionGallery.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/QuickActionGallery.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.note.QuickActionAlertDialogOneButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt index cd5b4a2c6..e07e65b15 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.dal -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedViewModel.kt index c46cab934..d283e565e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedViewModel.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.screen.FeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/FollowedTagsTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/FollowedTagsTabHeader.kt index 108bd44a8..4c46aa32b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/FollowedTagsTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/FollowedTagsTabHeader.kt @@ -24,7 +24,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserTagFollowCount import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/TabFollowedTags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/TabFollowedTags.kt index ee4b2aa31..9730cfbc6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/TabFollowedTags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/TabFollowedTags.kt @@ -31,7 +31,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserTagFollows import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayFollowUnfollowButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayFollowUnfollowButton.kt index 6ef8f98b9..0bf1d7d7c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayFollowUnfollowButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayFollowUnfollowButton.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowing import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.FollowButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayLNAddress.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayLNAddress.kt index c1a1f8299..776bdc8c9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayLNAddress.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayLNAddress.kt @@ -32,7 +32,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.actions.InformationDialog import com.vitorpamplona.amethyst.ui.components.ClickableTextPrimary import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt index 15c03b951..6f4a4dc8b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt @@ -45,7 +45,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUser import com.vitorpamplona.amethyst.ui.components.ClickableTextPrimary import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawBanner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawBanner.kt index 458969a29..8d6446762 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawBanner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawBanner.kt @@ -37,7 +37,7 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserBanner import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt index d0ae678b7..0ec05e877 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt @@ -28,7 +28,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeToMessage import com.vitorpamplona.amethyst.ui.painterRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileActions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileActions.kt index 1d0f129ef..0d9c6b30e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileActions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileActions.kt @@ -24,7 +24,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenUser import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt index f9c6c6576..bcbb6b113 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt @@ -49,7 +49,7 @@ import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserProfileDropDownMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserProfileDropDownMenu.kt index ba5c21679..c42054030 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserProfileDropDownMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserProfileDropDownMenu.kt @@ -30,7 +30,7 @@ import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.AnnotatedString import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.note.externalLinkForUser import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/UserAppRecommendationsFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/UserAppRecommendationsFeedViewModel.kt index 34cdb1f6f..2d169e701 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/UserAppRecommendationsFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/UserAppRecommendationsFeedViewModel.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.screen.FeedViewModel class UserAppRecommendationsFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/UserProfileAppRecommendationsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/UserProfileAppRecommendationsFeedFilter.kt index eba05ef88..00ebd03c9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/UserProfileAppRecommendationsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/UserProfileAppRecommendationsFeedFilter.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/WatchApp.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/WatchApp.kt index 4e68eb164..b24d30293 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/WatchApp.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/WatchApp.kt @@ -33,7 +33,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import coil3.compose.AsyncImage -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt index b0359ef39..7762a611d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt @@ -33,9 +33,9 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEventAndMapNotNull diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt index dd9419b52..b447f5436 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual.dal -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedViewModel.kt index 0da57999f..66a71f8f9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedViewModel.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.screen.FeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt index 68c644911..60c6c7d7e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads.dal -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadsFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadsFeedViewModel.kt index 2f16bb745..a13994af6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadsFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadsFeedViewModel.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.screen.FeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt index eaec8b103..3ba91b326 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt @@ -34,7 +34,7 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.RelayInfo +import com.vitorpamplona.amethyst.model.RelayInfo import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt index e253497ab..c12009031 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt @@ -25,8 +25,8 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.commons.model.User -import com.vitorpamplona.amethyst.commons.model.RelayInfo +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.RelayInfo import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelaysTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelaysTabHeader.kt index 8f431e5ef..4c211d588 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelaysTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelaysTabHeader.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.relays import androidx.compose.material3.Text import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/TabRelays.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/TabRelays.kt index aeb64b416..2a2a4258a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/TabRelays.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/TabRelays.kt @@ -29,7 +29,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/ReportsTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/ReportsTabHeader.kt index 18e83cf67..f6fc2b3ea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/ReportsTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/ReportsTabHeader.kt @@ -24,7 +24,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserReportCount import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/TabReports.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/TabReports.kt index 2d2f6a4b5..8701d7e94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/TabReports.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/TabReports.kt @@ -24,7 +24,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/WatchReportsAndUpdateFeed.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/WatchReportsAndUpdateFeed.kt index 538d332a8..3b27d5b59 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/WatchReportsAndUpdateFeed.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/WatchReportsAndUpdateFeed.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports import androidx.compose.runtime.Composable -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserReports import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.dal.UserProfileReportFeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/dal/UserProfileReportFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/dal/UserProfileReportFeedViewModel.kt index 016330ad2..b01ab9936 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/dal/UserProfileReportFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/dal/UserProfileReportFeedViewModel.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.screen.FeedViewModel class UserProfileReportFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/dal/UserProfileReportsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/dal/UserProfileReportsFeedFilter.kt index 4038c3e08..b04fa0f4b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/dal/UserProfileReportsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/dal/UserProfileReportsFeedFilter.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.dal -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedState.kt index 131cd24f9..868c1fc97 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedState.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.MutableStateFlow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/TabReceivedZaps.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/TabReceivedZaps.kt index 3ce69a793..e51280ff2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/TabReceivedZaps.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/TabReceivedZaps.kt @@ -24,7 +24,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.UserProfileZapsFeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/WatchZapsAndUpdateFeed.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/WatchZapsAndUpdateFeed.kt index 911ef4b9f..4930c1184 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/WatchZapsAndUpdateFeed.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/WatchZapsAndUpdateFeed.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserZaps import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.UserProfileZapsFeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapTabHeader.kt index 9d9db8147..c2d73597a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapTabHeader.kt @@ -24,7 +24,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserZapAmount import com.vitorpamplona.amethyst.ui.note.showAmountInteger import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsFeedFilter.kt index 1fe7fd244..06ea5284c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsFeedFilter.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.ZapReqResponse import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsFeedViewModel.kt index e472d934a..40ef049d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsFeedViewModel.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.LnZapFeedViewModel class UserProfileZapsFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRScreen.kt index 52081b6f8..89da19efa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRScreen.kt @@ -49,7 +49,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.DisplayNIP05 import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt index 0ddb9c1b4..bb6e918c1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt @@ -33,7 +33,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt index 18f738ec2..a5a5560ff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStat import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/report/ReportNoteDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/report/ReportNoteDialog.kt index 67616f4b8..f671ea691 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/report/ReportNoteDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/report/ReportNoteDialog.kt @@ -59,7 +59,7 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.TextSpinner import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenAccountsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenAccountsFeedFilter.kt index 45b4e650c..31ba32e76 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenAccountsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenAccountsFeedFilter.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.dal -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedFilter.kt index adb19f890..a27469c58 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedFilter.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.dal -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.FeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index 569a73e11..1a0c5e20d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -73,7 +73,7 @@ import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeCommunityApprovalNeedStatus diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/LevelFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/LevelFeedViewModel.kt index cd7f11636..3d006fffb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/LevelFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/LevelFeedViewModel.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ThreadLevelCalculator import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.feeds.FeedState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedFilter.kt index 4691b802f..f758e2d69 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedFilter.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal import androidx.compose.runtime.Immutable -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LevelSignature import com.vitorpamplona.amethyst.model.ThreadAssembler diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterEventsInThreadForRoot.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterEventsInThreadForRoot.kt index 04f46b7a8..c0cabe9ff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterEventsInThreadForRoot.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterEventsInThreadForRoot.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt index 4b865c4e5..45cdfd83d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ThreadAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingAddressables import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingEvents diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt index 17cb3aa64..16a001a81 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt @@ -51,7 +51,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt index 7f1a0cf26..bb2e9851a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.dal -import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt index 0c6e0413d..ef53e0037 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt index e8940388f..e034675e2 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst -import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache.getOrCreateAddressableNoteInternal import com.vitorpamplona.amethyst.ui.actions.Dao import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/zaps/UserProfileZapsFeedFilterTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/zaps/UserProfileZapsFeedFilterTest.kt index 4d16bb3c0..6e2f58188 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/zaps/UserProfileZapsFeedFilterTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/zaps/UserProfileZapsFeedFilterTest.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.zaps -import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.UserProfileZapsFeedFilter import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip56Reports/UserReportCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip56Reports/UserReportCache.kt index f078c4c83..97b3cdc6a 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip56Reports/UserReportCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip56Reports/UserReportCache.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.commons.model.nip56Reports import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.UserDependencies +import com.vitorpamplona.amethyst.commons.relays.EOSERelayList import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip56Reports.ReportType @@ -33,6 +34,9 @@ import kotlinx.coroutines.flow.update class UserReportCache : UserDependencies { val receivedReportsByAuthor = MutableStateFlow(mapOf>()) + /** Tracks EOSE (End Of Stored Events) for relay subscriptions */ + val latestEOSEs = EOSERelayList() + fun addReport(note: Note) { val author = note.author ?: return diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/trustedAssertions/UserCardsCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/trustedAssertions/UserCardsCache.kt index 944486ca5..f8a5b4552 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/trustedAssertions/UserCardsCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/trustedAssertions/UserCardsCache.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.commons.model.trustedAssertions import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.UserDependencies +import com.vitorpamplona.amethyst.commons.relays.EOSERelayList import com.vitorpamplona.amethyst.commons.util.PlatformNumberFormatter import com.vitorpamplona.quartz.experimental.relationshipStatus.ContactCardEvent import kotlinx.coroutines.Dispatchers @@ -36,6 +37,9 @@ import kotlinx.coroutines.flow.update class UserCardsCache : UserDependencies { val receivedCards = MutableStateFlow(mapOf()) + /** Tracks EOSE (End Of Stored Events) for relay subscriptions */ + val latestEOSEs = EOSERelayList() + fun addCard(note: AddressableNote) { val author = note.author ?: return diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relays/EOSERelayList.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relays/EOSERelayList.kt new file mode 100644 index 000000000..c8ad7b0f6 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relays/EOSERelayList.kt @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.relays + +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl + +typealias SincePerRelayMap = MutableMap + +/** + * Tracks EOSE (End Of Stored Events) timestamps for relay subscriptions. + * Used by UserCardsCache and similar classes to manage relay subscription state. + */ +class EOSERelayList { + var relayList: SincePerRelayMap = mutableMapOf() + + fun addOrUpdate( + relayUrl: NormalizedRelayUrl, + time: Long, + ) { + val eose = relayList[relayUrl] + if (eose == null) { + relayList[relayUrl] = MutableTime(time) + } else { + eose.updateIfNewer(time) + } + } + + fun clear() { + relayList = mutableMapOf() + } + + fun since() = relayList + + fun newEose( + relay: NormalizedRelayUrl, + time: Long, + ) = addOrUpdate(relay, time) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/MutableTime.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relays/MutableTime.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/MutableTime.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relays/MutableTime.kt index 7a6701355..2aedc0432 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/MutableTime.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relays/MutableTime.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.service.relays +package com.vitorpamplona.amethyst.commons.relays /* * Wrapper class to allow changing in EOSE without modifying the list it is included within From 5c4db83b40f6462340753d519990bc4f65752508 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Thu, 1 Jan 2026 12:46:07 +0200 Subject: [PATCH 025/137] run spotless --- .../java/com/vitorpamplona/amethyst/model/Account.kt | 8 ++------ .../vitorpamplona/amethyst/model/AccountSettings.kt | 5 ----- .../amethyst/model/AccountSyncedSettings.kt | 5 ----- .../amethyst/model/AccountSyncedSettingsInternal.kt | 5 ----- .../vitorpamplona/amethyst/model/AntiSpamFilter.kt | 5 ----- .../java/com/vitorpamplona/amethyst/model/Channel.kt | 5 ----- .../com/vitorpamplona/amethyst/model/Constants.kt | 5 ----- .../com/vitorpamplona/amethyst/model/HashtagIcon.kt | 5 ----- .../vitorpamplona/amethyst/model/LargeSoftCache.kt | 5 ----- .../amethyst/model/LargeSoftCacheAddressExt.kt | 5 ----- .../com/vitorpamplona/amethyst/model/LocalCache.kt | 3 --- .../amethyst/model/MediaAspectRatioCache.kt | 5 ----- .../com/vitorpamplona/amethyst/model/MiniFhir.kt | 5 ----- .../amethyst/model/ParticipantListBuilder.kt | 5 ----- .../vitorpamplona/amethyst/model/ThreadAssembler.kt | 5 ----- .../amethyst/model/ThreadLevelCalculator.kt | 5 ----- .../com/vitorpamplona/amethyst/model/UiSettings.kt | 5 ----- .../vitorpamplona/amethyst/model/UiSettingsFlow.kt | 5 ----- .../amethyst/model/UrlCachedPreviewer.kt | 5 ----- .../model/accountsCache/AccountCacheState.kt | 5 ----- .../edits/PrivateStorageRelayListDecryptionCache.kt | 5 ----- .../model/edits/PrivateStorageRelayListState.kt | 4 ++-- .../amethyst/model/emphChat/EphemeralChatChannel.kt | 5 ----- .../emphChat/EphemeralChatListDecryptionCache.kt | 5 ----- .../model/emphChat/EphemeralChatListState.kt | 4 ++-- .../model/localRelays/LocalRelayListState.kt | 5 ----- .../model/nip01UserMetadata/AccountHomeRelayState.kt | 5 ----- .../nip01UserMetadata/AccountOutboxRelayState.kt | 5 ----- .../nip01UserMetadata/NotificationInboxRelayState.kt | 5 ----- .../model/nip01UserMetadata/UserMetadataState.kt | 7 +------ .../DeclaredFollowsPerOutboxRelay.kt | 5 ----- .../FollowListOutboxOrProxyRelays.kt | 5 ----- .../FollowListReusedOutboxOrProxyRelays.kt | 5 ----- .../model/nip02FollowLists/FollowsPerOutboxRelay.kt | 5 ----- .../model/nip02FollowLists/Kind3FollowListState.kt | 7 ++----- .../model/nip03Timestamp/IncomingOtsEventVerifier.kt | 2 +- .../amethyst/model/nip03Timestamp/OtsState.kt | 2 +- .../TorAwareOkHttpOtsResolverBuilder.kt | 5 ----- .../amethyst/model/nip11RelayInfo/LoadRelayInfo.kt | 5 ----- .../model/nip11RelayInfo/Nip11CachedRetriever.kt | 5 ----- .../amethyst/model/nip11RelayInfo/Nip11Retriever.kt | 5 ----- .../amethyst/model/nip11RelayInfo/RetrieveResult.kt | 5 ----- .../amethyst/model/nip17Dms/DmInboxRelayState.kt | 5 ----- .../amethyst/model/nip17Dms/DmRelayListState.kt | 4 ++-- .../model/nip28PublicChats/PublicChatChannel.kt | 2 +- .../PublicChatListDecryptionCache.kt | 5 ----- .../model/nip28PublicChats/PublicChatListState.kt | 4 ++-- .../model/nip30CustomEmojis/EmojiPackState.kt | 4 ++-- .../model/nip38UserStatuses/UserStatusAction.kt | 2 -- .../model/nip47WalletConnect/NwcSignerState.kt | 2 +- .../amethyst/model/nip51Lists/BookmarkListState.kt | 2 +- .../amethyst/model/nip51Lists/HiddenUsersState.kt | 6 ------ .../blockPeopleList/BlockPeopleListState.kt | 2 +- .../blockedRelays/BlockedRelayListDecryptionCache.kt | 5 ----- .../blockedRelays/BlockedRelayListState.kt | 4 ++-- .../BroadcastRelayListDecryptionCache.kt | 5 ----- .../broadcastRelays/BroadcastRelayListState.kt | 4 ++-- .../model/nip51Lists/geohashLists/GeohashListCard.kt | 5 ----- .../nip51Lists/geohashLists/GeohashListState.kt | 4 ++-- .../hashtagLists/HashtagListDecryptionCache.kt | 5 ----- .../nip51Lists/hashtagLists/HashtagListState.kt | 4 ++-- .../indexerRelays/IndexerRelayListDecryptionCache.kt | 5 ----- .../indexerRelays/IndexerRelayListState.kt | 4 ++-- .../labeledBookmarkLists/LabeledBookmarkList.kt | 5 ----- .../LabeledBookmarkListsState.kt | 10 +++++----- .../nip51Lists/muteList/MuteListDecryptionCache.kt | 5 ----- .../model/nip51Lists/muteList/MuteListState.kt | 4 ++-- .../model/nip51Lists/peopleList/FollowListsState.kt | 12 ++++++------ .../model/nip51Lists/peopleList/PeopleList.kt | 5 +---- .../peopleList/PeopleListDecryptionCache.kt | 5 ----- .../model/nip51Lists/peopleList/PeopleListsState.kt | 12 ++++++------ .../proxyRelays/ProxyRelayListDecryptionCache.kt | 5 ----- .../nip51Lists/proxyRelays/ProxyRelayListState.kt | 4 ++-- .../model/nip51Lists/relayLists/RelayListCard.kt | 5 ----- .../searchRelays/SearchRelayListDecryptionCache.kt | 5 ----- .../nip51Lists/searchRelays/SearchRelayListState.kt | 4 ++-- .../trustedRelays/TrustedRelayListDecryptionCache.kt | 5 ----- .../trustedRelays/TrustedRelayListState.kt | 4 ++-- .../nip53LiveActivities/LiveActivitiesChannel.kt | 2 +- .../model/nip65RelayList/Nip65RelayListState.kt | 4 ++-- .../nip72Communities/CommunityListDecryptionCache.kt | 5 ----- .../model/nip72Communities/CommunityListState.kt | 4 ++-- .../model/nip78AppSpecific/AppSpecificState.kt | 2 +- .../nip96FileStorage/FileStorageServerListState.kt | 4 ++-- .../model/nipB7Blossom/BlossomServerListState.kt | 4 ++-- .../model/observables/LatestByKindAndAuthor.kt | 2 +- .../model/observables/LatestByKindWithETag.kt | 2 +- .../model/preferences/AccountPreferenceStores.kt | 5 ----- .../preferences/AccountSecretsEncryptedStores.kt | 5 ----- .../amethyst/model/preferences/DataStoreExt.kt | 5 ----- .../amethyst/model/preferences/EncryptedDataStore.kt | 5 ----- .../amethyst/model/preferences/KeyStoreEncryption.kt | 5 ----- .../model/preferences/TorSharedPreferences.kt | 5 ----- .../model/preferences/UISharedPreferences.kt | 5 ----- .../model/preferences/UpdatablePropertyFlow.kt | 5 ----- .../EmptyRoleBasedHttpClientBuilder.kt | 5 ----- .../privacyOptions/IRoleBasedHttpClientBuilder.kt | 5 ----- .../privacyOptions/RoleBasedHttpClientBuilder.kt | 5 ----- .../SingleRoleBasedHttpClientBuilder.kt | 5 ----- .../model/serverList/MergedFollowListsState.kt | 5 ----- .../MergedFollowPlusMineRelayListsState.kt | 5 ----- .../MergedFollowPlusMineWithIndexRelayListsState.kt | 5 ----- .../MergedFollowPlusMineWithSearchRelayListsState.kt | 5 ----- .../model/serverList/MergedServerListState.kt | 5 ----- .../model/serverList/TrustedRelayListsState.kt | 5 ----- .../model/topNavFeeds/CommunityRelayLoader.kt | 2 +- .../model/topNavFeeds/FeedDecryptionCaches.kt | 5 ----- .../model/topNavFeeds/FeedTopNavFilterState.kt | 5 ----- .../amethyst/model/topNavFeeds/IFeedFlowsType.kt | 5 ----- .../amethyst/model/topNavFeeds/IFeedTopNavFilter.kt | 5 ----- .../model/topNavFeeds/IFeedTopNavPerRelayFilter.kt | 5 ----- .../topNavFeeds/IFeedTopNavPerRelayFilterSet.kt | 5 ----- .../topNavFeeds/MergedTopFeedAuthorListsState.kt | 5 ----- .../amethyst/model/topNavFeeds/OutboxLoaderState.kt | 5 ----- .../amethyst/model/topNavFeeds/OutboxRelayLoader.kt | 2 +- .../allFollows/AllFollowsByOutboxTopNavFilter.kt | 5 ----- .../allFollows/AllFollowsByProxyTopNavFilter.kt | 5 ----- .../topNavFeeds/allFollows/AllFollowsFeedFlow.kt | 5 ----- .../allFollows/AllFollowsTopNavPerRelayFilter.kt | 5 ----- .../allFollows/AllFollowsTopNavPerRelayFilterSet.kt | 5 ----- .../AllUserFollowsByOutboxTopNavFilter.kt | 5 ----- .../AllUserFollowsByProxyTopNavFilter.kt | 5 ----- .../allUserFollows/AllUserFollowsFeedFlow.kt | 5 ----- .../allUserFollows/Kind3UserFollowsFeedFlow.kt | 5 ----- .../model/topNavFeeds/aroundMe/AroundMeExpander.kt | 5 ----- .../model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt | 5 ----- .../topNavFeeds/aroundMe/LocationTopNavFilter.kt | 5 ----- .../aroundMe/LocationTopNavPerRelayFilter.kt | 5 ----- .../aroundMe/LocationTopNavPerRelayFilterSet.kt | 5 ----- .../model/topNavFeeds/global/GlobalFeedFlow.kt | 5 ----- .../model/topNavFeeds/global/GlobalTopNavFilter.kt | 5 ----- .../topNavFeeds/global/GlobalTopNavPerRelayFilter.kt | 5 ----- .../global/GlobalTopNavPerRelayFilterSet.kt | 5 ----- .../model/topNavFeeds/hashtag/HashtagTopNavFilter.kt | 5 ----- .../hashtag/HashtagTopNavPerRelayFilter.kt | 5 ----- .../hashtag/HashtagTopNavPerRelayFilterSet.kt | 5 ----- .../allcommunities/AllCommunitiesTopNavFilter.kt | 5 ----- .../AllCommunitiesTopNavPerRelayFilter.kt | 5 ----- .../AllCommunitiesTopNavPerRelayFilterSet.kt | 5 ----- .../noteBased/author/AuthorsByOutboxTopNavFilter.kt | 5 ----- .../noteBased/author/AuthorsByProxyTopNavFilter.kt | 5 ----- .../noteBased/author/AuthorsTopNavPerRelayFilter.kt | 5 ----- .../author/AuthorsTopNavPerRelayFilterSet.kt | 5 ----- .../community/SingleCommunityTopNavFilter.kt | 5 ----- .../community/SingleCommunityTopNavPerRelayFilter.kt | 5 ----- .../SingleCommunityTopNavPerRelayFilterSet.kt | 5 ----- .../muted/MutedAuthorsByOutboxTopNavFilter.kt | 5 ----- .../muted/MutedAuthorsByProxyTopNavFilter.kt | 5 ----- .../muted/MutedAuthorsTopNavPerRelayFilter.kt | 5 ----- .../muted/MutedAuthorsTopNavPerRelayFilterSet.kt | 5 ----- .../model/topNavFeeds/unknown/UnknownFeedFlow.kt | 5 ----- .../model/topNavFeeds/unknown/UnknownTopNavFilter.kt | 5 ----- .../unknown/UnknownTopNavPerRelayFilterSet.kt | 5 ----- .../model/torState/AccountsTorStateConnector.kt | 5 ----- .../amethyst/model/torState/TorRelayEvaluation.kt | 5 ----- .../amethyst/model/torState/TorRelaySettings.kt | 5 ----- .../amethyst/model/torState/TorRelayState.kt | 5 ----- .../TrustProviderListDecryptionCache.kt | 5 ----- .../trustedAssertions/TrustProviderListState.kt | 6 +++--- .../amethyst/service/ZapPaymentHandler.kt | 4 ++-- .../notifications/EventNotificationConsumer.kt | 2 +- .../service/relayClient/CacheClientConnector.kt | 2 +- .../reqCommand/account/AccountObservers.kt | 2 +- .../follows/AccountFollowsLoaderSubAssembler.kt | 2 +- .../follows/FilterFindFollowMetadataForKey.kt | 2 +- .../reqCommand/channel/ChannelObservers.kt | 2 +- .../reqCommand/event/EventFinderFilterAssembler.kt | 2 +- .../event/EventFinderFilterAssemblerSubscription.kt | 2 +- .../reqCommand/event/loaders/FilterMissingEvents.kt | 2 +- .../reqCommand/user/UserFinderFilterAssembler.kt | 2 +- .../user/UserFinderFilterAssemblerSubscription.kt | 2 +- .../relayClient/reqCommand/user/UserObservers.kt | 4 ++-- .../user/loaders/UserOutboxFinderSubAssembler.kt | 2 +- .../user/watchers/FilterUserMetadataForKey.kt | 2 +- .../user/watchers/UserCardsSubAssembler.kt | 4 ++-- .../user/watchers/UserReportsSubAssembler.kt | 4 ++-- .../user/watchers/UserWatcherSubAssembler.kt | 2 +- .../amethyst/ui/actions/EditPostView.kt | 2 +- .../amethyst/ui/actions/EditPostViewModel.kt | 4 ++-- .../amethyst/ui/components/RichTextViewer.kt | 4 ++-- .../ui/components/markdown/MarkdownMediaRenderer.kt | 2 +- .../ui/components/toasts/multiline/ErrorList.kt | 2 +- .../toasts/multiline/MultiUserErrorMessageDialog.kt | 2 +- .../amethyst/ui/feeds/ChannelFeedContentState.kt | 2 +- .../amethyst/ui/feeds/FeedContentState.kt | 2 +- .../ui/navigation/drawer/AccountSwitchBottomSheet.kt | 2 +- .../amethyst/ui/navigation/drawer/DrawerContent.kt | 2 +- .../amethyst/ui/navigation/routes/RouteMaker.kt | 4 ++-- .../com/vitorpamplona/amethyst/ui/note/Loaders.kt | 2 +- .../com/vitorpamplona/amethyst/ui/note/PollNote.kt | 2 +- .../amethyst/ui/note/PollNoteViewModel.kt | 2 +- .../amethyst/ui/note/UpdateReactionTypeDialog.kt | 2 +- .../amethyst/ui/note/ZapCustomDialog.kt | 2 +- .../vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt | 2 +- .../userSuggestions/ShowUserSuggestionList.kt | 2 +- .../creators/userSuggestions/UserSuggestionState.kt | 2 +- .../amethyst/ui/note/elements/DisplayReward.kt | 2 +- .../ui/note/nip22Comments/CommentPostViewModel.kt | 4 ++-- .../amethyst/ui/note/types/AppDefinition.kt | 2 +- .../amethyst/ui/note/types/Classifieds.kt | 2 +- .../amethyst/ui/note/types/FileHeader.kt | 2 +- .../amethyst/ui/note/types/FileStorage.kt | 2 +- .../amethyst/ui/note/types/FollowList.kt | 2 +- .../amethyst/ui/note/types/LiveActivity.kt | 2 +- .../amethyst/ui/note/types/MedicalData.kt | 2 +- .../amethyst/ui/note/types/PictureDisplay.kt | 2 +- .../com/vitorpamplona/amethyst/ui/note/types/Poll.kt | 2 +- .../com/vitorpamplona/amethyst/ui/note/types/Text.kt | 2 +- .../vitorpamplona/amethyst/ui/note/types/Torrent.kt | 2 +- .../amethyst/ui/note/types/TorrentComment.kt | 2 +- .../vitorpamplona/amethyst/ui/note/types/Video.kt | 2 +- .../amethyst/ui/note/types/VideoDisplay.kt | 2 +- .../amethyst/ui/screen/FeedViewModel.kt | 2 +- .../amethyst/ui/screen/TopNavFilterState.kt | 2 +- .../amethyst/ui/screen/UserFeedViewModel.kt | 2 +- .../ui/screen/loggedIn/AccountFeedContentStates.kt | 2 +- .../amethyst/ui/screen/loggedIn/AccountViewModel.kt | 9 ++++----- .../ui/screen/loggedIn/DecryptAndIndexProcessor.kt | 2 +- .../bookmarks/dal/BookmarkPrivateFeedFilter.kt | 2 +- .../bookmarks/dal/BookmarkPublicFeedFilter.kt | 2 +- .../chats/feed/types/RenderCreateChannelNote.kt | 2 +- .../loggedIn/chats/feed/types/RenderEncryptedFile.kt | 2 +- .../chats/privateDM/dal/ChatroomFeedFilter.kt | 2 +- .../chats/privateDM/dal/ChatroomFeedViewModel.kt | 2 +- .../chats/privateDM/send/ChatNewMessageViewModel.kt | 4 ++-- .../chats/publicChannels/dal/ChannelFeedFilter.kt | 2 +- .../header/LongLiveActivityChannelHeader.kt | 2 +- .../header/ShortLiveActivityChannelHeader.kt | 2 +- .../send/ChannelNewMessageViewModel.kt | 4 ++-- .../chats/rooms/dal/ChatroomListKnownFeedFilter.kt | 2 +- .../chats/rooms/dal/ChatroomListNewFeedFilter.kt | 2 +- .../loggedIn/communities/dal/CommunityFeedFilter.kt | 4 ++-- .../communities/dal/CommunityFeedViewModel.kt | 2 +- .../communities/dal/CommunityModerationFeedFilter.kt | 4 ++-- .../dal/CommunityModerationFeedViewModel.kt | 2 +- .../nip23LongForm/DiscoverLongFormFeedFilter.kt | 2 +- .../discover/nip28Chats/DiscoverChatFeedFilter.kt | 2 +- .../nip28Chats/RenderPublicChatChannelThumb.kt | 2 +- .../nip51FollowSets/DiscoverFollowSetsFeedFilter.kt | 2 +- .../discover/nip51FollowSets/FollowSetCard.kt | 2 +- .../nip53LiveActivities/DiscoverLiveFeedFilter.kt | 2 +- .../discover/nip53LiveActivities/LiveActivityCard.kt | 4 ++-- .../discover/nip72Communities/CommunityCard.kt | 4 ++-- .../nip72Communities/DiscoverCommunityFeedFilter.kt | 2 +- .../discover/nip90DVMs/DiscoverNIP89FeedFilter.kt | 2 +- .../DiscoverMarketplaceFeedFilter.kt | 2 +- .../discover/nip99Classifieds/NewProductViewModel.kt | 4 ++-- .../loggedIn/drafts/dal/DraftEventsFeedFilter.kt | 2 +- .../dvms/dal/NIP90ContentDiscoveryResponseFilter.kt | 2 +- .../dal/FollowPackFeedConversationsFeedFilter.kt | 4 ++-- .../dal/FollowPackFeedConversationsFeedViewModel.kt | 2 +- .../feed/dal/FollowPackFeedNewThreadFeedFilter.kt | 4 ++-- .../feed/dal/FollowPackFeedNewThreadFeedViewModel.kt | 2 +- .../feed/dal/FollowPackMembersFeedFilter.kt | 4 ++-- .../feed/dal/FollowPackMembersUserFeedViewModel.kt | 2 +- .../feed/datasource/FollowPackFeedFilterAssembler.kt | 2 +- .../screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt | 2 +- .../screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt | 2 +- .../screen/loggedIn/home/ShortNotePostViewModel.kt | 4 ++-- .../loggedIn/home/dal/HomeConversationsFeedFilter.kt | 2 +- .../ui/screen/loggedIn/home/dal/HomeLiveFilter.kt | 2 +- .../loggedIn/home/dal/HomeNewThreadFeedFilter.kt | 2 +- .../loggedIn/lists/display/ShowUserSuggestions.kt | 2 +- .../loggedIn/lists/display/lists/PeopleListScreen.kt | 2 +- .../lists/display/lists/PeopleListViewModel.kt | 2 +- .../loggedIn/lists/display/packs/FollowPackScreen.kt | 2 +- .../lists/display/packs/FollowPackViewModel.kt | 2 +- .../ui/screen/loggedIn/lists/list/PeopleListItem.kt | 2 +- .../memberEdit/FollowListAndPackAndUserScreen.kt | 2 +- .../loggedIn/notifications/CardFeedContentState.kt | 4 ++-- .../notifications/NotificationSummaryState.kt | 4 ++-- .../notifications/dal/NotificationFeedFilter.kt | 4 ++-- .../notifications/donations/ZapTheDevsCard.kt | 2 +- .../publicMessages/NewPublicMessageViewModel.kt | 4 ++-- .../ui/screen/loggedIn/profile/ProfileScreen.kt | 2 +- .../bookmarks/dal/UserProfileBookmarksFeedFilter.kt | 4 ++-- .../dal/UserProfileBookmarksFeedViewModel.kt | 2 +- .../dal/UserProfileConversationsFeedFilter.kt | 4 ++-- .../dal/UserProfileConversationsFeedViewModel.kt | 2 +- .../profile/datasource/FilterUserProfileFollowers.kt | 2 +- .../profile/datasource/FilterUserProfileMedia.kt | 2 +- .../profile/datasource/FilterUserProfilePosts.kt | 2 +- .../datasource/FilterUserProfileZapReceived.kt | 2 +- .../followers/dal/UserProfileFollowersFeedFilter.kt | 2 +- .../dal/UserProfileFollowersUserFeedViewModel.kt | 2 +- .../follows/dal/UserProfileFollowsFeedFilter.kt | 2 +- .../dal/UserProfileFollowsUserFeedViewModel.kt | 2 +- .../screen/loggedIn/profile/gallery/GalleryThumb.kt | 2 +- .../gallery/dal/UserProfileGalleryFeedFilter.kt | 4 ++-- .../gallery/dal/UserProfileGalleryFeedViewModel.kt | 2 +- .../ui/screen/loggedIn/profile/header/DrawBanner.kt | 2 +- .../screen/loggedIn/profile/header/ProfileHeader.kt | 2 +- .../apps/UserProfileAppRecommendationsFeedFilter.kt | 2 +- .../loggedIn/profile/header/badges/DisplayBadges.kt | 2 +- .../mutual/dal/UserProfileMutualFeedFilter.kt | 4 ++-- .../mutual/dal/UserProfileMutualFeedViewModel.kt | 2 +- .../newthreads/dal/UserProfileNewThreadFeedFilter.kt | 4 ++-- .../dal/UserProfileNewThreadsFeedViewModel.kt | 2 +- .../loggedIn/profile/relays/RelayFeedViewModel.kt | 2 +- .../settings/dal/HiddenAccountsFeedFilter.kt | 2 +- .../settings/dal/SpammerAccountsFeedFilter.kt | 2 +- .../ui/screen/loggedIn/threadview/ThreadFeedView.kt | 2 +- .../loggedIn/threadview/dal/ThreadFeedFilter.kt | 3 +-- .../subassembies/FilterMissingEventsForThread.kt | 1 - .../ui/screen/loggedIn/video/dal/VideoFeedFilter.kt | 5 ++--- .../amethyst/NewMessageTaggerKeyParseTest.kt | 2 +- 306 files changed, 252 insertions(+), 872 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 264b54cb6..e91d9171f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -21,13 +21,9 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.IAccount -import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.LocalPreferences +import com.vitorpamplona.amethyst.commons.model.IAccount import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListDecryptionCache @@ -108,7 +104,6 @@ import com.vitorpamplona.quartz.experimental.bounties.BountyAddValueEvent import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent -import com.vitorpamplona.quartz.utils.DualCase import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryReadingStateEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent import com.vitorpamplona.quartz.experimental.interactiveStories.image @@ -211,6 +206,7 @@ import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent +import com.vitorpamplona.quartz.utils.DualCase import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.containsAny import kotlinx.coroutines.CoroutineScope diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index cd5dfcd3e..a654dc436 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt index 26251c0f8..16d1a7626 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt index 020e3769d..8b66d2ce1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import android.content.res.Resources import androidx.core.os.ConfigurationCompat import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt index fb57bb562..902a9015b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import android.util.LruCache import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.ui.note.njumpLink diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt index 6ff880a6b..336610ae9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NotesGatherer -import com.vitorpamplona.amethyst.model.User - import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.amethyst.ui.dal.ListChange diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Constants.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Constants.kt index ee72252ad..f6486e2be 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Constants.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Constants.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer object Constants { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt index 070603f64..f9afce5bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LargeSoftCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LargeSoftCache.kt index e327bb5dc..fb6377c42 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LargeSoftCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LargeSoftCache.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.quartz.utils.cache.CacheOperations import java.lang.ref.WeakReference import java.util.concurrent.ConcurrentSkipListMap diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LargeSoftCacheAddressExt.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LargeSoftCacheAddressExt.kt index a68632e9e..42a84bf44 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LargeSoftCacheAddressExt.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LargeSoftCacheAddressExt.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.utils.cache.CacheCollectors diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index a9119ec3a..28b05c3f2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -23,9 +23,6 @@ package com.vitorpamplona.amethyst.model import android.util.LruCache import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MediaAspectRatioCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MediaAspectRatioCache.kt index 7c18720f8..b4b199f9d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MediaAspectRatioCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MediaAspectRatioCache.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import android.util.LruCache interface MutableMediaAspectRatioCache { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MiniFhir.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MiniFhir.kt index c8abb5df9..1e56a62a8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MiniFhir.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MiniFhir.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Stable import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonTypeInfo diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ParticipantListBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ParticipantListBuilder.kt index ffadedace..8e1badf18 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ParticipantListBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ParticipantListBuilder.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.quartz.nip01Core.core.HexKey class ParticipantListBuilder { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt index 6e7ae6263..03ba05a3d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.quartz.nip01Core.core.Address diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadLevelCalculator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadLevelCalculator.kt index 228c66b6f..8b066ce09 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadLevelCalculator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadLevelCalculator.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt index 8742bf246..cfd116e51 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.R import kotlinx.serialization.Serializable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt index c30b4b762..d1d4303a7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt index f8706735e..b2e43b270 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import android.util.LruCache import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.service.previews.UrlPreview diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt index 6031ad0a3..0126df2fe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.accountsCache -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import android.content.ContentResolver import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AccountSettings diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListDecryptionCache.kt index a0127c9f0..ca8e73549 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListDecryptionCache.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.edits -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt index 280a11db6..b67f31d19 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.edits -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt index ab78e2557..12a28673d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.emphChat -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListDecryptionCache.kt index e53994812..60ff2f80f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListDecryptionCache.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.emphChat -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent import com.vitorpamplona.quartz.experimental.ephemChat.list.roomSet import com.vitorpamplona.quartz.experimental.ephemChat.list.rooms diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt index deb29e22f..3899d0aef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.emphChat -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/LocalRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/LocalRelayListState.kt index f0d33cb59..20917ae6c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/LocalRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/LocalRelayListState.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.localRelays -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountHomeRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountHomeRelayState.kt index 5b3359649..89403654e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountHomeRelayState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountHomeRelayState.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip01UserMetadata -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt index 32cfa28eb..65f76c500 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip01UserMetadata -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/NotificationInboxRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/NotificationInboxRelayState.kt index 84a3d7a79..0729475dc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/NotificationInboxRelayState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/NotificationInboxRelayState.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip01UserMetadata -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState import kotlinx.coroutines.CoroutineScope diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt index f471ea257..d64ec40bd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt @@ -20,14 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip01UserMetadata -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - -import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.utils.Log diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/DeclaredFollowsPerOutboxRelay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/DeclaredFollowsPerOutboxRelay.kt index 572d2c998..f2934b113 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/DeclaredFollowsPerOutboxRelay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/DeclaredFollowsPerOutboxRelay.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip02FollowLists -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxRelayLoader import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxOrProxyRelays.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxOrProxyRelays.kt index 8ae4dc37d..13d29c44f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxOrProxyRelays.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxOrProxyRelays.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip02FollowLists -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListReusedOutboxOrProxyRelays.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListReusedOutboxOrProxyRelays.kt index 504ada7df..2f5751936 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListReusedOutboxOrProxyRelays.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListReusedOutboxOrProxyRelays.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip02FollowLists -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt index 42999f6d5..921c8d353 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip02FollowLists -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/Kind3FollowListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/Kind3FollowListState.kt index 798986508..46f8a0cba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/Kind3FollowListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/Kind3FollowListState.kt @@ -20,14 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip02FollowLists -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - import androidx.compose.runtime.Immutable -import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/IncomingOtsEventVerifier.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/IncomingOtsEventVerifier.kt index f86e1a3fd..84f0b77d3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/IncomingOtsEventVerifier.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/IncomingOtsEventVerifier.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip03Timestamp -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache import kotlinx.coroutines.CoroutineScope diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt index f46820c3f..d941acebf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip03Timestamp -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/TorAwareOkHttpOtsResolverBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/TorAwareOkHttpOtsResolverBuilder.kt index 33f967321..b86a8c77b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/TorAwareOkHttpOtsResolverBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/TorAwareOkHttpOtsResolverBuilder.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip03Timestamp -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder import com.vitorpamplona.quartz.nip03Timestamp.okhttp.OkHttpBitcoinExplorer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/LoadRelayInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/LoadRelayInfo.kt index a5d0eaab5..4ed3eb50d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/LoadRelayInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/LoadRelayInfo.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip11RelayInfo -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.produceState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11CachedRetriever.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11CachedRetriever.kt index 1a8eaaa50..dbd59899f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11CachedRetriever.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11CachedRetriever.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip11RelayInfo -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import android.util.LruCache import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11Retriever.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11Retriever.kt index da740ce84..fd4bab976 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11Retriever.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11Retriever.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip11RelayInfo -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.toHttp import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/RetrieveResult.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/RetrieveResult.kt index ee9491a4b..bdc158317 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/RetrieveResult.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/RetrieveResult.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip11RelayInfo -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation import com.vitorpamplona.quartz.utils.TimeUtils diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmInboxRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmInboxRelayState.kt index deb665cf1..6ffe95928 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmInboxRelayState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmInboxRelayState.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip17Dms -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt index 0c91e4bde..ba18f9850 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip17Dms -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt index ba31086bd..75fa41af7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.model.nip28PublicChats import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.ui.note.toShortDisplay import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListDecryptionCache.kt index 84c75b282..a0a54ff93 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListDecryptionCache.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip28PublicChats -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent import com.vitorpamplona.quartz.nip28PublicChat.list.channelSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt index 10a9a52dd..f52bd4687 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip28PublicChats -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt index 6182c03b0..52d0f1aee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip30CustomEmojis -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedAddresses import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip38UserStatuses/UserStatusAction.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip38UserStatuses/UserStatusAction.kt index e9916f754..834e2437f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip38UserStatuses/UserStatusAction.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip38UserStatuses/UserStatusAction.kt @@ -21,8 +21,6 @@ package com.vitorpamplona.amethyst.model.nip38UserStatuses import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt index fad4ce0b1..2230daca6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.model.nip47WalletConnect import com.vitorpamplona.amethyst.commons.model.INwcSignerState -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentQueryState import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BookmarkListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BookmarkListState.kt index a822975c0..6606229ec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BookmarkListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BookmarkListState.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.model.nip51Lists import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.NoteState -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt index 9e509ba51..80e618b76 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt @@ -20,13 +20,7 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - -import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt index c4b9e7e67..f4db82512 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.NoteState -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleListDecryptionCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListDecryptionCache.kt index 2d1ecacc1..a3f3df679 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListDecryptionCache.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListState.kt index 39d639933..3fc015f51 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListDecryptionCache.kt index 064bfd46b..73ba12906 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListDecryptionCache.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListState.kt index 4f58858b8..388e78d4c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListCard.kt index 30e5bbeb3..25f818e72 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListCard.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.geohashLists -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - data class GeohashListCard( val relays: List, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListState.kt index fce1e5880..dc7eaa534 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.geohashLists -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent import com.vitorpamplona.quartz.utils.Log diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt index 9cc6f0c7e..6e73b068a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt index c12ed2de2..8a1341376 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent import com.vitorpamplona.quartz.utils.Log diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListDecryptionCache.kt index 59fbbee71..13d1ce9d4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListDecryptionCache.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListState.kt index 3aec5f510..738ec1c6d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListState.kt @@ -20,11 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkList.kt index e0e0b7115..74333236f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkList.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Stable import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt index 61c98794f..55f02b4e3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt @@ -20,15 +20,15 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.commons.model.anyNotNullEvent import com.vitorpamplona.amethyst.commons.model.eventIdSet import com.vitorpamplona.amethyst.commons.model.events -import com.vitorpamplona.amethyst.model.filter import com.vitorpamplona.amethyst.commons.model.updateFlow +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.filter import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.update import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListDecryptionCache.kt index 74ad33385..90314fc4a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListDecryptionCache.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.muteList -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListState.kt index 534a03dff..6ad8b31a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.muteList -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt index 7cd417768..16abb8276 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt @@ -20,16 +20,16 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.peopleList -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.commons.model.anyNotNullEvent import com.vitorpamplona.amethyst.commons.model.eventIdSet import com.vitorpamplona.amethyst.commons.model.events -import com.vitorpamplona.amethyst.model.filter import com.vitorpamplona.amethyst.commons.model.updateFlow +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.filter import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.update diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleList.kt index 9cf1c7655..c0f1af20a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleList.kt @@ -20,11 +20,8 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.peopleList -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.model.User import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListDecryptionCache.kt index bbf1bbcfb..30ab6b599 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListDecryptionCache.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.peopleList -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache import com.vitorpamplona.quartz.nip51Lists.muteList.mutedUserIdSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt index 06a208c4e..9f8076c60 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt @@ -20,16 +20,16 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.peopleList -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.commons.model.anyNotNullEvent import com.vitorpamplona.amethyst.commons.model.eventIdSet import com.vitorpamplona.amethyst.commons.model.events -import com.vitorpamplona.amethyst.model.filter import com.vitorpamplona.amethyst.commons.model.updateFlow +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.filter import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.update diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListDecryptionCache.kt index fba8dfe86..2f4fff3cd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListDecryptionCache.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListState.kt index a3bfe4e57..d50c88368 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayLists/RelayListCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayLists/RelayListCard.kt index 93a4e3030..bf12514c4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayLists/RelayListCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayLists/RelayListCard.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.relayLists -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListDecryptionCache.kt index d99d77b32..bf88a3752 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListDecryptionCache.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.searchRelays -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListState.kt index cc6ad368a..3d5b3919f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListState.kt @@ -20,11 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.searchRelays -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.DefaultSearchRelayList import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListDecryptionCache.kt index a97a56abe..276b47abb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListDecryptionCache.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListState.kt index b4cede81f..96250d6f5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt index 1d07e838f..d595164c2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.model.nip53LiveActivities import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.ui.note.toShortDisplay import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/Nip65RelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/Nip65RelayListState.kt index f961edbf0..bdeecd489 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/Nip65RelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/Nip65RelayListState.kt @@ -20,11 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip65RelayList -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.Constants import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListDecryptionCache.kt index e34c72a4b..2ea70cb3a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListDecryptionCache.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip72Communities -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt index 125dcad1b..3b025c79a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt @@ -20,11 +20,11 @@ */ package com.vitorpamplona.amethyst.model.nip72Communities +import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.NoteState -import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt index d36b874a9..6a69cc8e2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip78AppSpecific -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.AccountSyncedSettingsInternal import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.core.JsonMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip96FileStorage/FileStorageServerListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip96FileStorage/FileStorageServerListState.kt index 6d0612fc4..2d78464d1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip96FileStorage/FileStorageServerListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip96FileStorage/FileStorageServerListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip96FileStorage -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent import kotlinx.coroutines.CoroutineScope diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt index 23ddf1da3..de9d963e0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nipB7Blossom -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/LatestByKindAndAuthor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/LatestByKindAndAuthor.kt index dfc9ea1ca..c8b43ab5d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/LatestByKindAndAuthor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/LatestByKindAndAuthor.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.amethyst.model.observables import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.Event import kotlinx.coroutines.flow.MutableStateFlow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/LatestByKindWithETag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/LatestByKindWithETag.kt index c21514186..057b71030 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/LatestByKindWithETag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/LatestByKindWithETag.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.observables -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent import kotlinx.coroutines.flow.MutableStateFlow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountPreferenceStores.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountPreferenceStores.kt index 16f246fdc..28d856840 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountPreferenceStores.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountPreferenceStores.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.preferences -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.Preferences diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountSecretsEncryptedStores.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountSecretsEncryptedStores.kt index 5e25514c4..7f42b168f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountSecretsEncryptedStores.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountSecretsEncryptedStores.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.preferences -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.stringPreferencesKey import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/DataStoreExt.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/DataStoreExt.kt index 486d75054..18f9eac67 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/DataStoreExt.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/DataStoreExt.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.preferences -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/EncryptedDataStore.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/EncryptedDataStore.kt index b57efd277..6c8daf2b2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/EncryptedDataStore.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/EncryptedDataStore.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.preferences -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/KeyStoreEncryption.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/KeyStoreEncryption.kt index 426122328..eca8de687 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/KeyStoreEncryption.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/KeyStoreEncryption.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.preferences -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import android.os.Build import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/TorSharedPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/TorSharedPreferences.kt index 8d78be2e6..0944a4360 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/TorSharedPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/TorSharedPreferences.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.preferences -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import android.content.Context import androidx.compose.runtime.Stable import androidx.datastore.preferences.core.booleanPreferencesKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt index bf29638a1..d4c88552e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.preferences -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import android.content.Context import androidx.appcompat.app.AppCompatDelegate import androidx.compose.runtime.Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UpdatablePropertyFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UpdatablePropertyFlow.kt index 30e7d254b..5daabe519 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UpdatablePropertyFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UpdatablePropertyFlow.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.preferences -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/EmptyRoleBasedHttpClientBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/EmptyRoleBasedHttpClientBuilder.kt index cafb9a503..982083af5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/EmptyRoleBasedHttpClientBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/EmptyRoleBasedHttpClientBuilder.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.privacyOptions -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import okhttp3.OkHttpClient import java.net.InetSocketAddress diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/IRoleBasedHttpClientBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/IRoleBasedHttpClientBuilder.kt index 923a6a6ea..0b75bb3d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/IRoleBasedHttpClientBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/IRoleBasedHttpClientBuilder.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.privacyOptions -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import okhttp3.OkHttpClient interface IRoleBasedHttpClientBuilder { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/RoleBasedHttpClientBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/RoleBasedHttpClientBuilder.kt index b2ce4d961..7d7f61ec6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/RoleBasedHttpClientBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/RoleBasedHttpClientBuilder.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.privacyOptions -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow import com.vitorpamplona.amethyst.ui.tor.TorType diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/SingleRoleBasedHttpClientBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/SingleRoleBasedHttpClientBuilder.kt index 4cc97b3d6..6c3e85304 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/SingleRoleBasedHttpClientBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/SingleRoleBasedHttpClientBuilder.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.privacyOptions -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import okhttp3.OkHttpClient import java.net.InetSocketAddress diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt index 60ecab35f..c9434c50b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.serverList -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.nip02FollowLists.Kind3FollowListState import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt index 144d66e5e..a9e0ff6d9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.serverList -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxOrProxyRelays diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithIndexRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithIndexRelayListsState.kt index d9a5baee7..37cc66c47 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithIndexRelayListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithIndexRelayListsState.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.serverList -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxOrProxyRelays diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithSearchRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithSearchRelayListsState.kt index fbf114c56..825a6d98f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithSearchRelayListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithSearchRelayListsState.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.serverList -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxOrProxyRelays diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedServerListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedServerListState.kt index b0df8e49e..fb011d83e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedServerListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedServerListState.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.serverList -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.nip96FileStorage.FileStorageServerListState import com.vitorpamplona.amethyst.model.nipB7Blossom.BlossomServerListState import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt index 08e3b7ae2..be26a1f19 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.serverList -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt index bcc5ca1b1..16cf45525 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedDecryptionCaches.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedDecryptionCaches.kt index b8d8bc64e..22cc69c34 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedDecryptionCaches.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedDecryptionCaches.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.muteList.MuteListDecryptionCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt index 438d49a7e..17b3d2bf6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.ALL_FOLLOWS import com.vitorpamplona.amethyst.model.ALL_USER_FOLLOWS import com.vitorpamplona.amethyst.model.AROUND_ME diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedFlowsType.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedFlowsType.kt index e0cb002d5..150fa4c99 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedFlowsType.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedFlowsType.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.FlowCollector diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavFilter.kt index e0b393c58..2ec3d0a43 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavFilter.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilter.kt index 1ad010bcf..4cbff4957 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilter.kt @@ -20,9 +20,4 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - interface IFeedTopNavPerRelayFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilterSet.kt index 47281520d..5f922fbeb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilterSet.kt @@ -20,9 +20,4 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - interface IFeedTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/MergedTopFeedAuthorListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/MergedTopFeedAuthorListsState.kt index f26abeae7..0728886f2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/MergedTopFeedAuthorListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/MergedTopFeedAuthorListsState.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxLoaderState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxLoaderState.kt index 21086ff07..a2980535b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxLoaderState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxLoaderState.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.unknown.UnknownTopNavPerRelayFilterSet import kotlinx.coroutines.CoroutineScope diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt index eea9ec4f8..5b1b6f843 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.model.topNavFeeds import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.Constants import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt index 210e37a7a..cd6675eca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.allFollows -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.CommunityRelayLoader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByProxyTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByProxyTopNavFilter.kt index 7346426e8..ac271a340 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByProxyTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByProxyTopNavFilter.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.allFollows -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt index 2c0915993..e451c3129 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.allFollows -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.serverList.MergedFollowListsState import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilter.kt index 4bc3ac67b..af5595c04 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilter.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.allFollows -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilterSet.kt index e1aa10b63..032b4590c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilterSet.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.allFollows -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByOutboxTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByOutboxTopNavFilter.kt index dab91dcd4..b3bf7538e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByOutboxTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByOutboxTopNavFilter.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByProxyTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByProxyTopNavFilter.kt index 234d7827f..6064ce5b2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByProxyTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsByProxyTopNavFilter.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsFeedFlow.kt index e8304833a..24a26e3f7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/AllUserFollowsFeedFlow.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.serverList.MergedFollowListsState import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/Kind3UserFollowsFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/Kind3UserFollowsFeedFlow.kt index 9d65a8d53..ebf0967b8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/Kind3UserFollowsFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allUserFollows/Kind3UserFollowsFeedFlow.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.nip02FollowLists.Kind3FollowListState import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeExpander.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeExpander.kt index 2df4003e8..028931a18 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeExpander.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeExpander.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.fonfon.kgeohash.GeoHash fun compute50kmLine(geoHash: GeoHash): List { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt index 4a2adc1e1..a0c66edc0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter import com.vitorpamplona.amethyst.service.location.LocationState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavFilter.kt index 906c9ad4c..6dce8b3cb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavFilter.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilter.kt index 96b494d71..219c10719 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilter.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilterSet.kt index 62b41fbb4..85564aac7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilterSet.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalFeedFlow.kt index ee3598f96..3aa72e1f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalFeedFlow.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.global -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavFilter.kt index becf80f9e..2b961af8e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavFilter.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.global -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilter.kt index 83c22cb78..772a298dc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilter.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.global -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilterSet.kt index ffa6dd324..2e30e5dc5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilterSet.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.global -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavFilter.kt index 82479f03e..47235adc2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavFilter.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.hashtag -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilter.kt index faf4ddd18..8c5a0406f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilter.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.hashtag -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilterSet.kt index b20c4ba0f..35c5481b0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilterSet.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.hashtag -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavFilter.kt index 32025dd58..57e2e3425 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavFilter.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.CommunityRelayLoader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilter.kt index 89630ad58..dd532c3e9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilter.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter class AllCommunitiesTopNavPerRelayFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilterSet.kt index ed872dd0c..061c40e5a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilterSet.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavFilter.kt index 4c5b2afa7..96ae9d520 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavFilter.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByProxyTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByProxyTopNavFilter.kt index dee3812c2..4489c55ee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByProxyTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByProxyTopNavFilter.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilter.kt index bc34fca89..abd72b1ec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilter.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilterSet.kt index 9863462a4..8809e7496 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilterSet.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavFilter.kt index 48bfbebe2..8a4c3bb4d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavFilter.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilter.kt index 1d37ab727..0c6ba0610 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilter.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter import com.vitorpamplona.quartz.nip01Core.core.Address diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilterSet.kt index 66fbe8e55..c376579fa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilterSet.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavFilter.kt index e9341f8e7..76028ddbe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavFilter.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByProxyTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByProxyTopNavFilter.kt index dabeba417..36eea59a9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByProxyTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByProxyTopNavFilter.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilter.kt index f0f70ca4f..b56f83feb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilter.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilterSet.kt index f40d71441..69d4c1029 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilterSet.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownFeedFlow.kt index ab87abc1a..075f449ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownFeedFlow.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.unknown -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter import kotlinx.coroutines.flow.FlowCollector diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavFilter.kt index b1a63ee0c..c20e5793f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavFilter.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.unknown -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavPerRelayFilterSet.kt index db84c57c1..556f596fc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavPerRelayFilterSet.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds.unknown -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet object UnknownTopNavPerRelayFilterSet : IFeedTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/AccountsTorStateConnector.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/AccountsTorStateConnector.kt index 2ee648a0c..6748f1df1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/AccountsTorStateConnector.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/AccountsTorStateConnector.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.torState -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.CoroutineScope diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayEvaluation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayEvaluation.kt index 30831d901..1041e69cc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayEvaluation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayEvaluation.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.torState -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.ui.tor.TorType import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isLocalHost diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelaySettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelaySettings.kt index 290139085..b0dc31d59 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelaySettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelaySettings.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.torState -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.ui.tor.TorType data class TorRelaySettings( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt index 80835db30..6ec0711f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.torState -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow import com.vitorpamplona.amethyst.ui.tor.TorType diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListDecryptionCache.kt index 3ffc7f6ae..c35dfbaac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListDecryptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListDecryptionCache.kt @@ -20,11 +20,6 @@ */ package com.vitorpamplona.amethyst.model.trustedAssertions -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User - - import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent import com.vitorpamplona.quartz.experimental.trustedAssertions.list.serviceProviderSet import com.vitorpamplona.quartz.experimental.trustedAssertions.list.serviceProviders diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListState.kt index b00c0c70f..67c0ebd29 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListState.kt @@ -20,11 +20,10 @@ */ package com.vitorpamplona.amethyst.model.trustedAssertions -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState -import com.vitorpamplona.amethyst.commons.model.trustedAssertions.TrustProviderListState as ITrustProviderListState import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.experimental.trustedAssertions.list.TrustProviderListEvent import com.vitorpamplona.quartz.experimental.trustedAssertions.list.tags.ProviderTypes import com.vitorpamplona.quartz.experimental.trustedAssertions.list.tags.ServiceProviderTag @@ -43,6 +42,7 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch +import com.vitorpamplona.amethyst.commons.model.trustedAssertions.TrustProviderListState as ITrustProviderListState class TrustProviderListState( val signer: NostrSigner, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt index f876d22a5..42adc194f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt @@ -23,10 +23,10 @@ package com.vitorpamplona.amethyst.service import android.content.Context import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt index 3087ef3ec..1452776d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt @@ -26,9 +26,9 @@ import androidx.core.content.ContextCompat import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendDMNotification import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendZapNotification import com.vitorpamplona.amethyst.ui.note.showAmount diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt index 2d3086dc8..a91a08a84 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.service.relayClient -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.EventCollector diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountObservers.kt index 6afa547d7..d24717964 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountObservers.kt @@ -24,8 +24,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.User import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt index 978f7f376..4693c9e74 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.BundledUpdate import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.IEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/FilterFindFollowMetadataForKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/FilterFindFollowMetadataForKey.kt index a2d843594..afd76dec4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/FilterFindFollowMetadataForKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/FilterFindFollowMetadataForKey.kt @@ -20,12 +20,12 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Constants import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList import com.vitorpamplona.amethyst.model.DefaultSearchRelayList import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt index e66870d7c..1add909d1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt @@ -24,9 +24,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.ChannelState +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt index 0aacd5cf8..db3d6bb25 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.NoteEventLoaderSubAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers.EventWatcherSubAssembler diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssemblerSubscription.kt index 73ca743a4..d0ffe095e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssemblerSubscription.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt index b8495362a..d76b06042 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt index 967c78ddb..dc4240828 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders.UserOutboxFinderSubAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers.UserCardsSubAssembler diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssemblerSubscription.kt index 1fee4ac5f..7e1450331 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssemblerSubscription.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user import android.annotation.SuppressLint import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt index 11c1f728f..d40d4fb69 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt @@ -25,12 +25,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.UserState -import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserOutboxFinderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserOutboxFinderSubAssembler.kt index 07864e04b..76108f540 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserOutboxFinderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserOutboxFinderSubAssembler.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.BaseEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows.pickRelaysToLoadUsers import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt index d718bd403..2f634dbcf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt index 79ac83b98..a8bdc42cd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.commons.model.toHexSet +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState import com.vitorpamplona.amethyst.service.relays.MutableTime diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt index d23a370a6..2b4843562 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.commons.model.toHexSet +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState import com.vitorpamplona.amethyst.service.relays.MutableTime diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt index 0e5cdea78..d3dd7e145 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.BaseEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index a3570e4a1..8387333de 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -73,8 +73,8 @@ import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.viewmodel.compose.viewModel import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.playback.composable.VideoView import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt index 66bba4802..0018215d9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt @@ -31,10 +31,10 @@ import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 4951569b2..c0ca8c5a0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -62,8 +62,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em import com.vitorpamplona.amethyst.commons.compose.produceCachedState import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.commons.richtext.Base64Segment import com.vitorpamplona.amethyst.commons.richtext.BechSegment import com.vitorpamplona.amethyst.commons.richtext.CashuSegment @@ -87,6 +85,8 @@ import com.vitorpamplona.amethyst.commons.richtext.VideoSegment import com.vitorpamplona.amethyst.commons.richtext.WithdrawSegment import com.vitorpamplona.amethyst.model.HashtagIcon import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon import com.vitorpamplona.amethyst.service.CachedRichTextParser import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt index 222a916a3..7bb60c2c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt @@ -33,10 +33,10 @@ import androidx.compose.ui.unit.dp import com.halilibo.richtext.ui.MediaRenderer import com.halilibo.richtext.ui.string.InlineContent import com.halilibo.richtext.ui.string.RichTextString -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.HashtagIcon +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.components.DisplayFullNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt index 5ea2242d5..5950caf61 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt @@ -42,8 +42,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeToMessage diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt index 9df88cb8c..a296c5dc6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt @@ -31,8 +31,8 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt index 7edc16ab3..cf62c2cf5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.feeds import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.BundledInsert import com.vitorpamplona.amethyst.service.BundledUpdate import com.vitorpamplona.amethyst.service.checkNotInMainThread diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt index fcb224691..283735f7a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.feeds import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf -import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.BasicBundledInsert import com.vitorpamplona.amethyst.service.BasicBundledUpdate import com.vitorpamplona.amethyst.service.checkNotInMainThread diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt index b77230fc3..cda6c74fb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt @@ -57,8 +57,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPicture import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt index 452944cbf..fb16001a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt @@ -92,8 +92,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserContactCardsFollowerCount import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt index 247f29c5a..374c68d92 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.navigation.routes -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt index 50b302933..ca5886d93 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt @@ -33,9 +33,9 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt index a3a31d5f9..263b563c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt @@ -71,9 +71,9 @@ import androidx.compose.ui.window.Popup import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteZaps import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt index dffa2b764..a18b3f221 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt @@ -26,9 +26,9 @@ import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt index 73dbee486..819b366c1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt @@ -74,8 +74,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.service.firstFullChar import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEventAndMapNotNull import com.vitorpamplona.amethyst.ui.components.AnimatedBorderTextCornerRadius diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt index 3cc9df1ef..019455527 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt @@ -64,9 +64,9 @@ import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.ui.components.TextSpinner import com.vitorpamplona.amethyst.ui.components.TitleExplainer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt index 1373a92a7..27374f6a0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt @@ -39,9 +39,9 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserAboutMe import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/ShowUserSuggestionList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/ShowUserSuggestionList.kt index e5afc82bf..95b6cdca9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/ShowUserSuggestionList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/ShowUserSuggestionList.kt @@ -39,8 +39,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.searchCommand.UserSearchDataSourceSubscription import com.vitorpamplona.amethyst.ui.note.AboutDisplay import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/UserSuggestionState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/UserSuggestionState.kt index 922f41638..cad37791d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/UserSuggestionState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/UserSuggestionState.kt @@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.ui.note.creators.userSuggestions import androidx.compose.runtime.Stable import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.searchCommand.SearchQueryState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt index e26577355..102cc6f4d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt @@ -55,8 +55,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteReplies import com.vitorpamplona.amethyst.ui.components.ClickableTextColor import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index 445887199..1a99000a7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -33,10 +33,10 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.MediaCompressor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt index 1d93ba573..85074bd44 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt @@ -55,8 +55,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.ClickableTextPrimary import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt index 48bfa2d6b..bbae48b24 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt @@ -38,8 +38,8 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt index f456c26f4..0705aea28 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt @@ -25,11 +25,11 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.layout.ContentScale -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt index f91a1d3d5..7b3ea345f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt @@ -26,10 +26,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.layout.ContentScale import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent import com.vitorpamplona.amethyst.commons.richtext.MediaLocalImage import com.vitorpamplona.amethyst.commons.richtext.MediaLocalVideo +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.LoadNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt index 5e1c1e1de..c79454a9d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt @@ -41,8 +41,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEventAndMap import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt index 990fd77ad..88ccdf5cb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt @@ -45,9 +45,9 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.playback.composable.VideoView import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MedicalData.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MedicalData.kt index 8deff3805..e8606d59e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MedicalData.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MedicalData.kt @@ -39,10 +39,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Bundle import com.vitorpamplona.amethyst.model.FhirElementDatabase import com.vitorpamplona.amethyst.model.LensSpecification +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Patient import com.vitorpamplona.amethyst.model.Practitioner import com.vitorpamplona.amethyst.model.Prism diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PictureDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PictureDisplay.kt index a2677bff3..6b0f27e35 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PictureDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PictureDisplay.kt @@ -35,8 +35,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt index dd8d8e2c9..bf0a07f68 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt @@ -32,8 +32,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt index b3672d396..8432deaa2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt @@ -33,8 +33,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt index afddc9844..3b4eedba0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt @@ -49,8 +49,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.net.toUri import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.countToHumanReadableBytes import com.vitorpamplona.amethyst.ui.components.ShowMoreButton import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt index ece2433af..d102e605a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt @@ -44,8 +44,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.countToHumanReadableBytes import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.components.GenericLoadable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt index b21a24bec..b8c26a7be 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt @@ -42,11 +42,11 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VideoDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VideoDisplay.kt index eecd04680..ab73587d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VideoDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VideoDisplay.kt @@ -25,11 +25,11 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.layout.ContentScale -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index 8ded3eeea..0ba80943e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt index 573ad92ee..6cf7aea7d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt @@ -24,11 +24,11 @@ import android.content.Context import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.ALL_FOLLOWS import com.vitorpamplona.amethyst.model.ALL_USER_FOLLOWS import com.vitorpamplona.amethyst.model.AROUND_ME import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS import com.vitorpamplona.amethyst.service.checkNotInMainThread diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt index 4c6624a12..b31b33843 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt @@ -25,8 +25,8 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.BundledUpdate import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.dal.FeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt index 31813b631..10982b6c8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedContentState import com.vitorpamplona.amethyst.ui.feeds.FeedContentState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index fccc7eba7..006e2cf70 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -40,19 +40,18 @@ import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.GenericBaseCache import com.vitorpamplona.amethyst.commons.compose.GenericBaseCacheAsync -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.UiSettingsFlow import com.vitorpamplona.amethyst.model.UrlCachedPreviewer +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel -import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers -import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.observables.CreatedAtComparator import com.vitorpamplona.amethyst.model.privacyOptions.EmptyRoleBasedHttpClientBuilder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt index 7f591df71..0bf7a6cb9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.privateChats.ChatroomList import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPrivateFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPrivateFeedFilter.kt index 489334ea3..85cdc1d77 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPrivateFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPrivateFeedFilter.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.dal -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.FeedFilter class BookmarkPrivateFeedFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPublicFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPublicFeedFilter.kt index 561af0e2c..34a3962fb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPublicFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPublicFeedFilter.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.dal -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.FeedFilter class BookmarkPublicFeedFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt index 1e5393762..34c7600c2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt @@ -48,8 +48,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Constants +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt index 1cc768530..f48d0b3af 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt @@ -30,11 +30,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedFilter.kt index 34f8e9ba5..5dfd03295 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedFilter.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.dal -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.ChangesFlowFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt index 13a3f2616..dd6676f11 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt @@ -24,8 +24,8 @@ import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.ChangesFlowFilter import com.vitorpamplona.amethyst.ui.dal.ListChange import com.vitorpamplona.amethyst.ui.feeds.FeedContentState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index 66af69f87..c2ce04d5e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -32,10 +32,10 @@ import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt index 822507e72..db66af206 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.ChangesFlowFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt index 469b001b6..97fa81a62 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt @@ -40,8 +40,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt index 0f496f606..b66031baa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt @@ -36,8 +36,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index abc3d01ff..4d3a5b6b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -33,12 +33,12 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt index 4483051ca..4a786a408 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.replace import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt index 82a3423d1..5e9426ee1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.replace import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedFilter.kt index f354dd807..0e6b9efad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedFilter.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.dal -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedViewModel.kt index 32e32387c..abd71e4ae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.ui.screen.FeedViewModel class CommunityFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedFilter.kt index cd8b2efd7..114fb2f67 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedFilter.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.dal -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedViewModel.kt index 449407ae2..14b423fac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.ui.screen.FeedViewModel class CommunityModerationFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt index 050f48e9f..1e9464341 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.filterIntoSet import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt index 4e4f9fd7c..745f31a8c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt index 027e32d40..0d4c93a03 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt @@ -42,8 +42,8 @@ import coil3.compose.AsyncImage import coil3.compose.AsyncImagePainter import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.ParticipantListBuilder +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByProxyTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt index 40a4a5b12..0f29d1b81 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.filterIntoSet import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt index 4d12421ef..529f49992 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt @@ -38,8 +38,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt index c96f13be4..856de6e36 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByProxyTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt index 4b56a6acb..58ec5f2a1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt @@ -41,10 +41,10 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ParticipantListBuilder +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt index be38006fa..e1089042f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt @@ -43,10 +43,10 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ParticipantListBuilder +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt index 6890729d8..9ab2c6f8b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.mapNotNullIntoSet import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.FilterByListParams diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt index d8d47a564..5c3e8ef37 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.amethyst.model.filterIntoSet import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/DiscoverMarketplaceFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/DiscoverMarketplaceFeedFilter.kt index 8153489b3..65b26e890 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/DiscoverMarketplaceFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/DiscoverMarketplaceFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.filterIntoSet import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.FilterByListParams diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index b762be777..90bf27713 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -33,10 +33,10 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.MediaCompressor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/dal/DraftEventsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/dal/DraftEventsFeedFilter.kt index b7e17df5d..21364ee0e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/dal/DraftEventsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/dal/DraftEventsFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.dal -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.filterIntoSet import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryResponseFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryResponseFilter.kt index 24f169ff4..5e1102248 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryResponseFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryResponseFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.dal -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.observables.CreatedAtComparator import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.FilterByListParams diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedFilter.kt index ffcbc86fb..4895b9f7b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedFilter.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows.AllUserFollowsByOutboxTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows.AllUserFollowsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedViewModel.kt index 680c1b089..3851b9dfb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.ui.screen.FeedViewModel class FollowPackFeedConversationsFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt index 29fc09689..ac8b756a2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.filterIntoSet import com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows.AllUserFollowsByOutboxTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows.AllUserFollowsByProxyTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedViewModel.kt index 1b4d05062..74b9159a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.ui.screen.FeedViewModel class FollowPackFeedNewThreadFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersFeedFilter.kt index 64068dd08..2329eca2c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersFeedFilter.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache.checkGetOrCreateUser +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersUserFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersUserFeedViewModel.kt index 97269984b..2eff19754 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersUserFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersUserFeedViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel class FollowPackMembersUserFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterAssembler.kt index 1f43ad014..ef3f1a91a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterAssembler.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.datasource import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt index 0d9e13dd3..60418c3aa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.dal -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt index f72519b80..f961365e3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.dal -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index cf9652a1e..652cc425f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -36,10 +36,10 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState.EmojiMedia import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.CompressorQuality diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeConversationsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeConversationsFeedFilter.kt index 4c7ba220c..ad9757837 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeConversationsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeConversationsFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt index 4e3829fda..62f55d898 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt index f71c6510a..be69e6a00 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.filterIntoSet import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/ShowUserSuggestions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/ShowUserSuggestions.kt index ef7627709..1e4f1c93a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/ShowUserSuggestions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/ShowUserSuggestions.kt @@ -63,8 +63,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.searchCommand.UserSearchDataSourceSubscription import com.vitorpamplona.amethyst.ui.note.AboutDisplay import com.vitorpamplona.amethyst.ui.note.ClearTextIcon diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListScreen.kt index 95c5e9885..ff72e8bd8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListScreen.kt @@ -72,8 +72,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListViewModel.kt index 4c66116eb..168f8484b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListViewModel.kt @@ -26,9 +26,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import kotlinx.coroutines.Dispatchers diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackScreen.kt index d1eda7039..743fdf8c1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackScreen.kt @@ -65,8 +65,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackViewModel.kt index 6d5afad15..c51741b07 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackViewModel.kt @@ -26,9 +26,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent import kotlinx.coroutines.Dispatchers diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/PeopleListItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/PeopleListItem.kt index 3cd843690..5c9b84e41 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/PeopleListItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/PeopleListItem.kt @@ -55,8 +55,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleList import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserScreen.kt index 9563ac307..9333c185f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserScreen.kt @@ -36,8 +36,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt index ddc90aac7..8dc2fd2f4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt @@ -24,11 +24,11 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.BundledInsert import com.vitorpamplona.amethyst.service.BundledUpdate import com.vitorpamplona.amethyst.service.checkNotInMainThread diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt index 232ce75e8..daa23cac1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt @@ -25,10 +25,10 @@ import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel import com.patrykandpatrick.vico.core.common.data.ExtraStore import com.patrykandpatrick.vico.core.common.data.MutableExtraStore -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.BundledInsert import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.note.showAmountInteger diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt index fdcd0f3d9..fac9b7661 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.dal -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.filterIntoSet import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt index cc6c823ab..4f7d249be 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt @@ -45,8 +45,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.ReusableZapButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt index 060c90c20..19ba54057 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt @@ -33,10 +33,10 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.MediaCompressor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt index c7fc331cb..71eb5802c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt @@ -58,8 +58,8 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.IntSize import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenUser import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedFilter.kt index 61caaaa53..a094f7821 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedFilter.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.dal -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedViewModel.kt index fed67ff3e..8a84b7086 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.screen.FeedViewModel class UserProfileBookmarksFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedFilter.kt index 0c6d4ac73..8ecaead08 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedFilter.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.conversations.dal -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedViewModel.kt index 0291559c3..3b276e1a7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.conversations.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.screen.FeedViewModel class UserProfileConversationsFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileFollowers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileFollowers.kt index b3d8bc27a..42d758ae4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileFollowers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileFollowers.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMedia.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMedia.kt index 848429e4b..96e2b5a89 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMedia.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMedia.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt index 946127f67..dbbd86c0e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileZapReceived.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileZapReceived.kt index 62b5ca358..b48a22fb8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileZapReceived.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileZapReceived.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersFeedFilter.kt index abd424e28..4d81c18ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers.dal -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.dal.FeedFilter class UserProfileFollowersFeedFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersUserFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersUserFeedViewModel.kt index 864f60478..674421db3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersUserFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersUserFeedViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel class UserProfileFollowersUserFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsFeedFilter.kt index 475a56500..0208a8a55 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows.dal -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsUserFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsUserFeedViewModel.kt index 10d08d985..ea4e65f2a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsUserFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsUserFeedViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel class UserProfileFollowsUserFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt index 8b0e1db04..575d63492 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt @@ -45,11 +45,11 @@ import androidx.media3.ui.PlayerView import coil3.compose.AsyncImagePainter import coil3.compose.SubcomposeAsyncImage import coil3.compose.SubcomposeAsyncImageContent -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.commons.richtext.MediaUrlContent import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.RichTextParser.Companion.isVideoUrl +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.playback.composable.GetVideoController import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.GetMediaItem import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt index e07e65b15..63955e745 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.dal -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.amethyst.ui.dal.FilterByListParams diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedViewModel.kt index d283e565e..93ff86b75 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.screen.FeedViewModel class UserProfileGalleryFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawBanner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawBanner.kt index 8d6446762..579c6ae2a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawBanner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawBanner.kt @@ -37,8 +37,8 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserBanner import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog import com.vitorpamplona.amethyst.ui.painterRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt index bcbb6b113..5c9b35109 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt @@ -49,8 +49,8 @@ import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/UserProfileAppRecommendationsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/UserProfileAppRecommendationsFeedFilter.kt index 00ebd03c9..9bfda48aa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/UserProfileAppRecommendationsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/UserProfileAppRecommendationsFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt index 7762a611d..dbccec86d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt @@ -34,9 +34,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEventAndMapNotNull import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt index b447f5436..22a90a03e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt @@ -20,11 +20,11 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual.dal +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedViewModel.kt index 66a71f8f9..36dfbc4ce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.screen.FeedViewModel class UserProfileMutualFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt index 60c6c7d7e..3b4b1b727 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt @@ -20,11 +20,11 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads.dal +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadsFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadsFeedViewModel.kt index a13994af6..0a92c613f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadsFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadsFeedViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.screen.FeedViewModel class UserProfileNewThreadsFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt index c12009031..81440c8ec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt @@ -25,8 +25,8 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.RelayInfo +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenAccountsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenAccountsFeedFilter.kt index 31ba32e76..38d5541ae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenAccountsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenAccountsFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.dal -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CancellationException diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedFilter.kt index a27469c58..542fa1f92 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.dal -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.dal.FeedFilter class SpammerAccountsFeedFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index 1a0c5e20d..e3f5068b9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -73,9 +73,9 @@ import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeCommunityApprovalNeedStatus import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid import com.vitorpamplona.amethyst.ui.components.GenericLoadable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedFilter.kt index f758e2d69..f1ae4c13a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedFilter.kt @@ -21,10 +21,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal import androidx.compose.runtime.Immutable -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LevelSignature +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ThreadAssembler import com.vitorpamplona.amethyst.model.ThreadLevelCalculator import com.vitorpamplona.amethyst.ui.dal.FeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt index 45cdfd83d..8e0ad15d1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ThreadAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingAddressables import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingEvents diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt index bb2e9851a..6646dac18 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt @@ -20,12 +20,11 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.dal -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.filterIntoSet import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt index e034675e2..216691ae0 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.LocalCache.getOrCreateAddressableNoteInternal +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.actions.Dao import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.quartz.nip01Core.core.Address From 6553012372421af13f6a5df20929057b61fc5720 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 1 Jan 2026 12:22:07 -0500 Subject: [PATCH 026/137] Uses aliases for the Filter class --- .../quartz/nip01Core/relay/filters/Filter.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt index 2d1fb0188..2feae643c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt @@ -22,6 +22,8 @@ package com.vitorpamplona.quartz.nip01Core.relay.filters import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Kind import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper import com.vitorpamplona.quartz.nip01Core.core.OptimizedSerializable import com.vitorpamplona.quartz.utils.Log @@ -44,9 +46,9 @@ import com.vitorpamplona.quartz.utils.Log * follow Nostr requirements (64-char hex, onion addresses) and logs errors for invalid inputs. */ class Filter( - val ids: List? = null, - val authors: List? = null, - val kinds: List? = null, + val ids: List? = null, + val authors: List? = null, + val kinds: List? = null, val tags: Map>? = null, val tagsAll: Map>? = null, val since: Long? = null, From 90f38c3a588f86e6ea19fa8385f4dc2af60f0084 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 1 Jan 2026 12:22:20 -0500 Subject: [PATCH 027/137] adds a isRegular filter for the Kind alias --- .../kotlin/com/vitorpamplona/quartz/nip01Core/core/Kind.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Kind.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Kind.kt index 0aea6ba6f..7bf8cefb3 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Kind.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Kind.kt @@ -25,6 +25,8 @@ import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent typealias Kind = Int +fun Kind.isRegular() = this > 0 && this != ContactListEvent.KIND && this < 10_000 + fun Kind.isEphemeral() = this >= 20_000 && this < 30_000 fun Kind.isReplaceable() = this == MetadataEvent.KIND || this == ContactListEvent.KIND || (this >= 10_000 && this < 20_000) From 507256e0dd419fecffbd5cfa5693a6355dc4e033 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 1 Jan 2026 12:36:26 -0500 Subject: [PATCH 028/137] Fixes Or builder --- .../store/sqlite/sql/WhereClauseBuilder.kt | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/WhereClauseBuilder.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/WhereClauseBuilder.kt index 817ab0ea2..ad2edfb5c 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/WhereClauseBuilder.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/WhereClauseBuilder.kt @@ -86,7 +86,7 @@ class WhereClauseBuilder { fun and(block: WhereClauseBuilder.() -> Unit) = apply { val builder = WhereClauseBuilder().apply(block) - val builtCondition = builder.build() + val builtCondition = builder.buildAnd() if (builtCondition != null) { conditions.add(builtCondition) } @@ -95,22 +95,29 @@ class WhereClauseBuilder { fun or(block: WhereClauseBuilder.() -> Unit) = apply { val builder = WhereClauseBuilder().apply(block) - val builtCondition = builder.build() + val builtCondition = builder.buildOr() if (builtCondition != null) { conditions.add(builtCondition) } } - fun build(): Condition? = + fun buildAnd(): Condition? = when (conditions.size) { 0 -> null 1 -> conditions.first() else -> Condition.And(conditions.toList()) } + + fun buildOr(): Condition? = + when (conditions.size) { + 0 -> null + 1 -> conditions.first() + else -> Condition.Or(conditions.toList()) + } } fun where(block: WhereClauseBuilder.() -> Unit): WhereClause { - val condition = WhereClauseBuilder().apply(block).build() ?: Condition.And(emptyList()) + val condition = WhereClauseBuilder().apply(block).buildAnd() ?: Condition.And(emptyList()) return SqlSelectionBuilder(condition).build() } From 1978c8e17eb11f870bbb0be6c50eab6d0266bd85 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 1 Jan 2026 12:37:13 -0500 Subject: [PATCH 029/137] Faster way of coding equals to null in the sql builder --- .../store/sqlite/sql/SqlSelectionBuilder.kt | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/SqlSelectionBuilder.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/SqlSelectionBuilder.kt index 7e898c244..59c2946f0 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/SqlSelectionBuilder.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/SqlSelectionBuilder.kt @@ -39,12 +39,20 @@ class SqlSelectionBuilder( private fun buildCondition(cond: Condition): String = when (cond) { is Condition.Equals -> { - selectionArgs.add(cond.value.toString()) - "${cond.column} = ?" + if (cond.value == null) { + "${cond.column} IS NULL" + } else { + selectionArgs.add(cond.value.toString()) + "${cond.column} = ?" + } } is Condition.NotEquals -> { - selectionArgs.add(cond.value.toString()) - "${cond.column} != ?" + if (cond.value == null) { + "${cond.column} IS NULL" + } else { + selectionArgs.add(cond.value.toString()) + "${cond.column} != ?" + } } is Condition.GreaterThan -> { selectionArgs.add(cond.value.toString()) From 9d67d5d6dc9800d25f3964ee04b67e1b8c1f06cb Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 1 Jan 2026 12:37:32 -0500 Subject: [PATCH 030/137] Implements a raw add on to the query builder --- .../quartz/nip01Core/store/sqlite/sql/Condition.kt | 4 ++++ .../quartz/nip01Core/store/sqlite/sql/SqlSelectionBuilder.kt | 3 +++ .../quartz/nip01Core/store/sqlite/sql/WhereClauseBuilder.kt | 2 ++ 3 files changed, 9 insertions(+) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/Condition.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/Condition.kt index c2cd3a6e6..738cef423 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/Condition.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/Condition.kt @@ -21,6 +21,10 @@ package com.vitorpamplona.quartz.nip01Core.store.sqlite.sql sealed class Condition { + data class Raw( + val condition: String, + ) : Condition() + data class Equals( val column: String, val value: Any?, diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/SqlSelectionBuilder.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/SqlSelectionBuilder.kt index 59c2946f0..78606720d 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/SqlSelectionBuilder.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/SqlSelectionBuilder.kt @@ -38,6 +38,9 @@ class SqlSelectionBuilder( */ private fun buildCondition(cond: Condition): String = when (cond) { + is Condition.Raw -> { + cond.condition + } is Condition.Equals -> { if (cond.value == null) { "${cond.column} IS NULL" diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/WhereClauseBuilder.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/WhereClauseBuilder.kt index ad2edfb5c..1a42921f6 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/WhereClauseBuilder.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/WhereClauseBuilder.kt @@ -23,6 +23,8 @@ package com.vitorpamplona.quartz.nip01Core.store.sqlite.sql class WhereClauseBuilder { private val conditions = mutableListOf() + fun raw(condition: String) = apply { conditions.add(Condition.Raw(condition)) } + fun equals( column: String, value: Any?, From 45df452a38ff0398eaba3bebbcb2e1d7eddb4e2c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 1 Jan 2026 12:37:54 -0500 Subject: [PATCH 031/137] Adds stable to the desktop account manager --- .../vitorpamplona/amethyst/commons/account/AccountManager.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt index 6fb4df3e3..777bf0f2e 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.commons.account +import androidx.compose.runtime.Stable import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair @@ -45,6 +46,7 @@ sealed class AccountState { ) : AccountState() } +@Stable class AccountManager { private val _accountState = MutableStateFlow(AccountState.LoggedOut) val accountState: StateFlow = _accountState.asStateFlow() From 307703cbb7aa35823ffc495a72f153eed2e7acdf Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 1 Jan 2026 12:47:35 -0500 Subject: [PATCH 032/137] Explains ID index --- .../quartz/nip01Core/store/sqlite/EventIndexesModule.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt index e8410d5a5..c30fda299 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt @@ -63,6 +63,7 @@ class EventIndexesModule( """.trimIndent(), ) + // queries by ID (load events) db.execSQL("CREATE UNIQUE INDEX event_headers_id ON event_headers (id)") db.execSQL("CREATE INDEX query_by_kind_pubkey_dtag_idx ON event_headers (kind, pubkey, d_tag)") db.execSQL("CREATE INDEX query_by_created_at_id ON event_headers (created_at DESC, id)") From f48eba6a8eb96d3c805149c33698386fa70fbc67 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 1 Jan 2026 12:48:22 -0500 Subject: [PATCH 033/137] No need for this index since it will be covered by addressable_idx --- .../quartz/nip01Core/store/sqlite/EventIndexesModule.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt index c30fda299..035cc0a40 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt @@ -65,7 +65,6 @@ class EventIndexesModule( // queries by ID (load events) db.execSQL("CREATE UNIQUE INDEX event_headers_id ON event_headers (id)") - db.execSQL("CREATE INDEX query_by_kind_pubkey_dtag_idx ON event_headers (kind, pubkey, d_tag)") db.execSQL("CREATE INDEX query_by_created_at_id ON event_headers (created_at DESC, id)") // need to check if this is actually needed. From b7c7eabf2ac9f2e223784824530e8ec93a2189e3 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 1 Jan 2026 12:48:48 -0500 Subject: [PATCH 034/137] Explains limit index --- .../quartz/nip01Core/store/sqlite/EventIndexesModule.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt index 035cc0a40..bd3d90ef3 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt @@ -65,6 +65,8 @@ class EventIndexesModule( // queries by ID (load events) db.execSQL("CREATE UNIQUE INDEX event_headers_id ON event_headers (id)") + + // queries by limit (latest records), since, until (sync all) alone db.execSQL("CREATE INDEX query_by_created_at_id ON event_headers (created_at DESC, id)") // need to check if this is actually needed. From 8affb2564b016e5317aaeba169c3a2582a9a4004 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 1 Jan 2026 12:49:12 -0500 Subject: [PATCH 035/137] explains the fk index on tags --- .../quartz/nip01Core/store/sqlite/EventIndexesModule.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt index bd3d90ef3..a7e887c33 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt @@ -72,6 +72,7 @@ class EventIndexesModule( // need to check if this is actually needed. db.execSQL("CREATE INDEX query_by_created_at_kind_key ON event_headers (created_at DESC, kind, pubkey)") + // makes deletions on the event_header fast db.execSQL("CREATE INDEX fk_event_tags_header_id ON event_tags (event_header_row_id)") // This is a very slow index to build (half the insert time goes here) but it is extremely effective. From aac880c575dce4554697aa9faf6497483c8445ba Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 1 Jan 2026 12:51:43 -0500 Subject: [PATCH 036/137] Range queries are not effective as the first argument, switching to kind, key, created_at index, which also almost invalidates the need for the Replaceable idx and is only kept for the unique key consistency --- .../nip01Core/store/sqlite/ReplaceableTest.kt | 52 +++++++++---------- .../store/sqlite/EventIndexesModule.kt | 4 +- .../store/sqlite/ReplaceableModule.kt | 3 +- 3 files changed, 27 insertions(+), 32 deletions(-) diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/ReplaceableTest.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/ReplaceableTest.kt index 97c8aa75d..182414ef2 100644 --- a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/ReplaceableTest.kt +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/ReplaceableTest.kt @@ -113,17 +113,16 @@ class ReplaceableTest { @Test fun testTriggersIndexUsageKind0() { - val explainer = - db.store.explainQuery( - """ - SELECT * FROM event_headers - WHERE - event_headers.kind = 0 AND - event_headers.pubkey = 'aa' AND - event_headers.created_at < 1766686500 AND - ((event_headers.kind IN (0, 3)) OR (event_headers.kind >= 10000 AND event_headers.kind < 20000)); - """.trimIndent(), - ) + val sql = + """ + SELECT * FROM event_headers + WHERE + event_headers.kind = 0 AND + event_headers.pubkey = 'aa' AND + event_headers.created_at < 1766686500 + """.trimIndent() + + val explainer = db.store.explainQuery(sql) assertEquals( """ @@ -131,9 +130,8 @@ class ReplaceableTest { WHERE event_headers.kind = 0 AND event_headers.pubkey = 'aa' AND - event_headers.created_at < 1766686500 AND - ((event_headers.kind IN (0, 3)) OR (event_headers.kind >= 10000 AND event_headers.kind < 20000)); - └── SEARCH event_headers USING INDEX replaceable_idx (kind=? AND pubkey=?) + event_headers.created_at < 1766686500 + └── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=? AND created_at= 10000 AND event_headers.kind < 20000)); - """.trimIndent(), - ) + val sql = + """ + SELECT * FROM event_headers + WHERE + event_headers.kind = 3 AND + event_headers.pubkey = 'aa' AND + event_headers.created_at < 1766686500 + """.trimIndent() + + val explainer = db.store.explainQuery(sql) assertEquals( """ @@ -159,9 +156,8 @@ class ReplaceableTest { WHERE event_headers.kind = 3 AND event_headers.pubkey = 'aa' AND - event_headers.created_at < 1766686500 AND - ((event_headers.kind IN (0, 3)) OR (event_headers.kind >= 10000 AND event_headers.kind < 20000)); - └── SEARCH event_headers USING INDEX replaceable_idx (kind=? AND pubkey=?) + event_headers.created_at < 1766686500 + └── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=? AND created_at= 10000 AND event_headers.kind < 20000)); + event_headers.created_at < NEW.created_at; END; """.trimIndent(), ) From f6d920366d51a29ba9e57e0ff4eb8812c65f4de1 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 1 Jan 2026 12:53:02 -0500 Subject: [PATCH 037/137] Fixes some of the tests --- .../quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt index 46c836c0d..c2ef8fb7b 100644 --- a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt @@ -223,7 +223,7 @@ class QueryAssemblerTest { ON event_headers.row_id = filtered.row_id ORDER BY created_at DESC, id ├── CO-ROUTINE filtered - │ ├── SEARCH event_headers USING INDEX query_by_kind_pubkey_dtag_idx (kind=?) + │ ├── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=?) │ └── USE TEMP B-TREE FOR ORDER BY ├── SCAN filtered ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) @@ -254,7 +254,7 @@ class QueryAssemblerTest { ON event_headers.row_id = filtered.row_id ORDER BY created_at DESC, id ├── CO-ROUTINE filtered - │ ├── SEARCH event_headers USING INDEX query_by_kind_pubkey_dtag_idx (kind=?) + │ ├── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=?) │ └── USE TEMP B-TREE FOR ORDER BY ├── SCAN filtered ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) @@ -457,7 +457,7 @@ class QueryAssemblerTest { ON event_headers.row_id = filtered.row_id ORDER BY created_at DESC, id ├── CO-ROUTINE filtered - │ ├── SEARCH event_headers USING INDEX query_by_kind_pubkey_dtag_idx (kind=? AND pubkey=?) + │ ├── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=?) │ └── USE TEMP B-TREE FOR ORDER BY ├── SCAN filtered ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) From 238d4e0004e7d2c28ab49d0eee12c5e691b87106 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 1 Jan 2026 14:52:11 -0500 Subject: [PATCH 038/137] No need for the fts module anymore --- .../quartz/nip01Core/store/sqlite/EventIndexesModule.kt | 1 - .../quartz/nip01Core/store/sqlite/SQLiteEventStore.kt | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt index c474b6e11..75e6550f4 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt @@ -28,7 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent class EventIndexesModule( - val fts: FullTextSearchModule, val hasher: (db: SQLiteDatabase) -> TagNameValueHasher, val tagIndexStrategy: IndexingStrategy = DefaultIndexingStrategy(), ) : IModule { diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt index 3796b019d..bc9c837e4 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt @@ -50,7 +50,7 @@ class SQLiteEventStore( val seedModule = SeedModule() val fullTextSearchModule = FullTextSearchModule() - val eventIndexModule = EventIndexesModule(fullTextSearchModule, seedModule::hasher, tagIndexStrategy) + val eventIndexModule = EventIndexesModule(seedModule::hasher, tagIndexStrategy) val replaceableModule = ReplaceableModule() val addressableModule = AddressableModule() From 291db1f30b7c15f8a3a2419b82a7a75bf9b0b9f1 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 1 Jan 2026 14:57:48 -0500 Subject: [PATCH 039/137] duplicates kinds and pubkey on the tags table to take complex queries under 100 microseconds --- .../nip01Core/store/sqlite/DeletionTest.kt | 8 +- .../store/sqlite/QueryAssemblerTest.kt | 182 ++++++++++++++++-- .../store/sqlite/DeletionRequestModule.kt | 8 +- .../store/sqlite/EventIndexesModule.kt | 14 +- .../nip01Core/store/sqlite/QueryBuilder.kt | 39 ++-- 5 files changed, 207 insertions(+), 44 deletions(-) diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionTest.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionTest.kt index bb35f07be..064024322 100644 --- a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionTest.kt +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionTest.kt @@ -227,9 +227,9 @@ class DeletionTest { ON event_headers.row_id = event_tags.event_header_row_id WHERE event_tags.tag_hash IN (3221122, 223322) AND - event_tags.created_at >= 1766686500 AND - event_headers.kind = 5 AND - event_headers.pubkey_owner_hash = 22332323 + event_tags.kind = 5 AND + event_tags.pubkey_hash = 22332323 AND + event_tags.created_at >= 1766686500 """.trimIndent() val explainer = @@ -238,7 +238,7 @@ class DeletionTest { TestCase.assertEquals( """ ${sql.replace("\n","\n ")} - ├── SEARCH event_tags USING INDEX query_by_tags_hash (tag_hash=? AND created_at>?) + ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind_pubkey (tag_hash=? AND kind=? AND pubkey_hash=? AND created_at>?) └── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) """.trimIndent(), explainer, diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt index c2ef8fb7b..05f30020c 100644 --- a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt @@ -22,8 +22,13 @@ package com.vitorpamplona.quartz.nip01Core.store.sqlite import android.content.Context import androidx.test.core.app.ApplicationProvider +import com.vitorpamplona.quartz.experimental.relationshipStatus.ContactCardEvent +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent +import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent +import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent +import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import junit.framework.TestCase import junit.framework.TestCase.assertEquals import org.junit.After @@ -82,13 +87,12 @@ class QueryAssemblerTest { """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE (event_tags.tag_hash = "2657743813502222172") AND (event_tags.created_at >= "1750889190") AND (event_headers.kind = "5") AND (event_headers.pubkey = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d") + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "2657743813502222172") AND (event_tags.kind = "5") AND (event_tags.pubkey_hash = "1730514094536529999") AND (event_tags.created_at >= "1750889190") ) AS filtered ON event_headers.row_id = filtered.row_id ORDER BY created_at DESC, id ├── CO-ROUTINE filtered - │ ├── SEARCH event_tags USING INDEX query_by_tags_hash (tag_hash=? AND created_at>?) - │ ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind_pubkey (tag_hash=? AND kind=? AND pubkey_hash=? AND created_at>?) │ └── USE TEMP B-TREE FOR DISTINCT ├── SCAN filtered ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) @@ -280,13 +284,12 @@ class QueryAssemblerTest { """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_headers.kind = "3") ORDER BY event_tags.created_at DESC LIMIT 30 + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") ORDER BY event_tags.created_at DESC LIMIT 30 ) AS filtered ON event_headers.row_id = filtered.row_id ORDER BY created_at DESC, id ├── CO-ROUTINE filtered - │ ├── SEARCH event_tags USING INDEX query_by_tags_hash (tag_hash=?) - │ ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=? AND kind=?) │ └── USE TEMP B-TREE FOR DISTINCT ├── SCAN filtered ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) @@ -342,13 +345,12 @@ class QueryAssemblerTest { """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_headers.kind = "3") ORDER BY event_tags.created_at DESC LIMIT 30 + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") ORDER BY event_tags.created_at DESC LIMIT 30 ) AS filtered ON event_headers.row_id = filtered.row_id ORDER BY created_at DESC, id ├── CO-ROUTINE filtered - │ ├── SEARCH event_tags USING INDEX query_by_tags_hash (tag_hash=?) - │ ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=? AND kind=?) │ └── USE TEMP B-TREE FOR DISTINCT ├── SCAN filtered ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) @@ -374,13 +376,12 @@ class QueryAssemblerTest { """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_headers.pubkey = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d") ORDER BY event_tags.created_at DESC LIMIT 30 + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.pubkey_hash = "1730514094536529999") ORDER BY event_tags.created_at DESC LIMIT 30 ) AS filtered ON event_headers.row_id = filtered.row_id ORDER BY created_at DESC, id ├── CO-ROUTINE filtered │ ├── SEARCH event_tags USING INDEX query_by_tags_hash (tag_hash=?) - │ ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) │ └── USE TEMP B-TREE FOR DISTINCT ├── SCAN filtered ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) @@ -410,14 +411,13 @@ class QueryAssemblerTest { """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_tags as event_tagsIn1 ON event_tagsIn1.event_header_row_id = event_tags.event_header_row_id AND event_tagsIn1.created_at = event_tags.created_at INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tagsIn1.tag_hash = "-6379614208644810021") AND (event_headers.kind = "1") ORDER BY event_tags.created_at DESC LIMIT 30 + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_tags as event_tagsIn1 ON event_tagsIn1.event_header_row_id = event_tags.event_header_row_id AND event_tagsIn1.created_at = event_tags.created_at WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tagsIn1.tag_hash = "-6379614208644810021") AND (event_tags.kind = "1") ORDER BY event_tags.created_at DESC LIMIT 30 ) AS filtered ON event_headers.row_id = filtered.row_id ORDER BY created_at DESC, id ├── CO-ROUTINE filtered - │ ├── SEARCH event_tags USING INDEX query_by_tags_hash (tag_hash=?) - │ ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - │ ├── SEARCH event_tagsIn1 USING INDEX query_by_tags_hash (tag_hash=? AND event_header_row_id=?) + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=? AND kind=?) + │ ├── SEARCH event_tagsIn1 USING INDEX query_by_tags_hash (tag_hash=? AND created_at=?) │ └── USE TEMP B-TREE FOR DISTINCT ├── SCAN filtered ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) @@ -519,7 +519,7 @@ class QueryAssemblerTest { ON event_headers.row_id = filtered.row_id ORDER BY created_at DESC, id ├── CO-ROUTINE filtered - │ ├── SEARCH event_tags USING INDEX query_by_tags_hash (tag_hash=?) + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=?) │ ├── SEARCH event_tagsAll0_1 USING INDEX query_by_tags_hash (tag_hash=? AND created_at=?) │ └── USE TEMP B-TREE FOR DISTINCT ├── SCAN filtered @@ -548,13 +548,12 @@ class QueryAssemblerTest { """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_headers.kind = "3") AND (event_headers.pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") ORDER BY event_tags.created_at DESC LIMIT 500 + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") AND (event_tags.pubkey_hash = "5446767199141196776") ORDER BY event_tags.created_at DESC LIMIT 500 ) AS filtered ON event_headers.row_id = filtered.row_id ORDER BY created_at DESC, id ├── CO-ROUTINE filtered - │ ├── SEARCH event_tags USING INDEX query_by_tags_hash (tag_hash=?) - │ ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind_pubkey (tag_hash=? AND kind=? AND pubkey_hash=?) │ └── USE TEMP B-TREE FOR DISTINCT ├── SCAN filtered ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) @@ -563,4 +562,149 @@ class QueryAssemblerTest { sql, ) } + + @Test + fun testFollowersSinceNov2025() { + val sql = + explain( + Filter( + kinds = listOf(ContactListEvent.KIND), + tags = + mapOf( + "p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + ), + since = 1764553447, // Nov 2025 + ), + ) + println(sql) + assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") AND (event_tags.created_at >= "1764553447") + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY created_at DESC, id + ├── CO-ROUTINE filtered + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=? AND kind=? AND created_at>?) + │ └── USE TEMP B-TREE FOR DISTINCT + ├── SCAN filtered + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + sql, + ) + } + + @Test + fun testAllAddressablesOfAKindDownload() { + val sql = + explain( + Filter( + kinds = listOf(DraftWrapEvent.KIND), + authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + since = 1764553447, // Nov 2025 + ), + ) + println(sql) + assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT event_headers.row_id as row_id FROM event_headers WHERE (event_headers.kind = "31234") AND (event_headers.pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (event_headers.created_at >= "1764553447") AND ((event_headers.kind >= 30000 AND event_headers.kind < 40000)) + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY created_at DESC, id + ├── SEARCH event_headers USING COVERING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=? AND created_at>?) + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + sql, + ) + } + + @Test + fun testReplaceablesOfMultipleKindsDownloadByDateLimit() { + val sql = + explain( + Filter( + kinds = listOf(MetadataEvent.KIND, SearchRelayListEvent.KIND, AdvertisedRelayListEvent.KIND), + authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + limit = 20, + since = 1764553447, // Nov 2025 + ), + ) + assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT event_headers.row_id as row_id FROM event_headers WHERE (event_headers.kind IN ("0", "10007", "10002")) AND (event_headers.pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (event_headers.created_at >= "1764553447") ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 20 + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY created_at DESC, id + ├── CO-ROUTINE filtered + │ ├── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=? AND created_at>?) + │ └── USE TEMP B-TREE FOR ORDER BY + ├── SCAN filtered + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + sql, + ) + } + + @Test + fun testReplaceablesOfMultipleKindsDownload() { + val sql = + explain( + Filter( + kinds = listOf(MetadataEvent.KIND, SearchRelayListEvent.KIND, AdvertisedRelayListEvent.KIND), + authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + ), + ) + assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT event_headers.row_id as row_id FROM event_headers WHERE (event_headers.kind IN ("0", "10007", "10002")) AND (event_headers.pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY created_at DESC, id + ├── SEARCH event_headers USING COVERING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=?) + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + sql, + ) + } + + @Test + fun testContactCardDownloadFromTrustedKeys() { + val sql = + explain( + Filter( + kinds = listOf(ContactCardEvent.KIND), + authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + tags = + mapOf( + "d" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + ), + since = 1764553447, // Nov 2025 + ), + ) + assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT event_headers.row_id as row_id FROM event_headers WHERE (event_headers.kind = "30382") AND (event_headers.pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (event_headers.d_tag = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (event_headers.created_at >= "1764553447") AND ((event_headers.kind >= 30000 AND event_headers.kind < 40000)) + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY created_at DESC, id + ├── SEARCH event_headers USING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?) + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + sql, + ) + } } diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionRequestModule.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionRequestModule.kt index de7b13e59..deb4a061d 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionRequestModule.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionRequestModule.kt @@ -51,13 +51,11 @@ class DeletionRequestModule( SELECT RAISE(ABORT, 'blocked: a deletion event exists') WHERE EXISTS ( SELECT 1 FROM event_tags - INNER JOIN event_headers - ON event_headers.row_id = event_tags.event_header_row_id WHERE event_tags.tag_hash IN (NEW.etag_hash, NEW.atag_hash) AND - event_tags.created_at >= NEW.created_at AND - event_headers.kind = 5 AND - event_headers.pubkey_owner_hash = NEW.pubkey_owner_hash + event_tags.kind = 5 AND + event_tags.pubkey_hash = NEW.pubkey_owner_hash AND + event_tags.created_at >= NEW.created_at ); END; """.trimIndent(), diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt index 75e6550f4..e96c9f93c 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt @@ -57,6 +57,8 @@ class EventIndexesModule( event_header_row_id INTEGER NOT NULL, tag_hash INTEGER NOT NULL, created_at INTEGER NOT NULL, + kind INTEGER NOT NULL, + pubkey_hash INTEGER NOT NULL, FOREIGN KEY (event_header_row_id) REFERENCES event_headers(row_id) ON DELETE CASCADE ) """.trimIndent(), @@ -74,8 +76,10 @@ class EventIndexesModule( // makes deletions on the event_header fast db.execSQL("CREATE INDEX fk_event_tags_header_id ON event_tags (event_header_row_id)") - // This is a very slow index to build (half the insert time goes here) but it is extremely effective. - db.execSQL("CREATE INDEX query_by_tags_hash ON event_tags (tag_hash, created_at DESC)") + // This is a very slow index to build (80% of the insert time goes here) but it is extremely effective. + db.execSQL("CREATE INDEX query_by_tags_hash ON event_tags (tag_hash, created_at DESC)") + db.execSQL("CREATE INDEX query_by_tags_hash_kind ON event_tags (tag_hash, kind, created_at DESC)") + db.execSQL("CREATE INDEX query_by_tags_hash_kind_pubkey ON event_tags (tag_hash, kind, pubkey_hash, created_at DESC)") // Prevent updates to maintain immutability db.execSQL( @@ -117,9 +121,9 @@ class EventIndexesModule( val sqlInsertTags = """ INSERT OR ROLLBACK INTO event_tags - (event_header_row_id, tag_hash, created_at) + (event_header_row_id, tag_hash, created_at, kind, pubkey_hash) VALUES - (?,?,?) + (?,?,?,?,?) """.trimIndent() fun insert( @@ -178,6 +182,8 @@ class EventIndexesModule( stmtTags.bindLong(1, headerId) stmtTags.bindLong(2, it) stmtTags.bindLong(3, event.createdAt) + stmtTags.bindLong(4, kindLong) + stmtTags.bindLong(5, pubkeyHash) stmtTags.executeInsert() } diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt index 94713826c..0fc2cc569 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt @@ -24,6 +24,7 @@ import android.database.Cursor import android.database.sqlite.SQLiteDatabase import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper +import com.vitorpamplona.quartz.nip01Core.core.isAddressable import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.store.sqlite.sql.where import com.vitorpamplona.quartz.utils.EventFactory @@ -338,10 +339,7 @@ class QueryBuilder( val needHeaders = with(filter) { - (ids != null) || - (authors != null && authors.isNotEmpty()) || - (kinds != null && kinds.isNotEmpty()) || - (tags != null && tags.containsKey("d")) + (ids != null) || (tags != null && tags.containsKey("d")) } val hasHeaders = @@ -440,20 +438,37 @@ class QueryBuilder( // range search is bad but most of the time these are up the top with few elements. if (reverseLookup) { + filter.kinds?.let { equalsOrIn("event_tags.kind", it) } + filter.authors?.let { equalsOrIn("event_tags.pubkey_hash", it.map { hasher.hash(it) }) } + filter.since?.let { greaterThanOrEquals("event_tags.created_at", it) } filter.until?.let { lessThanOrEquals("event_tags.created_at", it) } + + // there are indexes for these, starting with tags. + filter.tags?.forEach { (tagName, tagValues) -> + if (tagName == "d") { + equalsOrIn("event_headers.d_tag", tagValues) + } + } } else { + filter.kinds?.let { equalsOrIn("event_headers.kind", it) } + filter.authors?.let { equalsOrIn("event_headers.pubkey", it) } + + // there are indexes for these, starting with tags. + filter.tags?.forEach { (tagName, tagValues) -> + if (tagName == "d") { + equalsOrIn("event_headers.d_tag", tagValues) + } + } + filter.since?.let { greaterThanOrEquals("event_headers.created_at", it) } filter.until?.let { lessThanOrEquals("event_headers.created_at", it) } - } - filter.kinds?.let { equalsOrIn("event_headers.kind", it) } - filter.authors?.let { equalsOrIn("event_headers.pubkey", it) } - - // there are indexes for these, starting with tags. - filter.tags?.forEach { (tagName, tagValues) -> - if (tagName == "d") { - equalsOrIn("event_headers.d_tag", tagValues) + // no need to add the replaceable because query_by_kind_pubkey_created already covers it + val isAllAddressable = filter.kinds?.all { it.isAddressable() } ?: false + if (isAllAddressable) { + // matches unique index kind >= 30000 AND kind < 40000 + raw("(event_headers.kind >= 30000 AND event_headers.kind < 40000)") } } From 7b919ba44a68c4c1f3c3da061f6e2f392e3522b8 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 1 Jan 2026 15:07:27 -0500 Subject: [PATCH 040/137] added description --- .../com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt index 2feae643c..8872600d1 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt @@ -37,6 +37,7 @@ import com.vitorpamplona.quartz.utils.Log * - authors: Optional list of author public keys (must be 64 characters). * - kinds: Optional list of event kinds to include. * - tags: Optional map of tag names to values arrays (common tags like 'p', 'e', 'a' are validated). + * - tagsAll: Optional map of tag names to values arrays that must all match (common tags like 'p', 'e', 'a' are validated). * - since: Optional timestamp for filtering events with publication time ≥ this value. * - until: Optional timestamp for filtering events with publication time ≤ this value. * - limit: Optional maximum number of events to request. From 4c9df0bfef056224a4f5446abfde4208d6a92a3b Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Thu, 1 Jan 2026 20:09:47 +0000 Subject: [PATCH 041/137] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-pl-rPL/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index e0303c84f..11b8200fe 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -158,6 +158,7 @@ Nagraj wiadomość Nagrywanie wiadomości Kliknij i przytrzymaj aby nagrać wiadomość + Nagraj ponownie Nagrywanie Nagrywanie %1$s Wgrywanie… From 7c63d654f2d082f0632a1a6583e3eea9944dd0ff Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 1 Jan 2026 21:29:20 -0500 Subject: [PATCH 042/137] Inverts the name of the isFilledFilter function --- .../nip01Core/store/sqlite/QueryBuilder.kt | 2 +- .../relay/client/pool/RelayBasedFilter.kt | 6 ++--- .../quartz/nip01Core/relay/filters/Filter.kt | 22 +++++++++---------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt index 0fc2cc569..29bbf3f54 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt @@ -327,7 +327,7 @@ class QueryBuilder( filter: Filter, hasher: TagNameValueHasher, ): QuerySpec? { - if (!filter.isFilledFilter()) return null + if (filter.isEmpty()) return null val mustJoinSearch = (filter.search != null) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayBasedFilter.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayBasedFilter.kt index ba5b30576..584df7a8f 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayBasedFilter.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayBasedFilter.kt @@ -35,10 +35,10 @@ class RelayBasedFilter( fun List.groupByRelay(): Map> { val result = mutableMapOf>() for (relayBasedFilter in this) { - if (relayBasedFilter.filter.isFilledFilter()) { - result.getOrPut(relayBasedFilter.relay) { mutableListOf() }.add(relayBasedFilter.filter) - } else { + if (relayBasedFilter.filter.isEmpty()) { Log.e("FilterError", "Ignoring empty filter for ${relayBasedFilter.relay}") + } else { + result.getOrPut(relayBasedFilter.relay) { mutableListOf() }.add(relayBasedFilter.filter) } } return result diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt index 8872600d1..eae7fc8e9 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt @@ -74,18 +74,18 @@ class Filter( ) = Filter(ids, authors, kinds, tags, tagsAll, since, until, limit, search) /** - * Returns true if this filter contains any non-null and non-empty criteria. + * Returns true if this filter doesn't filter for anything. */ - fun isFilledFilter() = - (ids != null && ids.isNotEmpty()) || - (authors != null && authors.isNotEmpty()) || - (kinds != null && kinds.isNotEmpty()) || - (tags != null && tags.isNotEmpty() && tags.values.all { it.isNotEmpty() }) || - (tagsAll != null && tagsAll.isNotEmpty() && tagsAll.values.all { it.isNotEmpty() }) || - (since != null) || - (until != null) || - (limit != null) || - (search != null && search.isNotEmpty()) + fun isEmpty() = + (ids == null || ids.isEmpty()) && + (authors == null || authors.isEmpty()) && + (kinds == null || kinds.isEmpty()) && + (tags == null || tags.isEmpty() && tags.values.all { it.isNotEmpty() }) && + (tagsAll == null || tagsAll.isEmpty() && tagsAll.values.all { it.isNotEmpty() }) && + (since == null) && + (until == null) && + (limit == null) && + (search == null || search.isEmpty()) init { ids?.forEach { From 146e4e553306d746f8df87853649a9b7b23e3517 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Fri, 2 Jan 2026 07:51:31 +0200 Subject: [PATCH 043/137] initial desktop phase1 --- .../commons/network/RelayConnectionManager.kt | 19 + .../amethyst/commons/ui/note/NoteCard.kt | 11 +- .../amethyst/commons/util/EventExtensions.kt | 1 + .../vitorpamplona/amethyst/desktop/Main.kt | 170 ++++- .../amethyst/desktop/ui/ComposeNoteDialog.kt | 227 +++++++ .../amethyst/desktop/ui/FeedScreen.kt | 280 ++++++++- .../amethyst/desktop/ui/NoteActions.kt | 212 +++++++ .../desktop/ui/NotificationsScreen.kt | 305 +++++++++ .../amethyst/desktop/ui/UserProfileScreen.kt | 587 ++++++++++++++++++ 9 files changed, 1748 insertions(+), 64 deletions(-) create mode 100644 desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt create mode 100644 desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt create mode 100644 desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt create mode 100644 desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayConnectionManager.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayConnectionManager.kt index 45f9571c2..b09c662ae 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayConnectionManager.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayConnectionManager.kt @@ -99,6 +99,25 @@ open class RelayConnectionManager( client.send(event, relays) } + /** + * Broadcasts an event to all configured relays. + * Events will be sent to relays as they become connected. + */ + fun broadcastToAll(event: Event) { + // Use all configured relays, not just currently connected ones + // The NostrClient will queue/send events as relays connect + val configuredRelays = relayStatuses.value.keys + val connectedCount = connectedRelays.value.size + println("[RelayManager] broadcastToAll: event kind=${event.kind}, ID=${event.id.take(8)}...") + println("[RelayManager] Configured relays: ${configuredRelays.size}, Connected: $connectedCount") + configuredRelays.forEach { relay -> + val isConnected = connectedRelays.value.contains(relay) + println("[RelayManager] - $relay: ${if (isConnected) "CONNECTED" else "pending"}") + } + send(event, configuredRelays) + println("[RelayManager] broadcastToAll complete") + } + private fun updateRelayStatus( url: NormalizedRelayUrl, update: (RelayStatus) -> RelayStatus, diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCard.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCard.kt index a82e4faf8..7a05268b2 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCard.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCard.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.commons.ui.note +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -50,6 +51,7 @@ import com.vitorpamplona.amethyst.commons.util.toTimeAgo */ data class NoteDisplayData( val id: String, + val pubKeyHex: String, val pubKeyDisplay: String, val content: String, val createdAt: Long, @@ -64,6 +66,7 @@ fun NoteCard( note: NoteDisplayData, modifier: Modifier = Modifier, onClick: (() -> Unit)? = null, + onAuthorClick: ((String) -> Unit)? = null, ) { val richTextParser = remember { RichTextParser() } val urls = remember(note.content) { richTextParser.parseValidUrls(note.content) } @@ -82,12 +85,18 @@ fun NoteCard( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - // Author (truncated) + // Author (truncated, clickable) Text( text = note.pubKeyDisplay.take(20) + if (note.pubKeyDisplay.length > 20) "..." else "", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary, maxLines = 1, + modifier = + if (onAuthorClick != null) { + Modifier.clickable { onAuthorClick(note.pubKeyHex) } + } else { + Modifier + }, ) // Timestamp diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/EventExtensions.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/EventExtensions.kt index 1f0de406e..0c392b8f8 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/EventExtensions.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/EventExtensions.kt @@ -38,6 +38,7 @@ fun Event.toNoteDisplayData(): NoteDisplayData { return NoteDisplayData( id = id, + pubKeyHex = pubKey, pubKeyDisplay = npub, content = content, createdAt = createdAt, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt index 7e91eb32d..a04407ee9 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt @@ -81,8 +81,32 @@ import com.vitorpamplona.amethyst.commons.ui.screens.MessagesPlaceholder import com.vitorpamplona.amethyst.commons.ui.screens.NotificationsPlaceholder import com.vitorpamplona.amethyst.commons.ui.screens.SearchPlaceholder import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager +import com.vitorpamplona.amethyst.desktop.ui.ComposeNoteDialog import com.vitorpamplona.amethyst.desktop.ui.FeedScreen import com.vitorpamplona.amethyst.desktop.ui.LoginScreen +import com.vitorpamplona.amethyst.desktop.ui.NotificationsScreen +import com.vitorpamplona.amethyst.desktop.ui.UserProfileScreen + +private val isMacOS = System.getProperty("os.name").lowercase().contains("mac") + +/** + * Desktop navigation state - extends AppScreen with dynamic destinations. + */ +sealed class DesktopScreen { + data object Feed : DesktopScreen() + + data object Search : DesktopScreen() + + data object Messages : DesktopScreen() + + data object Notifications : DesktopScreen() + + data object MyProfile : DesktopScreen() + + data class UserProfile(val pubKeyHex: String) : DesktopScreen() + + data object Settings : DesktopScreen() +} fun main() = application { @@ -92,6 +116,7 @@ fun main() = height = 800.dp, position = WindowPosition.Aligned(Alignment.Center), ) + var showComposeDialog by remember { mutableStateOf(false) } Window( onCloseRequest = ::exitApplication, @@ -102,25 +127,58 @@ fun main() = Menu("File") { Item( "New Note", - shortcut = KeyShortcut(Key.N, ctrl = true), - onClick = { /* TODO: Open new note dialog */ }, + shortcut = + if (isMacOS) { + KeyShortcut(Key.N, meta = true) + } else { + KeyShortcut(Key.N, ctrl = true) + }, + onClick = { showComposeDialog = true }, ) Separator() Item( "Settings", - shortcut = KeyShortcut(Key.Comma, ctrl = true), + shortcut = + if (isMacOS) { + KeyShortcut(Key.Comma, meta = true) + } else { + KeyShortcut(Key.Comma, ctrl = true) + }, onClick = { /* TODO: Open settings */ }, ) Separator() Item( "Quit", - shortcut = KeyShortcut(Key.Q, ctrl = true), + shortcut = + if (isMacOS) { + KeyShortcut(Key.Q, meta = true) + } else { + KeyShortcut(Key.Q, ctrl = true) + }, onClick = ::exitApplication, ) } Menu("Edit") { - Item("Copy", shortcut = KeyShortcut(Key.C, ctrl = true), onClick = { }) - Item("Paste", shortcut = KeyShortcut(Key.V, ctrl = true), onClick = { }) + Item( + "Copy", + shortcut = + if (isMacOS) { + KeyShortcut(Key.C, meta = true) + } else { + KeyShortcut(Key.C, ctrl = true) + }, + onClick = { }, + ) + Item( + "Paste", + shortcut = + if (isMacOS) { + KeyShortcut(Key.V, meta = true) + } else { + KeyShortcut(Key.V, ctrl = true) + }, + onClick = { }, + ) } Menu("View") { Item("Feed", onClick = { }) @@ -133,13 +191,21 @@ fun main() = } } - App() + App( + showComposeDialog = showComposeDialog, + onShowComposeDialog = { showComposeDialog = true }, + onDismissComposeDialog = { showComposeDialog = false }, + ) } } @Composable -fun App() { - var currentScreen by remember { mutableStateOf(AppScreen.Feed) } +fun App( + showComposeDialog: Boolean, + onShowComposeDialog: () -> Unit, + onDismissComposeDialog: () -> Unit, +) { + var currentScreen by remember { mutableStateOf(DesktopScreen.Feed) } val relayManager = remember { DesktopRelayConnectionManager() } val accountManager = remember { AccountManager() } val accountState by accountManager.accountState.collectAsState() @@ -163,17 +229,29 @@ fun App() { is AccountState.LoggedOut -> { LoginScreen( accountManager = accountManager, - onLoginSuccess = { currentScreen = AppScreen.Feed }, + onLoginSuccess = { currentScreen = DesktopScreen.Feed }, ) } is AccountState.LoggedIn -> { + val account = accountState as AccountState.LoggedIn + MainContent( currentScreen = currentScreen, onScreenChange = { currentScreen = it }, relayManager = relayManager, accountManager = accountManager, - account = accountState as AccountState.LoggedIn, + account = account, + onShowComposeDialog = onShowComposeDialog, ) + + // Compose dialog + if (showComposeDialog) { + ComposeNoteDialog( + onDismiss = onDismissComposeDialog, + relayManager = relayManager, + account = account, + ) + } } } } @@ -182,11 +260,12 @@ fun App() { @Composable fun MainContent( - currentScreen: AppScreen, - onScreenChange: (AppScreen) -> Unit, + currentScreen: DesktopScreen, + onScreenChange: (DesktopScreen) -> Unit, relayManager: DesktopRelayConnectionManager, accountManager: AccountManager, account: AccountState.LoggedIn, + onShowComposeDialog: () -> Unit, ) { Row(Modifier.fillMaxSize()) { // Sidebar Navigation @@ -199,36 +278,36 @@ fun MainContent( NavigationRailItem( icon = { Icon(Icons.Default.Home, contentDescription = "Feed") }, label = { Text("Feed") }, - selected = currentScreen == AppScreen.Feed, - onClick = { onScreenChange(AppScreen.Feed) }, + selected = currentScreen == DesktopScreen.Feed, + onClick = { onScreenChange(DesktopScreen.Feed) }, ) NavigationRailItem( icon = { Icon(Icons.Default.Search, contentDescription = "Search") }, label = { Text("Search") }, - selected = currentScreen == AppScreen.Search, - onClick = { onScreenChange(AppScreen.Search) }, + selected = currentScreen == DesktopScreen.Search, + onClick = { onScreenChange(DesktopScreen.Search) }, ) NavigationRailItem( icon = { Icon(Icons.Default.Email, contentDescription = "Messages") }, label = { Text("DMs") }, - selected = currentScreen == AppScreen.Messages, - onClick = { onScreenChange(AppScreen.Messages) }, + selected = currentScreen == DesktopScreen.Messages, + onClick = { onScreenChange(DesktopScreen.Messages) }, ) NavigationRailItem( icon = { Icon(Icons.Default.Notifications, contentDescription = "Notifications") }, label = { Text("Alerts") }, - selected = currentScreen == AppScreen.Notifications, - onClick = { onScreenChange(AppScreen.Notifications) }, + selected = currentScreen == DesktopScreen.Notifications, + onClick = { onScreenChange(DesktopScreen.Notifications) }, ) NavigationRailItem( icon = { Icon(Icons.Default.Person, contentDescription = "Profile") }, label = { Text("Profile") }, - selected = currentScreen == AppScreen.Profile, - onClick = { onScreenChange(AppScreen.Profile) }, + selected = currentScreen == DesktopScreen.MyProfile || currentScreen is DesktopScreen.UserProfile, + onClick = { onScreenChange(DesktopScreen.MyProfile) }, ) Spacer(Modifier.weight(1f)) @@ -238,8 +317,8 @@ fun MainContent( NavigationRailItem( icon = { Icon(Icons.Default.Settings, contentDescription = "Settings") }, label = { Text("Settings") }, - selected = currentScreen == AppScreen.Settings, - onClick = { onScreenChange(AppScreen.Settings) }, + selected = currentScreen == DesktopScreen.Settings, + onClick = { onScreenChange(DesktopScreen.Settings) }, ) Spacer(Modifier.height(16.dp)) @@ -252,12 +331,41 @@ fun MainContent( modifier = Modifier.weight(1f).fillMaxHeight().padding(24.dp), ) { when (currentScreen) { - AppScreen.Feed -> FeedScreen(relayManager) - AppScreen.Search -> SearchPlaceholder() - AppScreen.Messages -> MessagesPlaceholder() - AppScreen.Notifications -> NotificationsPlaceholder() - AppScreen.Profile -> ProfileScreen(account, accountManager) - AppScreen.Settings -> RelaySettingsScreen(relayManager) + DesktopScreen.Feed -> + FeedScreen( + relayManager = relayManager, + account = account, + onCompose = onShowComposeDialog, + onNavigateToProfile = { pubKeyHex -> + onScreenChange(DesktopScreen.UserProfile(pubKeyHex)) + }, + ) + DesktopScreen.Search -> SearchPlaceholder() + DesktopScreen.Messages -> MessagesPlaceholder() + DesktopScreen.Notifications -> NotificationsScreen(relayManager, account) + DesktopScreen.MyProfile -> + UserProfileScreen( + pubKeyHex = account.pubKeyHex, + relayManager = relayManager, + account = account, + onBack = { onScreenChange(DesktopScreen.Feed) }, + onCompose = onShowComposeDialog, + onNavigateToProfile = { pubKeyHex -> + onScreenChange(DesktopScreen.UserProfile(pubKeyHex)) + }, + ) + is DesktopScreen.UserProfile -> + UserProfileScreen( + pubKeyHex = currentScreen.pubKeyHex, + relayManager = relayManager, + account = account, + onBack = { onScreenChange(DesktopScreen.Feed) }, + onCompose = onShowComposeDialog, + onNavigateToProfile = { pubKeyHex -> + onScreenChange(DesktopScreen.UserProfile(pubKeyHex)) + }, + ) + DesktopScreen.Settings -> RelaySettingsScreen(relayManager) } } } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt new file mode 100644 index 000000000..f9e48c53f --- /dev/null +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt @@ -0,0 +1,227 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.desktop.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import com.vitorpamplona.amethyst.commons.account.AccountState +import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.events.eTag +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTag +import com.vitorpamplona.quartz.nip01Core.tags.references.references +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip10Notes.content.findHashtags +import com.vitorpamplona.quartz.nip10Notes.content.findURLs +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +@Composable +fun ComposeNoteDialog( + onDismiss: () -> Unit, + relayManager: DesktopRelayConnectionManager, + account: AccountState.LoggedIn, + replyTo: com.vitorpamplona.quartz.nip01Core.core.Event? = null, +) { + var content by remember { mutableStateOf("") } + var isPosting by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf(null) } + val scope = rememberCoroutineScope() + + Dialog(onDismissRequest = { if (!isPosting) onDismiss() }) { + Card( + modifier = Modifier.width(600.dp).padding(16.dp), + ) { + Column(modifier = Modifier.padding(24.dp)) { + Text( + if (replyTo != null) "Reply" else "New Note", + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + ) + + replyTo?.let { reply -> + Spacer(Modifier.height(8.dp)) + Text( + "Replying to: ${reply.content.take(50)}${if (reply.content.length > 50) "..." else ""}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + Spacer(Modifier.height(16.dp)) + + OutlinedTextField( + value = content, + onValueChange = { + content = it + errorMessage = null + }, + modifier = Modifier.fillMaxWidth().height(200.dp), + label = { Text("What's on your mind?") }, + placeholder = { Text("Write your note...") }, + enabled = !isPosting, + maxLines = 10, + ) + + Spacer(Modifier.height(8.dp)) + + // Character count + Text( + "${content.length} characters", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + errorMessage?.let { error -> + Spacer(Modifier.height(8.dp)) + Text( + error, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error, + ) + } + + Spacer(Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + ) { + OutlinedButton( + onClick = onDismiss, + enabled = !isPosting, + ) { + Text("Cancel") + } + + Spacer(Modifier.width(8.dp)) + + Button( + onClick = { + if (content.isBlank()) { + errorMessage = "Note cannot be empty" + return@Button + } + + scope.launch { + isPosting = true + errorMessage = null + + try { + publishNote( + content = content, + account = account, + relayManager = relayManager, + replyTo = replyTo, + ) + onDismiss() + } catch (e: Exception) { + errorMessage = "Failed to publish: ${e.message}" + } finally { + isPosting = false + } + } + }, + enabled = !isPosting && content.isNotBlank(), + ) { + Text(if (isPosting) "Publishing..." else "Publish") + } + } + } + } + } +} + +/** + * Publishes a text note to relays. + * Uses the Account's key to sign the event. + */ +private suspend fun publishNote( + content: String, + account: AccountState.LoggedIn, + relayManager: DesktopRelayConnectionManager, + replyTo: com.vitorpamplona.quartz.nip01Core.core.Event?, +) { + withContext(Dispatchers.IO) { + println("[ComposeNoteDialog] Starting publishNote: content length=${content.length}") + + // Build TextNoteEvent using Quartz + val template = + TextNoteEvent.build(content) { + // If replying, add e-tag and p-tag + if (replyTo != null) { + val etag = ETag(replyTo.id) + etag.relay = null + etag.author = replyTo.pubKey + eTag(etag) + pTag(PTag(replyTo.pubKey, relayHint = null)) + } + + // Extract hashtags and URLs from content + hashtags(findHashtags(content)) + references(findURLs(content)) + } + + // Sign the event + val signedEvent = + if (account.isReadOnly) { + // Read-only mode - can't sign events + println("[ComposeNoteDialog] Error: Cannot post in read-only mode") + throw IllegalStateException("Cannot post in read-only mode") + } else { + // Sign with nsec (full key) + val signer = account.signer + println("[ComposeNoteDialog] Signing event with pubkey: ${account.pubKeyHex.take(8)}...") + signer.sign(template) + } + + println("[ComposeNoteDialog] Event signed successfully, ID: ${signedEvent.id.take(8)}...") + + // Broadcast to all configured relays + println("[ComposeNoteDialog] Broadcasting to relays...") + relayManager.broadcastToAll(signedEvent) + println("[ComposeNoteDialog] Broadcast complete") + } +} diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt index 9eb70e313..941def5ee 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt @@ -22,19 +22,37 @@ package com.vitorpamplona.amethyst.desktop.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.Button +import androidx.compose.material3.FilterChip +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.DisposableEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.account.AccountState import com.vitorpamplona.amethyst.commons.ui.components.LoadingState import com.vitorpamplona.amethyst.commons.ui.feed.FeedHeader import com.vitorpamplona.amethyst.commons.ui.note.NoteCard @@ -44,35 +62,141 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +/** + * Note card with action buttons. + */ @Composable -fun FeedScreen(relayManager: DesktopRelayConnectionManager) { +fun FeedNoteCard( + event: Event, + relayManager: DesktopRelayConnectionManager, + account: AccountState.LoggedIn?, + onReply: () -> Unit, + onNavigateToProfile: (String) -> Unit = {}, +) { + Column { + NoteCard( + note = event.toNoteDisplayData(), + onAuthorClick = onNavigateToProfile, + ) + + // Action buttons (only if logged in) + if (account != null) { + NoteActionsRow( + event = event, + relayManager = relayManager, + account = account, + onReplyClick = onReply, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), + ) + } + } +} + +enum class FeedMode { + GLOBAL, + FOLLOWING, +} + +@Composable +fun FeedScreen( + relayManager: DesktopRelayConnectionManager, + account: AccountState.LoggedIn? = null, + onCompose: () -> Unit = {}, + onNavigateToProfile: (String) -> Unit = {}, +) { val connectedRelays by relayManager.connectedRelays.collectAsState() val relayStatuses by relayManager.relayStatuses.collectAsState() val events = remember { mutableStateListOf() } val seenIds = remember { mutableSetOf() } + var replyToEvent by remember { mutableStateOf(null) } + var feedMode by remember { mutableStateOf(FeedMode.GLOBAL) } + var followedUsers by remember { mutableStateOf>(emptySet()) } - // Use relayStatuses.keys (configured relays) to initiate subscription, - // not connectedRelays (which is empty until subscriptions trigger connections) - val configuredRelays = relayStatuses.keys - - DisposableEffect(configuredRelays) { - if (configuredRelays.isNotEmpty()) { - val subId = "global-feed-${System.currentTimeMillis()}" - val filters = - listOf( - Filter( - kinds = listOf(TextNoteEvent.KIND), - limit = 50, - ), - ) - + // Load followed users for Following feed mode + DisposableEffect(relayStatuses, account, feedMode) { + val configuredRelays = relayStatuses.keys + if (configuredRelays.isNotEmpty() && account != null && feedMode == FeedMode.FOLLOWING) { + val contactListSubId = "feed-contacts-${account.pubKeyHex}-${System.currentTimeMillis()}" relayManager.subscribe( - subId = subId, - filters = filters, + subId = contactListSubId, + filters = + listOf( + Filter( + kinds = listOf(ContactListEvent.KIND), + authors = listOf(account.pubKeyHex), + limit = 1, + ), + ), relays = configuredRelays, listener = + object : IRequestListener { + override fun onEvent( + event: Event, + isLive: Boolean, + relay: NormalizedRelayUrl, + forFilters: List?, + ) { + if (event is ContactListEvent) { + followedUsers = event.verifiedFollowKeySet() + } + } + + override fun onEose( + relay: NormalizedRelayUrl, + forFilters: List?, + ) {} + }, + ) + + onDispose { + relayManager.unsubscribe(contactListSubId) + } + } else { + onDispose {} + } + } + + DisposableEffect(relayStatuses, feedMode, followedUsers) { + val configuredRelays = relayStatuses.keys + if (configuredRelays.isNotEmpty()) { + // Clear previous events when switching modes + events.clear() + seenIds.clear() + + val subId = "${feedMode.name.lowercase()}-feed-${System.currentTimeMillis()}" + val filters = + when (feedMode) { + FeedMode.GLOBAL -> + listOf( + Filter( + kinds = listOf(TextNoteEvent.KIND), + limit = 50, + ), + ) + FeedMode.FOLLOWING -> + if (followedUsers.isNotEmpty()) { + listOf( + Filter( + kinds = listOf(TextNoteEvent.KIND), + authors = followedUsers.toList(), + limit = 50, + ), + ) + } else { + // No followed users yet, return empty filter + emptyList() + } + } + + if (filters.isNotEmpty()) { + relayManager.subscribe( + subId = subId, + filters = filters, + relays = configuredRelays, + listener = object : IRequestListener { override fun onEvent( event: Event, @@ -97,40 +221,132 @@ fun FeedScreen(relayManager: DesktopRelayConnectionManager) { // End of stored events } }, - ) + ) - onDispose { - relayManager.unsubscribe(subId) + onDispose { + relayManager.unsubscribe(subId) + } + } else { + onDispose {} } } else { - onDispose { } + onDispose {} } } Column(modifier = Modifier.fillMaxSize()) { - FeedHeader( - title = "Global Feed", - connectedRelayCount = connectedRelays.size, - onRefresh = { relayManager.connect() }, - ) + // Header with compose button + Row( + modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + if (feedMode == FeedMode.GLOBAL) "Global Feed" else "Following Feed", + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onBackground, + ) - Spacer(Modifier.height(16.dp)) + // Feed mode selector + if (account != null) { + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + FilterChip( + selected = feedMode == FeedMode.GLOBAL, + onClick = { feedMode = FeedMode.GLOBAL }, + label = { Text("Global") }, + ) + FilterChip( + selected = feedMode == FeedMode.FOLLOWING, + onClick = { feedMode = FeedMode.FOLLOWING }, + label = { Text("Following") }, + ) + } + } + } + + Spacer(Modifier.height(4.dp)) + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + "${connectedRelays.size} relays connected", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + if (feedMode == FeedMode.FOLLOWING) { + Text( + " • ${followedUsers.size} followed", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Spacer(Modifier.width(8.dp)) + IconButton( + onClick = { relayManager.connect() }, + modifier = Modifier.size(24.dp), + ) { + Icon( + Icons.Default.Refresh, + contentDescription = "Refresh", + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(18.dp), + ) + } + } + } + + // New Post button (primary action) + Button( + onClick = onCompose, + enabled = account != null && !account.isReadOnly, + ) { + Icon(Icons.Default.Add, "New Post", Modifier.size(18.dp)) + Spacer(Modifier.width(8.dp)) + Text("New Post") + } + } + + Spacer(Modifier.height(8.dp)) if (connectedRelays.isEmpty()) { LoadingState("Connecting to relays...") + } else if (feedMode == FeedMode.FOLLOWING && followedUsers.isEmpty()) { + LoadingState("Loading followed users...") } else if (events.isEmpty()) { - LoadingState("Loading notes...") + LoadingState( + if (feedMode == FeedMode.FOLLOWING) { + "No notes from followed users yet" + } else { + "Loading notes..." + }, + ) } else { LazyColumn( verticalArrangement = Arrangement.spacedBy(8.dp), ) { items(events, key = { it.id }) { event -> - // Use NoteCard from commons - NoteCard( - note = event.toNoteDisplayData(), + FeedNoteCard( + event = event, + relayManager = relayManager, + account = account, + onReply = { replyToEvent = event }, + onNavigateToProfile = onNavigateToProfile, ) } } } + + // Reply dialog + if (replyToEvent != null && account != null) { + ComposeNoteDialog( + onDismiss = { replyToEvent = null }, + relayManager = relayManager, + account = account, + replyTo = replyToEvent, + ) + } } } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt new file mode 100644 index 000000000..b630d2c62 --- /dev/null +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt @@ -0,0 +1,212 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.desktop.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.outlined.FavoriteBorder +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.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.account.AccountState +import com.vitorpamplona.amethyst.commons.icons.Reply +import com.vitorpamplona.amethyst.commons.icons.Repost +import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent +import com.vitorpamplona.quartz.nip18Reposts.RepostEvent +import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * Action buttons row for a note (react, reply, repost). + */ +@Composable +fun NoteActionsRow( + event: Event, + relayManager: DesktopRelayConnectionManager, + account: AccountState.LoggedIn, + onReplyClick: () -> Unit, + modifier: Modifier = Modifier, +) { + var isLiked by remember { mutableStateOf(false) } + var isReposted by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + // Reply button + IconButton( + onClick = onReplyClick, + modifier = Modifier.size(32.dp), + ) { + Icon( + Reply, + contentDescription = "Reply", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(18.dp), + ) + } + + // Like button + IconButton( + onClick = { + if (!isLiked) { + scope.launch { + reactToNote( + event = event, + reaction = "+", + account = account, + relayManager = relayManager, + ) + isLiked = true + } + } + }, + modifier = Modifier.size(32.dp), + ) { + Icon( + if (isLiked) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder, + contentDescription = if (isLiked) "Unlike" else "Like", + tint = + if (isLiked) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + modifier = Modifier.size(18.dp), + ) + } + + // Repost button + IconButton( + onClick = { + if (!isReposted) { + scope.launch { + repostNote( + event = event, + account = account, + relayManager = relayManager, + ) + isReposted = true + } + } + }, + modifier = Modifier.size(32.dp), + ) { + Icon( + Repost, + contentDescription = "Repost", + tint = + if (isReposted) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + modifier = Modifier.size(18.dp), + ) + } + + // Placeholder for action count + Text( + "", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } +} + +/** + * Creates a reaction event and broadcasts to relays. + */ +private suspend fun reactToNote( + event: Event, + reaction: String, + account: AccountState.LoggedIn, + relayManager: DesktopRelayConnectionManager, +) { + withContext(Dispatchers.IO) { + // Build reaction event + val reactedTo = EventHintBundle(event, null) + val template = ReactionEvent.like(reactedTo) + + // Sign with user's key + val signedEvent = account.signer.sign(template) + + // Broadcast to all relays + relayManager.broadcastToAll(signedEvent) + } +} + +/** + * Creates a repost event and broadcasts to relays. + */ +private suspend fun repostNote( + event: Event, + account: AccountState.LoggedIn, + relayManager: DesktopRelayConnectionManager, +) { + withContext(Dispatchers.IO) { + // Build repost event + val template = + if (event.kind == 1) { + // Text note - use RepostEvent (kind 6) + RepostEvent.build( + boostedPost = event, + eventSourceRelay = null, + authorHomeRelay = null, + ) + } else { + // Other kinds - use GenericRepostEvent (kind 16) + GenericRepostEvent.build( + boostedPost = event, + eventSourceRelay = null, + authorHomeRelay = null, + ) + } + + // Sign with user's key + val signedEvent = account.signer.sign(template) + + // Broadcast to all relays + relayManager.broadcastToAll(signedEvent) + } +} diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt new file mode 100644 index 000000000..00a3d01ff --- /dev/null +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt @@ -0,0 +1,305 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.desktop.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.account.AccountState +import com.vitorpamplona.amethyst.commons.icons.Reply +import com.vitorpamplona.amethyst.commons.icons.Repost +import com.vitorpamplona.amethyst.commons.icons.Zap +import com.vitorpamplona.amethyst.commons.ui.components.LoadingState +import com.vitorpamplona.amethyst.commons.ui.feed.FeedHeader +import com.vitorpamplona.amethyst.commons.util.toNoteDisplayData +import com.vitorpamplona.amethyst.commons.util.toTimeAgo +import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull +import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent +import com.vitorpamplona.quartz.nip18Reposts.RepostEvent +import com.vitorpamplona.quartz.nip19Bech32.toNpub +import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent +import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent + +/** + * Notification types for display. + */ +sealed class NotificationItem( + open val event: Event, + open val timestamp: Long, +) { + data class Mention(override val event: Event, override val timestamp: Long) : NotificationItem(event, timestamp) + + data class Reply(override val event: Event, override val timestamp: Long) : NotificationItem(event, timestamp) + + data class Reaction(override val event: Event, override val timestamp: Long, val content: String) : + NotificationItem(event, timestamp) + + data class Repost(override val event: Event, override val timestamp: Long) : NotificationItem(event, timestamp) + + data class Zap(override val event: Event, override val timestamp: Long, val amount: Long?) : + NotificationItem(event, timestamp) +} + +@Composable +fun NotificationsScreen( + relayManager: DesktopRelayConnectionManager, + account: AccountState.LoggedIn, +) { + val connectedRelays by relayManager.connectedRelays.collectAsState() + val relayStatuses by relayManager.relayStatuses.collectAsState() + val notifications = remember { mutableStateListOf() } + val seenIds = remember { mutableSetOf() } + + DisposableEffect(relayStatuses, account.pubKeyHex) { + val configuredRelays = relayStatuses.keys + if (configuredRelays.isNotEmpty()) { + val subId = "notifications-${account.pubKeyHex}-${System.currentTimeMillis()}" + val filters = + listOf( + // Mentions, replies, reactions, reposts, zaps + Filter( + kinds = + listOf( + TextNoteEvent.KIND, // 1 - mentions/replies + ReactionEvent.KIND, // 7 - reactions + RepostEvent.KIND, // 6 - reposts + GenericRepostEvent.KIND, // 16 - generic reposts + LnZapEvent.KIND, // 9735 - zaps + ), + tags = mapOf("p" to listOf(account.pubKeyHex)), // Events mentioning user + limit = 100, + ), + ) + + relayManager.subscribe( + subId = subId, + filters = filters, + relays = configuredRelays, + listener = + object : IRequestListener { + override fun onEvent( + event: Event, + isLive: Boolean, + relay: NormalizedRelayUrl, + forFilters: List?, + ) { + // Skip events from the user themselves (except zaps) + if (event.pubKey == account.pubKeyHex && event !is LnZapEvent) { + return + } + + if (event.id !in seenIds) { + seenIds.add(event.id) + + val notification = + when (event) { + is ReactionEvent -> + NotificationItem.Reaction( + event = event, + timestamp = event.createdAt, + content = event.content, + ) + is RepostEvent, is GenericRepostEvent -> + NotificationItem.Repost( + event = event, + timestamp = event.createdAt, + ) + is LnZapEvent -> { + // Extract amount from zap (simplified - full parsing in production) + val amount = event.amount?.toLong() + NotificationItem.Zap( + event = event, + timestamp = event.createdAt, + amount = amount, + ) + } + is TextNoteEvent -> { + // Check if it's a reply (has e-tag) or mention + val eTags = event.tags.filter { it.size > 1 && it[0] == "e" } + val isReply = eTags.isNotEmpty() + if (isReply) { + NotificationItem.Reply(event, event.createdAt) + } else { + NotificationItem.Mention(event, event.createdAt) + } + } + else -> NotificationItem.Mention(event, event.createdAt) + } + + notifications.add(0, notification) + if (notifications.size > 200) { + val removed = notifications.removeAt(notifications.size - 1) + seenIds.remove(removed.event.id) + } + } + } + + override fun onEose( + relay: NormalizedRelayUrl, + forFilters: List?, + ) { + // End of stored events + } + }, + ) + + onDispose { + relayManager.unsubscribe(subId) + } + } else { + onDispose { } + } + } + + Column(modifier = Modifier.fillMaxSize()) { + FeedHeader( + title = "Notifications", + connectedRelayCount = connectedRelays.size, + onRefresh = { relayManager.connect() }, + ) + + Spacer(Modifier.height(16.dp)) + + if (connectedRelays.isEmpty()) { + LoadingState("Connecting to relays...") + } else if (notifications.isEmpty()) { + LoadingState("Loading notifications...") + } else { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(notifications, key = { it.event.id }) { notification -> + NotificationCard(notification) + } + } + } + } +} + +@Composable +fun NotificationCard(notification: NotificationItem) { + val (icon, label, color) = + when (notification) { + is NotificationItem.Mention -> Triple(Icons.Default.Favorite, "mentioned you", MaterialTheme.colorScheme.primary) + is NotificationItem.Reply -> Triple(Reply, "replied", MaterialTheme.colorScheme.secondary) + is NotificationItem.Reaction -> + Triple( + Icons.Default.Favorite, + "reacted ${notification.content}", + MaterialTheme.colorScheme.tertiary, + ) + is NotificationItem.Repost -> Triple(Repost, "reposted", MaterialTheme.colorScheme.primary) + is NotificationItem.Zap -> { + val amountText = notification.amount?.let { " ${it / 1000} sats" } ?: "" + Triple(Zap, "zapped$amountText", MaterialTheme.colorScheme.primary) + } + } + + val authorDisplay = + try { + notification.event.pubKey.hexToByteArrayOrNull()?.toNpub()?.take(20) ?: notification.event.pubKey.take(20) + } catch (e: Exception) { + notification.event.pubKey.take(20) + } + + Card( + modifier = Modifier.fillMaxWidth(), + colors = + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + ), + ) { + Column(modifier = Modifier.padding(12.dp)) { + // Header: icon + label + author + time + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + icon, + contentDescription = label, + tint = color, + modifier = Modifier.size(16.dp), + ) + Text( + text = "$authorDisplay $label", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + Text( + text = notification.timestamp.toTimeAgo(withDot = false), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + ) + } + + // Content (for text-based notifications) + if (notification is NotificationItem.Mention || + notification is NotificationItem.Reply + ) { + Spacer(Modifier.height(8.dp)) + Text( + text = notification.event.content.take(200), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 3, + ) + } + } + } +} diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt new file mode 100644 index 000000000..a2c9ff798 --- /dev/null +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt @@ -0,0 +1,587 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.desktop.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.PersonAdd +import androidx.compose.material.icons.filled.PersonRemove +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.account.AccountState +import com.vitorpamplona.amethyst.commons.ui.components.LoadingState +import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull +import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser +import com.vitorpamplona.quartz.nip01Core.tags.people.pTag +import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent +import com.vitorpamplona.quartz.nip02FollowList.ReadWrite +import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip19Bech32.toNpub +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * User profile screen showing user info, follow button, and their posts. + */ +@Composable +fun UserProfileScreen( + pubKeyHex: String, + relayManager: DesktopRelayConnectionManager, + account: AccountState.LoggedIn?, + onBack: () -> Unit, + onCompose: () -> Unit = {}, + onNavigateToProfile: (String) -> Unit = {}, +) { + val connectedRelays by relayManager.connectedRelays.collectAsState() + val relayStatuses by relayManager.relayStatuses.collectAsState() + + // User metadata + var displayName by remember { mutableStateOf(null) } + var about by remember { mutableStateOf(null) } + var picture by remember { mutableStateOf(null) } + var followersCount by remember { mutableStateOf(0) } + var followingCount by remember { mutableStateOf(0) } + + // User's posts + val events = remember { mutableStateListOf() } + val seenIds = remember { mutableSetOf() } + var postsLoading by remember { mutableStateOf(true) } + var postsError by remember { mutableStateOf(null) } + var retryTrigger by remember { mutableStateOf(0) } + + // Follow state + var isFollowing by remember { mutableStateOf(false) } + var currentContactList by remember { mutableStateOf(null) } + var isFollowLoading by remember { mutableStateOf(false) } + var followError by remember { mutableStateOf(null) } + val scope = rememberCoroutineScope() + + // Load current user's contact list (for follow state) + DisposableEffect(relayStatuses, account) { + val configuredRelays = relayStatuses.keys + if (configuredRelays.isNotEmpty() && account != null) { + val contactListSubId = "my-contacts-${account.pubKeyHex}-${System.currentTimeMillis()}" + relayManager.subscribe( + subId = contactListSubId, + filters = + listOf( + Filter( + kinds = listOf(ContactListEvent.KIND), // Kind 3 + authors = listOf(account.pubKeyHex), + limit = 1, + ), + ), + relays = configuredRelays, + listener = + object : IRequestListener { + override fun onEvent( + event: Event, + isLive: Boolean, + relay: NormalizedRelayUrl, + forFilters: List?, + ) { + if (event is ContactListEvent) { + currentContactList = event + isFollowing = event.isTaggedUser(pubKeyHex) + } + } + + override fun onEose( + relay: NormalizedRelayUrl, + forFilters: List?, + ) {} + }, + ) + + onDispose { + relayManager.unsubscribe(contactListSubId) + } + } else { + onDispose {} + } + } + + // Subscribe to user metadata and posts + DisposableEffect(relayStatuses, pubKeyHex, retryTrigger) { + val configuredRelays = relayStatuses.keys + if (configuredRelays.isNotEmpty()) { + postsLoading = true + postsError = null + events.clear() + seenIds.clear() + // Metadata subscription (kind 0) + val metadataSubId = "profile-metadata-$pubKeyHex-${System.currentTimeMillis()}" + relayManager.subscribe( + subId = metadataSubId, + filters = + listOf( + Filter( + kinds = listOf(0), // Metadata + authors = listOf(pubKeyHex), + limit = 1, + ), + ), + relays = configuredRelays, + listener = + object : IRequestListener { + override fun onEvent( + event: Event, + isLive: Boolean, + relay: NormalizedRelayUrl, + forFilters: List?, + ) { + // Parse metadata JSON (simplified - full parsing in production) + try { + val content = event.content + displayName = extractJsonField(content, "display_name") ?: extractJsonField(content, "name") + about = extractJsonField(content, "about") + picture = extractJsonField(content, "picture") + } catch (e: Exception) { + // Ignore parse errors + } + } + + override fun onEose( + relay: NormalizedRelayUrl, + forFilters: List?, + ) {} + }, + ) + + // Posts subscription (kind 1) + val postsSubId = "profile-posts-$pubKeyHex-${System.currentTimeMillis()}" + relayManager.subscribe( + subId = postsSubId, + filters = + listOf( + Filter( + kinds = listOf(TextNoteEvent.KIND), + authors = listOf(pubKeyHex), + limit = 50, + ), + ), + relays = configuredRelays, + listener = + object : IRequestListener { + override fun onEvent( + event: Event, + isLive: Boolean, + relay: NormalizedRelayUrl, + forFilters: List?, + ) { + if (event.id !in seenIds) { + seenIds.add(event.id) + events.add(0, event) + if (events.size > 100) { + val removed = events.removeAt(events.size - 1) + seenIds.remove(removed.id) + } + } + } + + override fun onEose( + relay: NormalizedRelayUrl, + forFilters: List?, + ) { + // At least one relay finished sending events + postsLoading = false + } + }, + ) + + // Set timeout for loading state + val timeoutJob = kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.Default).launch { + kotlinx.coroutines.delay(10000) // 10 second timeout + if (postsLoading) { + postsError = "Request timed out. Check relay connections." + postsLoading = false + } + } + + onDispose { + timeoutJob.cancel() + relayManager.unsubscribe(metadataSubId) + relayManager.unsubscribe(postsSubId) + } + } else { + postsLoading = false + postsError = "No relays configured" + onDispose {} + } + } + + Column(modifier = Modifier.fillMaxSize()) { + // Header with back button + Row( + modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back") + } + Spacer(Modifier.width(8.dp)) + Text( + "Profile", + style = MaterialTheme.typography.headlineMedium, + ) + } + + if (account != null && !account.isReadOnly && pubKeyHex != account.pubKeyHex) { + Column(horizontalAlignment = Alignment.End) { + Button( + onClick = { + scope.launch { + isFollowLoading = true + followError = null + try { + if (isFollowing) { + unfollowUser(pubKeyHex, account, relayManager, currentContactList) + isFollowing = false + } else { + followUser(pubKeyHex, account, relayManager, currentContactList) + isFollowing = true + } + } catch (e: Exception) { + followError = e.message ?: "Failed to update follow status" + } finally { + isFollowLoading = false + } + } + }, + enabled = !isFollowLoading, + ) { + if (isFollowLoading) { + androidx.compose.material3.CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.onPrimary, + ) + Spacer(Modifier.width(8.dp)) + Text(if (isFollowing) "Unfollowing..." else "Following...") + } else { + Icon( + if (isFollowing) Icons.Default.PersonRemove else Icons.Default.PersonAdd, + contentDescription = if (isFollowing) "Unfollow" else "Follow", + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.width(8.dp)) + Text(if (isFollowing) "Unfollow" else "Follow") + } + } + + followError?.let { error -> + Spacer(Modifier.height(4.dp)) + Text( + error, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error, + ) + } + } + } + } + + if (connectedRelays.isEmpty()) { + LoadingState("Connecting to relays...") + } else { + // Profile card + Card( + modifier = Modifier.fillMaxWidth(), + colors = + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + ), + ) { + Column(modifier = Modifier.padding(24.dp)) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.Top, + ) { + // Profile picture placeholder + Surface( + modifier = Modifier.size(80.dp).clip(CircleShape), + color = MaterialTheme.colorScheme.primary, + ) { + // TODO: Load actual image from picture URL + } + + Column(modifier = Modifier.weight(1f)) { + Text( + displayName ?: (pubKeyHex.hexToByteArrayOrNull()?.toNpub()?.take(20) ?: pubKeyHex.take(20)), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + ) + Spacer(Modifier.height(4.dp)) + Text( + (pubKeyHex.hexToByteArrayOrNull()?.toNpub()?.take(32) ?: pubKeyHex.take(32)) + "...", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + + if (about != null) { + Spacer(Modifier.height(16.dp)) + Text( + about!!, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + ) + } + + Spacer(Modifier.height(16.dp)) + + Row(horizontalArrangement = Arrangement.spacedBy(24.dp)) { + Column { + Text( + "$followersCount", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + Text( + "Followers", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Column { + Text( + "$followingCount", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + Text( + "Following", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } + + Spacer(Modifier.height(24.dp)) + + // User's posts + Text( + "Posts", + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(bottom = 8.dp), + ) + + when { + postsError != null -> { + // Error state with retry + Box( + modifier = Modifier.fillMaxWidth().padding(32.dp), + contentAlignment = Alignment.Center, + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + "Failed to load posts", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.error, + ) + Spacer(Modifier.height(8.dp)) + Text( + postsError!!, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(Modifier.height(16.dp)) + OutlinedButton(onClick = { retryTrigger++ }) { + Text("Retry") + } + } + } + } + postsLoading -> { + // Loading state + Box( + modifier = Modifier.fillMaxWidth().padding(32.dp), + contentAlignment = Alignment.Center, + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + androidx.compose.material3.CircularProgressIndicator() + Spacer(Modifier.height(16.dp)) + Text( + "Loading posts...", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + events.isEmpty() -> { + // Empty state (loaded but no posts) + Box( + modifier = Modifier.fillMaxWidth().padding(32.dp), + contentAlignment = Alignment.Center, + ) { + Text( + "No posts yet", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + else -> { + // Posts loaded successfully + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(events, key = { it.id }) { event -> + FeedNoteCard( + event = event, + relayManager = relayManager, + account = account, + onReply = onCompose, + onNavigateToProfile = onNavigateToProfile, + ) + } + } + } + } + } + } +} + +/** + * Simple JSON field extractor (not production-ready, just for demo). + */ +private fun extractJsonField( + json: String, + field: String, +): String? { + val regex = """"$field"\s*:\s*"([^"]*)"""".toRegex() + return regex.find(json)?.groupValues?.get(1) +} + +/** + * Follows a user by publishing an updated contact list event. + */ +private suspend fun followUser( + pubKeyHex: String, + account: AccountState.LoggedIn, + relayManager: DesktopRelayConnectionManager, + currentContactList: ContactListEvent?, +) { + withContext(Dispatchers.IO) { + println("[UserProfile] Starting followUser: target=${pubKeyHex.take(8)}...") + + val updatedEvent = + if (currentContactList != null) { + println("[UserProfile] Adding to existing contact list") + // Add to existing contact list + ContactListEvent.followUser( + earlierVersion = currentContactList, + pubKeyHex = pubKeyHex, + signer = account.signer, + ) + } else { + println("[UserProfile] Creating new contact list") + // Create new contact list with this user + ContactListEvent.createFromScratch( + followUsers = listOf(ContactTag(pubKeyHex)), + relayUse = emptyMap(), + signer = account.signer, + ) + } + + println("[UserProfile] ContactListEvent created, broadcasting...") + relayManager.broadcastToAll(updatedEvent) + println("[UserProfile] Follow broadcast complete") + } +} + +/** + * Unfollows a user by publishing an updated contact list event without them. + */ +private suspend fun unfollowUser( + pubKeyHex: String, + account: AccountState.LoggedIn, + relayManager: DesktopRelayConnectionManager, + currentContactList: ContactListEvent?, +) { + withContext(Dispatchers.IO) { + println("[UserProfile] Starting unfollowUser: target=${pubKeyHex.take(8)}...") + + if (currentContactList != null) { + println("[UserProfile] Removing from existing contact list") + val updatedEvent = + ContactListEvent.unfollowUser( + earlierVersion = currentContactList, + pubKeyHex = pubKeyHex, + signer = account.signer, + ) + + println("[UserProfile] ContactListEvent updated, broadcasting...") + relayManager.broadcastToAll(updatedEvent) + println("[UserProfile] Unfollow broadcast complete") + } else { + println("[UserProfile] Error: No contact list to unfollow from") + } + } +} From 6581fe18a3f22a3900c906c435b6bf127aebb9ed Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Fri, 2 Jan 2026 07:52:36 +0200 Subject: [PATCH 044/137] initial desktop phase1 fixes --- .../vitorpamplona/amethyst/desktop/Main.kt | 6 +-- .../amethyst/desktop/ui/FeedScreen.kt | 43 +++++++++---------- .../amethyst/desktop/ui/NoteActions.kt | 1 - .../desktop/ui/NotificationsScreen.kt | 35 +++++++++++---- .../amethyst/desktop/ui/UserProfileScreen.kt | 16 +++---- 5 files changed, 58 insertions(+), 43 deletions(-) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt index a04407ee9..898dac477 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt @@ -74,11 +74,9 @@ import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState import com.vitorpamplona.amethyst.commons.account.AccountManager import com.vitorpamplona.amethyst.commons.account.AccountState -import com.vitorpamplona.amethyst.commons.navigation.AppScreen import com.vitorpamplona.amethyst.commons.ui.profile.ProfileInfoCard import com.vitorpamplona.amethyst.commons.ui.relay.RelayStatusCard import com.vitorpamplona.amethyst.commons.ui.screens.MessagesPlaceholder -import com.vitorpamplona.amethyst.commons.ui.screens.NotificationsPlaceholder import com.vitorpamplona.amethyst.commons.ui.screens.SearchPlaceholder import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager import com.vitorpamplona.amethyst.desktop.ui.ComposeNoteDialog @@ -103,7 +101,9 @@ sealed class DesktopScreen { data object MyProfile : DesktopScreen() - data class UserProfile(val pubKeyHex: String) : DesktopScreen() + data class UserProfile( + val pubKeyHex: String, + ) : DesktopScreen() data object Settings : DesktopScreen() } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt index 941def5ee..a3d5744f5 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt @@ -54,7 +54,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.account.AccountState import com.vitorpamplona.amethyst.commons.ui.components.LoadingState -import com.vitorpamplona.amethyst.commons.ui.feed.FeedHeader import com.vitorpamplona.amethyst.commons.ui.note.NoteCard import com.vitorpamplona.amethyst.commons.util.toNoteDisplayData import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager @@ -197,30 +196,30 @@ fun FeedScreen( filters = filters, relays = configuredRelays, listener = - object : IRequestListener { - override fun onEvent( - event: Event, - isLive: Boolean, - relay: NormalizedRelayUrl, - forFilters: List?, - ) { - if (event.id !in seenIds) { - seenIds.add(event.id) - events.add(0, event) - if (events.size > 200) { - val removed = events.removeAt(events.size - 1) - seenIds.remove(removed.id) + object : IRequestListener { + override fun onEvent( + event: Event, + isLive: Boolean, + relay: NormalizedRelayUrl, + forFilters: List?, + ) { + if (event.id !in seenIds) { + seenIds.add(event.id) + events.add(0, event) + if (events.size > 200) { + val removed = events.removeAt(events.size - 1) + seenIds.remove(removed.id) + } } } - } - override fun onEose( - relay: NormalizedRelayUrl, - forFilters: List?, - ) { - // End of stored events - } - }, + override fun onEose( + relay: NormalizedRelayUrl, + forFilters: List?, + ) { + // End of stored events + } + }, ) onDispose { diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt index b630d2c62..870296264 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt @@ -45,7 +45,6 @@ import com.vitorpamplona.amethyst.commons.icons.Repost import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt index 00a3d01ff..10fb8d247 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt @@ -53,7 +53,6 @@ import com.vitorpamplona.amethyst.commons.icons.Repost import com.vitorpamplona.amethyst.commons.icons.Zap import com.vitorpamplona.amethyst.commons.ui.components.LoadingState import com.vitorpamplona.amethyst.commons.ui.feed.FeedHeader -import com.vitorpamplona.amethyst.commons.util.toNoteDisplayData import com.vitorpamplona.amethyst.commons.util.toTimeAgo import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager import com.vitorpamplona.quartz.nip01Core.core.Event @@ -75,17 +74,32 @@ sealed class NotificationItem( open val event: Event, open val timestamp: Long, ) { - data class Mention(override val event: Event, override val timestamp: Long) : NotificationItem(event, timestamp) + data class Mention( + override val event: Event, + override val timestamp: Long, + ) : NotificationItem(event, timestamp) - data class Reply(override val event: Event, override val timestamp: Long) : NotificationItem(event, timestamp) + data class Reply( + override val event: Event, + override val timestamp: Long, + ) : NotificationItem(event, timestamp) - data class Reaction(override val event: Event, override val timestamp: Long, val content: String) : - NotificationItem(event, timestamp) + data class Reaction( + override val event: Event, + override val timestamp: Long, + val content: String, + ) : NotificationItem(event, timestamp) - data class Repost(override val event: Event, override val timestamp: Long) : NotificationItem(event, timestamp) + data class Repost( + override val event: Event, + override val timestamp: Long, + ) : NotificationItem(event, timestamp) - data class Zap(override val event: Event, override val timestamp: Long, val amount: Long?) : - NotificationItem(event, timestamp) + data class Zap( + override val event: Event, + override val timestamp: Long, + val amount: Long?, + ) : NotificationItem(event, timestamp) } @Composable @@ -245,7 +259,10 @@ fun NotificationCard(notification: NotificationItem) { val authorDisplay = try { - notification.event.pubKey.hexToByteArrayOrNull()?.toNpub()?.take(20) ?: notification.event.pubKey.take(20) + notification.event.pubKey + .hexToByteArrayOrNull() + ?.toNpub() + ?.take(20) ?: notification.event.pubKey.take(20) } catch (e: Exception) { notification.event.pubKey.take(20) } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt index a2c9ff798..cc4fbf2b0 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt @@ -65,12 +65,11 @@ import com.vitorpamplona.amethyst.commons.account.AccountState import com.vitorpamplona.amethyst.commons.ui.components.LoadingState import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser -import com.vitorpamplona.quartz.nip01Core.tags.people.pTag import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip02FollowList.ReadWrite import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag @@ -250,13 +249,14 @@ fun UserProfileScreen( ) // Set timeout for loading state - val timeoutJob = kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.Default).launch { - kotlinx.coroutines.delay(10000) // 10 second timeout - if (postsLoading) { - postsError = "Request timed out. Check relay connections." - postsLoading = false + val timeoutJob = + kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.Default).launch { + kotlinx.coroutines.delay(10000) // 10 second timeout + if (postsLoading) { + postsError = "Request timed out. Check relay connections." + postsLoading = false + } } - } onDispose { timeoutJob.cancel() From 2751855186c6d3287c784671db88c7e5d0f51d61 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Fri, 2 Jan 2026 13:29:55 +0200 Subject: [PATCH 045/137] extract actions from amethyst --- .../model/nip18Reposts/RepostAction.kt | 28 +-- .../model/nip25Reactions/ReactionAction.kt | 70 +------ .../amethyst/commons/actions/FollowAction.kt | 123 +++++++++++ .../amethyst/commons/actions/PublishAction.kt | 85 ++++++++ .../commons/actions/ReactionAction.kt | 193 ++++++++++++++++++ .../amethyst/commons/actions/RepostAction.kt | 95 +++++++++ .../amethyst/desktop/ui/ComposeNoteDialog.kt | 45 +--- .../amethyst/desktop/ui/NoteActions.kt | 36 +--- .../amethyst/desktop/ui/UserProfileScreen.kt | 32 +-- 9 files changed, 529 insertions(+), 178 deletions(-) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/FollowAction.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/PublishAction.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/ReactionAction.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/RepostAction.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip18Reposts/RepostAction.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip18Reposts/RepostAction.kt index 4d5d1276d..265a60cbe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip18Reposts/RepostAction.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip18Reposts/RepostAction.kt @@ -23,33 +23,17 @@ package com.vitorpamplona.amethyst.model.nip18Reposts import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent -import com.vitorpamplona.quartz.nip18Reposts.RepostEvent +import com.vitorpamplona.amethyst.commons.actions.RepostAction as CommonsRepostAction +/** + * Android wrapper - pure proxy to commons implementation. + * All logic and validation handled in commons. + */ class RepostAction { companion object { suspend fun repost( note: Note, signer: NostrSigner, - ): Event? { - val noteEvent = note.event ?: return null - - if (note.hasBoostedInTheLast5Minutes(signer.pubKey)) { - // has already bosted in the past 5mins - return null - } - - val noteHint = note.relayHintUrl() - val authorHint = note.author?.bestRelayHint() - - val template = - if (noteEvent.kind == 1) { - RepostEvent.build(noteEvent, noteHint, authorHint) - } else { - GenericRepostEvent.build(noteEvent, noteHint, authorHint) - } - - return signer.sign(template) - } + ): Event? = CommonsRepostAction.repost(note, signer) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip25Reactions/ReactionAction.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip25Reactions/ReactionAction.kt index bf580d23c..192625b0b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip25Reactions/ReactionAction.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip25Reactions/ReactionAction.kt @@ -22,14 +22,15 @@ package com.vitorpamplona.amethyst.model.nip25Reactions import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip17Dm.NIP17Factory -import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag +import com.vitorpamplona.amethyst.commons.actions.ReactionAction as CommonsReactionAction +/** + * Android wrapper - pure proxy to commons implementation. + * All logic and validation handled in commons. + */ class ReactionAction { companion object { suspend fun reactTo( @@ -39,65 +40,6 @@ class ReactionAction { signer: NostrSigner, onPublic: (ReactionEvent) -> Unit, onPrivate: suspend (NIP17Factory.Result) -> Unit, - ) { - if (!signer.isWriteable()) return - - if (note.hasReacted(by, reaction)) { - // has already liked this note - return - } - - val noteEvent = note.event - if (noteEvent is NIP17Group) { - val users = noteEvent.groupMembers().toList() - - if (reaction.startsWith(":")) { - val emojiUrl = EmojiUrlTag.decode(reaction) - if (emojiUrl != null) { - note.toEventHint()?.let { - onPrivate( - NIP17Factory().createReactionWithinGroup( - emojiUrl = emojiUrl, - originalNote = it, - to = users, - signer = signer, - ), - ) - } - - return - } - } - - note.toEventHint()?.let { - onPrivate( - NIP17Factory().createReactionWithinGroup( - content = reaction, - originalNote = it, - to = users, - signer = signer, - ), - ) - } - return - } else { - if (reaction.startsWith(":")) { - val emojiUrl = EmojiUrlTag.decode(reaction) - if (emojiUrl != null) { - note.event?.let { - val template = ReactionEvent.build(emojiUrl, EventHintBundle(it, note.relayHintUrl())) - - onPublic(signer.sign(template)) - } - - return - } - } - - note.toEventHint()?.let { - onPublic(signer.sign(ReactionEvent.build(reaction, it))) - } - } - } + ) = CommonsReactionAction.reactTo(note, reaction, by, signer, onPublic, onPrivate) } } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/FollowAction.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/FollowAction.kt new file mode 100644 index 000000000..1be5372b5 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/FollowAction.kt @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.actions + +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent +import com.vitorpamplona.quartz.nip02FollowList.ReadWrite +import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag + +/** + * Shared action for following/unfollowing users. + * Creates NIP-02 ContactList events (kind 3). + */ +object FollowAction { + /** + * Follows a user by creating an updated ContactListEvent. + * + * @param pubKeyHex The hex public key of the user to follow + * @param signer The NostrSigner to sign the event + * @param currentContactList The current contact list event (if any) + * @return Signed ContactListEvent with the user added + * @throws IllegalStateException if signer is not writeable + */ + suspend fun follow( + pubKeyHex: String, + signer: NostrSigner, + currentContactList: ContactListEvent? = null, + ): ContactListEvent { + if (!signer.isWriteable()) { + throw IllegalStateException("Cannot follow: signer is not writeable") + } + + val updatedEvent = + if (currentContactList != null) { + // Add to existing contact list + ContactListEvent.followUser( + earlierVersion = currentContactList, + pubKeyHex = pubKeyHex, + signer = signer, + ) + } else { + // Create new contact list from scratch + ContactListEvent.createFromScratch( + followUsers = listOf(ContactTag(pubKeyHex)), + relayUse = emptyMap(), + signer = signer, + ) + } + + return updatedEvent + } + + /** + * Unfollows a user by creating an updated ContactListEvent. + * + * @param pubKeyHex The hex public key of the user to unfollow + * @param signer The NostrSigner to sign the event + * @param currentContactList The current contact list event (required for unfollow) + * @return Signed ContactListEvent with the user removed + * @throws IllegalStateException if signer is not writeable + * @throws IllegalArgumentException if currentContactList is null + */ + suspend fun unfollow( + pubKeyHex: String, + signer: NostrSigner, + currentContactList: ContactListEvent?, + ): ContactListEvent { + if (!signer.isWriteable()) { + throw IllegalStateException("Cannot unfollow: signer is not writeable") + } + + requireNotNull(currentContactList) { + "Cannot unfollow: current contact list is required" + } + + // Remove from existing contact list + val updatedEvent = + ContactListEvent.unfollowUser( + earlierVersion = currentContactList, + pubKeyHex = pubKeyHex, + signer = signer, + ) + + return updatedEvent + } + + /** + * Checks if a user is currently followed in the contact list. + * + * @param pubKeyHex The hex public key to check + * @param currentContactList The current contact list event (if any) + * @return true if the user is followed, false otherwise + */ + fun isFollowing( + pubKeyHex: String, + currentContactList: ContactListEvent?, + ): Boolean { + if (currentContactList == null) return false + + // Check if the pubKeyHex is in the contact list's p-tags + return currentContactList.tags.any { tag -> + tag.size >= 2 && tag[0] == "p" && tag[1] == pubKeyHex + } + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/PublishAction.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/PublishAction.kt new file mode 100644 index 000000000..37814d8a4 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/PublishAction.kt @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.actions + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.events.eTag +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTag +import com.vitorpamplona.quartz.nip01Core.tags.references.references +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip10Notes.content.findHashtags +import com.vitorpamplona.quartz.nip10Notes.content.findURLs + +/** + * Shared action for publishing new text notes. + * Handles replies, hashtags, and URL references. + */ +object PublishAction { + /** + * Publishes a text note (NIP-01 kind 1). + * + * @param content The text content of the note + * @param signer The NostrSigner to sign the event + * @param replyTo Optional event to reply to (adds e-tag and p-tag) + * @return Signed TextNoteEvent ready to broadcast + * @throws IllegalStateException if signer is not writeable + */ + suspend fun publishTextNote( + content: String, + signer: NostrSigner, + replyTo: Event? = null, + ): TextNoteEvent { + if (!signer.isWriteable()) { + throw IllegalStateException("Cannot publish: signer is not writeable") + } + + val template = + TextNoteEvent.build(content) { + // If replying, add e-tag and p-tag + if (replyTo != null) { + val etag = ETag(replyTo.id) + etag.relay = null + etag.author = replyTo.pubKey + eTag(etag) + pTag(PTag(replyTo.pubKey, relayHint = null)) + } + + // Extract hashtags and URLs from content + hashtags(findHashtags(content)) + references(findURLs(content)) + } + + return signer.sign(template) + } + + /** + * Publishes a reply to an existing note. + */ + suspend fun publishReply( + content: String, + replyTo: Event, + signer: NostrSigner, + ): TextNoteEvent = publishTextNote(content, signer, replyTo) +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/ReactionAction.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/ReactionAction.kt new file mode 100644 index 000000000..45dbc088f --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/ReactionAction.kt @@ -0,0 +1,193 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.actions + +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip17Dm.NIP17Factory +import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group +import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag + +/** + * Shared action for reacting to events (likes, emoji reactions). + * Supports public reactions, custom emoji reactions, and NIP-17 private group reactions. + * + * Full-featured version extracted from Android for maximum code reuse across platforms. + */ +object ReactionAction { + /** + * Creates a signed reaction event. + * + * @param event The event to react to + * @param reaction The reaction content (e.g., "+", "❤️", ":custom_emoji:") + * @param signer The NostrSigner to sign the event + * @param relayHint Optional relay hint URL where the original event was seen + * @return Signed ReactionEvent ready to broadcast + * @throws IllegalStateException if signer is not writeable + */ + suspend fun reactTo( + event: Event, + reaction: String, + signer: NostrSigner, + relayHint: String? = null, + ): ReactionEvent { + if (!signer.isWriteable()) { + throw IllegalStateException("Cannot react: signer is not writeable") + } + + val normalizedRelayHint = relayHint?.let { RelayUrlNormalizer.normalizeOrNull(it) } + val eventHint = EventHintBundle(event, normalizedRelayHint) + + // Handle custom emoji reactions (format: ":emoji_name:") + val template = + if (reaction.startsWith(":")) { + val emojiUrl = EmojiUrlTag.decode(reaction) + if (emojiUrl != null) { + ReactionEvent.build(emojiUrl, eventHint) + } else { + // Fallback to text if emoji decode fails + ReactionEvent.build(reaction, eventHint) + } + } else { + ReactionEvent.build(reaction, eventHint) + } + + return signer.sign(template) + } + + /** + * Creates a "like" reaction ("+"). + */ + suspend fun like( + event: Event, + signer: NostrSigner, + relayHint: String? = null, + ): ReactionEvent = reactTo(event, "+", signer, relayHint) + + /** + * Advanced: React to an event with support for NIP-17 private groups. + * + * This method handles both public and private group reactions: + * - For NIP17Group events: Creates private reactions within the group + * - For regular events: Creates public reactions + * + * @param event The event to react to + * @param eventHint EventHintBundle with relay information + * @param reaction The reaction content (e.g., "+", "❤️", ":custom_emoji:") + * @param signer The NostrSigner to sign the event + * @param onPublic Callback for public reactions (returns signed ReactionEvent) + * @param onPrivate Callback for private group reactions (returns NIP17Factory.Result) + */ + suspend fun reactToWithGroupSupport( + event: Event, + eventHint: EventHintBundle, + reaction: String, + signer: NostrSigner, + onPublic: suspend (ReactionEvent) -> Unit, + onPrivate: suspend (NIP17Factory.Result) -> Unit, + ) { + if (!signer.isWriteable()) { + throw IllegalStateException("Cannot react: signer is not writeable") + } + + // Check if this is a NIP-17 private group event + if (event is NIP17Group) { + val users = event.groupMembers().toList() + + // Handle custom emoji reactions in groups + if (reaction.startsWith(":")) { + val emojiUrl = EmojiUrlTag.decode(reaction) + if (emojiUrl != null) { + onPrivate( + NIP17Factory().createReactionWithinGroup( + emojiUrl = emojiUrl, + originalNote = eventHint, + to = users, + signer = signer, + ), + ) + return + } + } + + // Regular text reaction in group + onPrivate( + NIP17Factory().createReactionWithinGroup( + content = reaction, + originalNote = eventHint, + to = users, + signer = signer, + ), + ) + } else { + // Public reaction + val template = + if (reaction.startsWith(":")) { + val emojiUrl = EmojiUrlTag.decode(reaction) + if (emojiUrl != null) { + ReactionEvent.build(emojiUrl, eventHint) + } else { + ReactionEvent.build(reaction, eventHint) + } + } else { + ReactionEvent.build(reaction, eventHint) + } + + onPublic(signer.sign(template)) + } + } + + /** + * Android-compatible overload: React to a note with full validation. + * + * This accepts Note/User directly and handles all validation including + * duplicate checking and writeability checks. + * + * @param note The note to react to + * @param reaction The reaction content + * @param by The user reacting + * @param signer The NostrSigner to sign the event + * @param onPublic Callback for public reactions + * @param onPrivate Callback for private group reactions + */ + suspend fun reactTo( + note: Note, + reaction: String, + by: User, + signer: NostrSigner, + onPublic: (ReactionEvent) -> Unit, + onPrivate: suspend (NIP17Factory.Result) -> Unit, + ) { + // All validation in commons + if (!signer.isWriteable()) return + if (note.hasReacted(by, reaction)) return + + val noteEvent = note.event ?: return + val eventHint = note.toEventHint() ?: return + + reactToWithGroupSupport(noteEvent, eventHint, reaction, signer, onPublic, onPrivate) + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/RepostAction.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/RepostAction.kt new file mode 100644 index 000000000..16f478856 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/RepostAction.kt @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.actions + +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent +import com.vitorpamplona.quartz.nip18Reposts.RepostEvent + +/** + * Shared action for reposting (boosting) events. + * Supports both NIP-18 reposts (kind 6 for kind 1) and generic reposts (kind 16 for other kinds). + */ +object RepostAction { + /** + * Creates a signed repost event. + * + * @param event The event to repost + * @param signer The NostrSigner to sign the event + * @param noteHint Optional relay hint where the note was seen + * @param authorHint Optional relay hint for the author's outbox + * @return Signed repost event ready to broadcast + * @throws IllegalStateException if signer is not writeable + */ + suspend fun repost( + event: Event, + signer: NostrSigner, + noteHint: String? = null, + authorHint: String? = null, + ): Event { + if (!signer.isWriteable()) { + throw IllegalStateException("Cannot repost: signer is not writeable") + } + + // Normalize relay hints + val normalizedNoteHint = noteHint?.let { RelayUrlNormalizer.normalizeOrNull(it) } + val normalizedAuthorHint = authorHint?.let { RelayUrlNormalizer.normalizeOrNull(it) } + + // Use NIP-18 RepostEvent (kind 6) for text notes (kind 1) + // Use GenericRepostEvent (kind 16) for all other kinds + val template = + if (event.kind == 1) { + RepostEvent.build(event, normalizedNoteHint, normalizedAuthorHint) + } else { + GenericRepostEvent.build(event, normalizedNoteHint, normalizedAuthorHint) + } + + return signer.sign(template) + } + + /** + * Android-compatible overload: Repost a note with full validation. + * + * This accepts Note directly and handles all validation including + * duplicate checking (5-minute window) and writeability checks. + * + * @param note The note to repost + * @param signer The NostrSigner to sign the event + * @return Signed repost event, or null if validation fails + */ + suspend fun repost( + note: Note, + signer: NostrSigner, + ): Event? { + // All validation in commons + if (!signer.isWriteable()) return null + if (note.hasBoostedInTheLast5Minutes(signer.pubKey)) return null + + val noteEvent = note.event ?: return null + val noteHint = note.relayHintUrl()?.url + val authorHint = note.author?.bestRelayHint()?.url + + return repost(noteEvent, signer, noteHint, authorHint) + } +} diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt index f9e48c53f..bf6ef13a4 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt @@ -44,16 +44,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import com.vitorpamplona.amethyst.commons.account.AccountState +import com.vitorpamplona.amethyst.commons.actions.PublishAction import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager -import com.vitorpamplona.quartz.nip01Core.tags.events.ETag -import com.vitorpamplona.quartz.nip01Core.tags.events.eTag -import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag -import com.vitorpamplona.quartz.nip01Core.tags.people.pTag -import com.vitorpamplona.quartz.nip01Core.tags.references.references -import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip10Notes.content.findHashtags -import com.vitorpamplona.quartz.nip10Notes.content.findURLs import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -187,35 +179,16 @@ private suspend fun publishNote( withContext(Dispatchers.IO) { println("[ComposeNoteDialog] Starting publishNote: content length=${content.length}") - // Build TextNoteEvent using Quartz - val template = - TextNoteEvent.build(content) { - // If replying, add e-tag and p-tag - if (replyTo != null) { - val etag = ETag(replyTo.id) - etag.relay = null - etag.author = replyTo.pubKey - eTag(etag) - pTag(PTag(replyTo.pubKey, relayHint = null)) - } + // Check read-only mode + if (account.isReadOnly) { + println("[ComposeNoteDialog] Error: Cannot post in read-only mode") + throw IllegalStateException("Cannot post in read-only mode") + } - // Extract hashtags and URLs from content - hashtags(findHashtags(content)) - references(findURLs(content)) - } + println("[ComposeNoteDialog] Signing event with pubkey: ${account.pubKeyHex.take(8)}...") - // Sign the event - val signedEvent = - if (account.isReadOnly) { - // Read-only mode - can't sign events - println("[ComposeNoteDialog] Error: Cannot post in read-only mode") - throw IllegalStateException("Cannot post in read-only mode") - } else { - // Sign with nsec (full key) - val signer = account.signer - println("[ComposeNoteDialog] Signing event with pubkey: ${account.pubKeyHex.take(8)}...") - signer.sign(template) - } + // Use shared PublishAction from commons + val signedEvent = PublishAction.publishTextNote(content, account.signer, replyTo) println("[ComposeNoteDialog] Event signed successfully, ID: ${signedEvent.id.take(8)}...") diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt index 870296264..328693e26 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt @@ -40,14 +40,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.account.AccountState +import com.vitorpamplona.amethyst.commons.actions.ReactionAction +import com.vitorpamplona.amethyst.commons.actions.RepostAction import com.vitorpamplona.amethyst.commons.icons.Reply import com.vitorpamplona.amethyst.commons.icons.Repost import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle -import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent -import com.vitorpamplona.quartz.nip18Reposts.RepostEvent -import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -163,12 +161,8 @@ private suspend fun reactToNote( relayManager: DesktopRelayConnectionManager, ) { withContext(Dispatchers.IO) { - // Build reaction event - val reactedTo = EventHintBundle(event, null) - val template = ReactionEvent.like(reactedTo) - - // Sign with user's key - val signedEvent = account.signer.sign(template) + // Use shared ReactionAction from commons + val signedEvent = ReactionAction.reactTo(event, reaction, account.signer) // Broadcast to all relays relayManager.broadcastToAll(signedEvent) @@ -184,26 +178,8 @@ private suspend fun repostNote( relayManager: DesktopRelayConnectionManager, ) { withContext(Dispatchers.IO) { - // Build repost event - val template = - if (event.kind == 1) { - // Text note - use RepostEvent (kind 6) - RepostEvent.build( - boostedPost = event, - eventSourceRelay = null, - authorHomeRelay = null, - ) - } else { - // Other kinds - use GenericRepostEvent (kind 16) - GenericRepostEvent.build( - boostedPost = event, - eventSourceRelay = null, - authorHomeRelay = null, - ) - } - - // Sign with user's key - val signedEvent = account.signer.sign(template) + // Use shared RepostAction from commons + val signedEvent = RepostAction.repost(event, account.signer) // Broadcast to all relays relayManager.broadcastToAll(signedEvent) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt index cc4fbf2b0..02fe9ff2c 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt @@ -62,6 +62,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.account.AccountState +import com.vitorpamplona.amethyst.commons.actions.FollowAction import com.vitorpamplona.amethyst.commons.ui.components.LoadingState import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager import com.vitorpamplona.quartz.nip01Core.core.Event @@ -71,8 +72,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip02FollowList.ReadWrite -import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip19Bech32.toNpub import kotlinx.coroutines.Dispatchers @@ -531,24 +530,8 @@ private suspend fun followUser( withContext(Dispatchers.IO) { println("[UserProfile] Starting followUser: target=${pubKeyHex.take(8)}...") - val updatedEvent = - if (currentContactList != null) { - println("[UserProfile] Adding to existing contact list") - // Add to existing contact list - ContactListEvent.followUser( - earlierVersion = currentContactList, - pubKeyHex = pubKeyHex, - signer = account.signer, - ) - } else { - println("[UserProfile] Creating new contact list") - // Create new contact list with this user - ContactListEvent.createFromScratch( - followUsers = listOf(ContactTag(pubKeyHex)), - relayUse = emptyMap(), - signer = account.signer, - ) - } + // Use shared FollowAction from commons + val updatedEvent = FollowAction.follow(pubKeyHex, account.signer, currentContactList) println("[UserProfile] ContactListEvent created, broadcasting...") relayManager.broadcastToAll(updatedEvent) @@ -570,12 +553,9 @@ private suspend fun unfollowUser( if (currentContactList != null) { println("[UserProfile] Removing from existing contact list") - val updatedEvent = - ContactListEvent.unfollowUser( - earlierVersion = currentContactList, - pubKeyHex = pubKeyHex, - signer = account.signer, - ) + + // Use shared FollowAction from commons + val updatedEvent = FollowAction.unfollow(pubKeyHex, account.signer, currentContactList) println("[UserProfile] ContactListEvent updated, broadcasting...") relayManager.broadcastToAll(updatedEvent) From 36bb89fd36eedefcbd482d3a2996c6990cb4ed60 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Fri, 2 Jan 2026 14:54:24 +0200 Subject: [PATCH 046/137] update desktop with refactored classes --- .../amethyst/commons/model/User.kt | 21 +- .../commons/state/EventCollectionState.kt | 232 ++++++++++++++++++ .../amethyst/commons/state/FollowState.kt | 163 ++++++++++++ .../amethyst/commons/state/LoadingState.kt | 109 ++++++++ .../commons/state/UserMetadataState.kt | 57 +++++ .../amethyst/desktop/ui/FeedScreen.kt | 28 ++- .../desktop/ui/NotificationsScreen.kt | 92 +++---- .../amethyst/desktop/ui/UserProfileScreen.kt | 90 ++++--- 8 files changed, 676 insertions(+), 116 deletions(-) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/state/EventCollectionState.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/state/FollowState.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/state/LoadingState.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/state/UserMetadataState.kt diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt index 08e1af1fd..287992db5 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt @@ -42,7 +42,6 @@ import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.utils.DualCase import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.containsAny -import kotlinx.coroutines.flow.MutableStateFlow import java.math.BigDecimal interface UserDependencies @@ -326,23 +325,9 @@ data class RelayInfo( var counter: Long, ) -@Stable -class UserBundledRefresherFlow( - val user: User, -) { - val stateFlow = MutableStateFlow(UserState(user)) - - fun invalidateData() { - stateFlow.tryEmit(UserState(user)) - } - - fun hasObservers() = stateFlow.subscriptionCount.value > 0 -} - -@Immutable -class UserState( - val user: User, -) +// Re-export from commons.state for backwards compatibility +typealias UserBundledRefresherFlow = com.vitorpamplona.amethyst.commons.state.UserMetadataState +typealias UserState = com.vitorpamplona.amethyst.commons.state.UserState fun Set.toHexSet() = mapTo(LinkedHashSet(size)) { it.pubkeyHex } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/state/EventCollectionState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/state/EventCollectionState.kt new file mode 100644 index 000000000..9307da691 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/state/EventCollectionState.kt @@ -0,0 +1,232 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.state + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +/** + * Generic event collection state with deduplication, batching, sorting, and size limits. + * + * Provides efficient management of event/item collections with: + * - Automatic deduplication by ID + * - Batched updates (250ms default) to reduce recomposition + * - Optional sorting via comparator + * - Automatic trimming to max size + * - Thread-safe operations + * + * @param T The type of items to collect (must have a unique ID) + * @param getId Function to extract unique ID from an item + * @param sortComparator Optional comparator for sorting items (null = prepend newest first) + * @param maxSize Maximum number of items to keep (older items trimmed) + * @param batchDelayMs Delay in milliseconds before flushing batched updates (default 250ms) + * @param scope CoroutineScope for batching jobs + * + * Usage example: + * ``` + * val feedState = EventCollectionState( + * getId = { it.id }, + * sortComparator = compareByDescending { it.createdAt }, + * maxSize = 200, + * scope = viewModelScope + * ) + * + * // Add items (batched automatically) + * feedState.addItem(event) + * feedState.addItems(eventList) + * + * // Observe + * val items by feedState.items.collectAsState() + * ``` + */ +class EventCollectionState( + private val getId: (T) -> String, + private val sortComparator: Comparator? = null, + private val maxSize: Int = 200, + private val batchDelayMs: Long = 250, + private val scope: CoroutineScope, +) { + private val _items = MutableStateFlow>(emptyList()) + val items: StateFlow> = _items.asStateFlow() + + private val seenIds = mutableSetOf() + private val pendingItems = mutableListOf() + private val mutex = Mutex() + private var batchJob: Job? = null + + /** + * Add a single item to the collection. + * Updates are batched and applied after batchDelayMs. + * + * @param item The item to add + */ + fun addItem(item: T) { + scope.launch { + mutex.withLock { + val itemId = getId(item) + if (itemId !in seenIds) { + pendingItems.add(item) + scheduleBatchUpdate() + } + } + } + } + + /** + * Add multiple items to the collection. + * Updates are batched and applied after batchDelayMs. + * + * @param items The items to add + */ + fun addItems(items: List) { + scope.launch { + mutex.withLock { + val newItems = items.filter { getId(it) !in seenIds } + if (newItems.isNotEmpty()) { + pendingItems.addAll(newItems) + scheduleBatchUpdate() + } + } + } + } + + /** + * Remove an item by ID. + * + * @param id The ID of the item to remove + */ + fun removeItem(id: String) { + scope.launch { + mutex.withLock { + seenIds.remove(id) + _items.value = _items.value.filter { getId(it) != id } + } + } + } + + /** + * Remove multiple items by ID. + * + * @param ids The IDs of items to remove + */ + fun removeItems(ids: Set) { + scope.launch { + mutex.withLock { + seenIds.removeAll(ids) + _items.value = _items.value.filter { getId(it) !in ids } + } + } + } + + /** + * Clear all items from the collection. + */ + fun clear() { + scope.launch { + mutex.withLock { + seenIds.clear() + pendingItems.clear() + _items.value = emptyList() + batchJob?.cancel() + batchJob = null + } + } + } + + /** + * Get current item count. + */ + val size: Int + get() = _items.value.size + + /** + * Check if collection is empty. + */ + val isEmpty: Boolean + get() = _items.value.isEmpty() + + /** + * Schedules a batched update if not already scheduled. + * Cancels existing batch job and starts a new one. + */ + private fun scheduleBatchUpdate() { + batchJob?.cancel() + batchJob = + scope.launch { + delay(batchDelayMs) + applyBatchUpdate() + } + } + + /** + * Applies pending items to the collection. + * Merges with existing items, sorts if comparator provided, and trims to maxSize. + */ + private suspend fun applyBatchUpdate() { + mutex.withLock { + if (pendingItems.isEmpty()) return + + // Add pending IDs to seenIds + pendingItems.forEach { seenIds.add(getId(it)) } + + // Merge with existing items + val merged = _items.value + pendingItems + + // Sort if comparator provided, otherwise keep newest first (pending items already at end) + val sorted = + if (sortComparator != null) { + merged.sortedWith(sortComparator) + } else { + // Reverse so newest (pending) items come first + (pendingItems.reversed() + _items.value).distinctBy { getId(it) } + } + + // Trim to maxSize and update seenIds + val trimmed = + if (sorted.size > maxSize) { + val kept = sorted.take(maxSize) + val removed = sorted.drop(maxSize) + removed.forEach { seenIds.remove(getId(it)) } + kept + } else { + sorted + } + + _items.value = trimmed + pendingItems.clear() + } + } + + /** + * Force flush pending items immediately without waiting for batch delay. + */ + suspend fun flush() { + batchJob?.cancel() + applyBatchUpdate() + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/state/FollowState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/state/FollowState.kt new file mode 100644 index 000000000..9996f94ac --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/state/FollowState.kt @@ -0,0 +1,163 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.state + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.commons.actions.FollowAction +import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * Represents the current follow relationship status. + * + * @property isFollowing Whether the target user is currently followed + * @property contactList The current ContactListEvent (NIP-02 kind 3) + */ +@Immutable +data class FollowStatus( + val isFollowing: Boolean, + val contactList: ContactListEvent?, +) + +/** + * Reactive state for tracking follow/unfollow status and actions. + * + * Combines current follow status with action state (loading, success, error) + * using LoadingState pattern. Delegates business logic to FollowAction. + * + * Usage: + * ``` + * val followState = FollowState(myPubKeyHex) + * + * // Update when contact list arrives from relay + * followState.updateContactList(contactListEvent, targetPubKeyHex) + * + * // Follow action + * followState.setFollowLoading() + * try { + * val updated = FollowAction.follow(targetPubKeyHex, signer, contactList) + * relayManager.broadcast(updated) + * followState.setFollowSuccess(updated, targetPubKeyHex) + * } catch (e: Exception) { + * followState.setFollowError(e.message ?: "Follow failed") + * } + * + * // Observe in UI + * when (val state = followState.state.collectAsState().value) { + * is LoadingState.Idle -> { /* Not loaded yet */ } + * is LoadingState.Loading -> CircularProgressIndicator() + * is LoadingState.Success -> { + * val status = state.data + * if (status.isFollowing) UnfollowButton() else FollowButton() + * } + * is LoadingState.Error -> ErrorMessage(state.message) + * } + * ``` + * + * @property myPubKeyHex The current user's public key hex (for context) + */ +@Stable +class FollowState( + private val myPubKeyHex: String, +) { + private val _state = MutableStateFlow>(LoadingState.Idle) + val state: StateFlow> = _state.asStateFlow() + + /** + * Updates the follow status based on a ContactListEvent. + * + * Checks if targetPubKeyHex is in the contact list's p-tags. + * + * @param event The ContactListEvent (NIP-02 kind 3) + * @param targetPubKeyHex The public key of the user to check + */ + fun updateContactList( + event: ContactListEvent, + targetPubKeyHex: String, + ) { + val isFollowing = FollowAction.isFollowing(targetPubKeyHex, event) + _state.value = LoadingState.Success(FollowStatus(isFollowing, event)) + } + + /** + * Sets the state to Loading (follow/unfollow action in progress). + * + * Call this before initiating a follow/unfollow action. + */ + fun setFollowLoading() { + _state.value = LoadingState.Loading + } + + /** + * Sets the state to Success with updated follow status. + * + * Call this after successfully broadcasting a follow/unfollow event. + * + * @param newContactList The updated ContactListEvent + * @param targetPubKeyHex The public key of the user that was followed/unfollowed + */ + fun setFollowSuccess( + newContactList: ContactListEvent, + targetPubKeyHex: String, + ) { + val isFollowing = FollowAction.isFollowing(targetPubKeyHex, newContactList) + _state.value = LoadingState.Success(FollowStatus(isFollowing, newContactList)) + } + + /** + * Sets the state to Error. + * + * Call this if follow/unfollow action fails. + * + * @param message The error message + * @param throwable Optional throwable for debugging + */ + fun setFollowError( + message: String, + throwable: Throwable? = null, + ) { + _state.value = LoadingState.Error(message, throwable) + } + + /** + * Gets the current follow status, if loaded. + * + * @return FollowStatus if state is Success, null otherwise + */ + fun currentStatusOrNull(): FollowStatus? = state.value.dataOrNull() + + /** + * Gets the current ContactListEvent, if loaded. + * + * @return ContactListEvent if state is Success, null otherwise + */ + fun currentContactListOrNull(): ContactListEvent? = currentStatusOrNull()?.contactList + + /** + * Checks if currently following, if loaded. + * + * @return true if following, false if not following or not loaded + */ + fun isFollowing(): Boolean = currentStatusOrNull()?.isFollowing ?: false +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/state/LoadingState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/state/LoadingState.kt new file mode 100644 index 000000000..fe15f18e9 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/state/LoadingState.kt @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.state + +/** + * Generic loading state representation for async operations. + * + * Provides type-safe state management for loading, success, error, and empty states. + * Eliminates the need for multiple boolean flags (isLoading, hasError, etc.). + * + * @param T The type of data when successfully loaded + * + * Usage example: + * ``` + * val feedState: StateFlow>> = ... + * + * when (val state = feedState.collectAsState().value) { + * is LoadingState.Idle -> { /* Not started yet */ } + * is LoadingState.Loading -> CircularProgressIndicator() + * is LoadingState.Success -> LazyColumn { items(state.data) { ... } } + * is LoadingState.Error -> ErrorMessage(state.message) + * is LoadingState.Empty -> EmptyPlaceholder() + * } + * ``` + */ +sealed class LoadingState { + /** + * Initial state - operation has not started yet. + * Useful for actions (like follow/unfollow) that haven't been triggered. + */ + object Idle : LoadingState() + + /** + * Operation is in progress. + */ + object Loading : LoadingState() + + /** + * Operation completed successfully with data. + * + * @param data The loaded data + */ + data class Success( + val data: T, + ) : LoadingState() + + /** + * Operation failed with an error. + * + * @param message Human-readable error message + * @param throwable Optional exception for debugging/logging + */ + data class Error( + val message: String, + val throwable: Throwable? = null, + ) : LoadingState() + + /** + * Operation completed successfully but returned no data. + * Useful for feeds/lists that have no items. + */ + object Empty : LoadingState() + + /** + * Returns true if this state represents a successful load (Success or Empty). + */ + val isSuccessful: Boolean + get() = this is Success || this is Empty + + /** + * Returns true if this state represents a failure. + */ + val isError: Boolean + get() = this is Error + + /** + * Returns true if this state is currently loading. + */ + val isLoading: Boolean + get() = this is Loading + + /** + * Returns the data if this is a Success state, null otherwise. + */ + fun dataOrNull(): T? = if (this is Success) data else null + + /** + * Returns the error message if this is an Error state, null otherwise. + */ + fun errorOrNull(): String? = if (this is Error) message else null +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/state/UserMetadataState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/state/UserMetadataState.kt new file mode 100644 index 000000000..c19a5aeb5 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/state/UserMetadataState.kt @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.state + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.commons.model.User +import kotlinx.coroutines.flow.MutableStateFlow + +/** + * Reactive state wrapper for User metadata. + * + * Provides a StateFlow that emits UserState when user metadata changes. + * Used by both Android ViewModels and Desktop composables for reactive UI updates. + * + * Android pattern: UserBundledRefresherFlow (typealias for backwards compatibility) + */ +@Stable +class UserMetadataState( + val user: User, +) { + val stateFlow = MutableStateFlow(UserState(user)) + + fun invalidateData() { + stateFlow.tryEmit(UserState(user)) + } + + fun hasObservers() = stateFlow.subscriptionCount.value > 0 +} + +/** + * Immutable snapshot of User state. + * + * Emitted by UserMetadataState.stateFlow to trigger recomposition. + */ +@Immutable +class UserState( + val user: User, +) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt index a3d5744f5..ef027ebbb 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt @@ -45,14 +45,15 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.account.AccountState +import com.vitorpamplona.amethyst.commons.state.EventCollectionState import com.vitorpamplona.amethyst.commons.ui.components.LoadingState import com.vitorpamplona.amethyst.commons.ui.note.NoteCard import com.vitorpamplona.amethyst.commons.util.toNoteDisplayData @@ -108,8 +109,17 @@ fun FeedScreen( ) { val connectedRelays by relayManager.connectedRelays.collectAsState() val relayStatuses by relayManager.relayStatuses.collectAsState() - val events = remember { mutableStateListOf() } - val seenIds = remember { mutableSetOf() } + val scope = rememberCoroutineScope() + val eventState = + remember { + EventCollectionState( + getId = { it.id }, + sortComparator = compareByDescending { it.createdAt }, + maxSize = 200, + scope = scope, + ) + } + val events by eventState.items.collectAsState() var replyToEvent by remember { mutableStateOf(null) } var feedMode by remember { mutableStateOf(FeedMode.GLOBAL) } var followedUsers by remember { mutableStateOf>(emptySet()) } @@ -162,8 +172,7 @@ fun FeedScreen( val configuredRelays = relayStatuses.keys if (configuredRelays.isNotEmpty()) { // Clear previous events when switching modes - events.clear() - seenIds.clear() + eventState.clear() val subId = "${feedMode.name.lowercase()}-feed-${System.currentTimeMillis()}" val filters = @@ -203,14 +212,7 @@ fun FeedScreen( relay: NormalizedRelayUrl, forFilters: List?, ) { - if (event.id !in seenIds) { - seenIds.add(event.id) - events.add(0, event) - if (events.size > 200) { - val removed = events.removeAt(events.size - 1) - seenIds.remove(removed.id) - } - } + eventState.addItem(event) } override fun onEose( diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt index 10fb8d247..336496408 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt @@ -42,8 +42,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -51,6 +51,7 @@ import com.vitorpamplona.amethyst.commons.account.AccountState import com.vitorpamplona.amethyst.commons.icons.Reply import com.vitorpamplona.amethyst.commons.icons.Repost import com.vitorpamplona.amethyst.commons.icons.Zap +import com.vitorpamplona.amethyst.commons.state.EventCollectionState import com.vitorpamplona.amethyst.commons.ui.components.LoadingState import com.vitorpamplona.amethyst.commons.ui.feed.FeedHeader import com.vitorpamplona.amethyst.commons.util.toTimeAgo @@ -109,8 +110,17 @@ fun NotificationsScreen( ) { val connectedRelays by relayManager.connectedRelays.collectAsState() val relayStatuses by relayManager.relayStatuses.collectAsState() - val notifications = remember { mutableStateListOf() } - val seenIds = remember { mutableSetOf() } + val scope = rememberCoroutineScope() + val notificationState = + remember { + EventCollectionState( + getId = { it.event.id }, + sortComparator = null, // Prepend new items (no sorting) + maxSize = 200, + scope = scope, + ) + } + val notifications by notificationState.items.collectAsState() DisposableEffect(relayStatuses, account.pubKeyHex) { val configuredRelays = relayStatuses.keys @@ -150,50 +160,42 @@ fun NotificationsScreen( return } - if (event.id !in seenIds) { - seenIds.add(event.id) - - val notification = - when (event) { - is ReactionEvent -> - NotificationItem.Reaction( - event = event, - timestamp = event.createdAt, - content = event.content, - ) - is RepostEvent, is GenericRepostEvent -> - NotificationItem.Repost( - event = event, - timestamp = event.createdAt, - ) - is LnZapEvent -> { - // Extract amount from zap (simplified - full parsing in production) - val amount = event.amount?.toLong() - NotificationItem.Zap( - event = event, - timestamp = event.createdAt, - amount = amount, - ) - } - is TextNoteEvent -> { - // Check if it's a reply (has e-tag) or mention - val eTags = event.tags.filter { it.size > 1 && it[0] == "e" } - val isReply = eTags.isNotEmpty() - if (isReply) { - NotificationItem.Reply(event, event.createdAt) - } else { - NotificationItem.Mention(event, event.createdAt) - } - } - else -> NotificationItem.Mention(event, event.createdAt) + val notification = + when (event) { + is ReactionEvent -> + NotificationItem.Reaction( + event = event, + timestamp = event.createdAt, + content = event.content, + ) + is RepostEvent, is GenericRepostEvent -> + NotificationItem.Repost( + event = event, + timestamp = event.createdAt, + ) + is LnZapEvent -> { + // Extract amount from zap (simplified - full parsing in production) + val amount = event.amount?.toLong() + NotificationItem.Zap( + event = event, + timestamp = event.createdAt, + amount = amount, + ) } - - notifications.add(0, notification) - if (notifications.size > 200) { - val removed = notifications.removeAt(notifications.size - 1) - seenIds.remove(removed.event.id) + is TextNoteEvent -> { + // Check if it's a reply (has e-tag) or mention + val eTags = event.tags.filter { it.size > 1 && it[0] == "e" } + val isReply = eTags.isNotEmpty() + if (isReply) { + NotificationItem.Reply(event, event.createdAt) + } else { + NotificationItem.Mention(event, event.createdAt) + } + } + else -> NotificationItem.Mention(event, event.createdAt) } - } + + notificationState.addItem(notification) } override fun onEose( diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt index 02fe9ff2c..52ae95e56 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt @@ -51,7 +51,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -63,6 +62,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.account.AccountState import com.vitorpamplona.amethyst.commons.actions.FollowAction +import com.vitorpamplona.amethyst.commons.state.EventCollectionState +import com.vitorpamplona.amethyst.commons.state.FollowState import com.vitorpamplona.amethyst.commons.ui.components.LoadingState import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager import com.vitorpamplona.quartz.nip01Core.core.Event @@ -70,7 +71,6 @@ import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip19Bech32.toNpub @@ -100,19 +100,28 @@ fun UserProfileScreen( var followersCount by remember { mutableStateOf(0) } var followingCount by remember { mutableStateOf(0) } + val scope = rememberCoroutineScope() + // User's posts - val events = remember { mutableStateListOf() } - val seenIds = remember { mutableSetOf() } + val eventState = + remember { + EventCollectionState( + getId = { it.id }, + sortComparator = compareByDescending { it.createdAt }, + maxSize = 200, + scope = scope, + ) + } + val events by eventState.items.collectAsState() var postsLoading by remember { mutableStateOf(true) } var postsError by remember { mutableStateOf(null) } var retryTrigger by remember { mutableStateOf(0) } // Follow state - var isFollowing by remember { mutableStateOf(false) } - var currentContactList by remember { mutableStateOf(null) } - var isFollowLoading by remember { mutableStateOf(false) } - var followError by remember { mutableStateOf(null) } - val scope = rememberCoroutineScope() + val followState = + remember(account) { + FollowState(myPubKeyHex = account?.pubKeyHex ?: "") + } // Load current user's contact list (for follow state) DisposableEffect(relayStatuses, account) { @@ -139,8 +148,7 @@ fun UserProfileScreen( forFilters: List?, ) { if (event is ContactListEvent) { - currentContactList = event - isFollowing = event.isTaggedUser(pubKeyHex) + followState.updateContactList(event, pubKeyHex) } } @@ -165,8 +173,7 @@ fun UserProfileScreen( if (configuredRelays.isNotEmpty()) { postsLoading = true postsError = null - events.clear() - seenIds.clear() + eventState.clear() // Metadata subscription (kind 0) val metadataSubId = "profile-metadata-$pubKeyHex-${System.currentTimeMillis()}" relayManager.subscribe( @@ -227,14 +234,7 @@ fun UserProfileScreen( relay: NormalizedRelayUrl, forFilters: List?, ) { - if (event.id !in seenIds) { - seenIds.add(event.id) - events.add(0, event) - if (events.size > 100) { - val removed = events.removeAt(events.size - 1) - seenIds.remove(removed.id) - } - } + eventState.addItem(event) } override fun onEose( @@ -292,26 +292,28 @@ fun UserProfileScreen( Button( onClick = { scope.launch { - isFollowLoading = true - followError = null + followState.setFollowLoading() try { - if (isFollowing) { - unfollowUser(pubKeyHex, account, relayManager, currentContactList) - isFollowing = false - } else { - followUser(pubKeyHex, account, relayManager, currentContactList) - isFollowing = true - } + val currentStatus = followState.currentStatusOrNull() + val updatedEvent = + if (currentStatus?.isFollowing == true) { + unfollowUser(pubKeyHex, account, relayManager, currentStatus.contactList) + } else { + followUser(pubKeyHex, account, relayManager, currentStatus?.contactList) + } + followState.setFollowSuccess(updatedEvent, pubKeyHex) } catch (e: Exception) { - followError = e.message ?: "Failed to update follow status" - } finally { - isFollowLoading = false + followState.setFollowError(e.message ?: "Failed to update follow status", e) } } }, - enabled = !isFollowLoading, + enabled = followState.state.value !is com.vitorpamplona.amethyst.commons.state.LoadingState.Loading, ) { - if (isFollowLoading) { + val state = followState.state.collectAsState().value + val isFollowing = (state as? com.vitorpamplona.amethyst.commons.state.LoadingState.Success)?.data?.isFollowing ?: false + val isLoading = state is com.vitorpamplona.amethyst.commons.state.LoadingState.Loading + + if (isLoading) { androidx.compose.material3.CircularProgressIndicator( modifier = Modifier.size(16.dp), strokeWidth = 2.dp, @@ -330,7 +332,12 @@ fun UserProfileScreen( } } - followError?.let { error -> + val errorMessage = + followState.state + .collectAsState() + .value + .errorOrNull() + errorMessage?.let { error -> Spacer(Modifier.height(4.dp)) Text( error, @@ -526,7 +533,7 @@ private suspend fun followUser( account: AccountState.LoggedIn, relayManager: DesktopRelayConnectionManager, currentContactList: ContactListEvent?, -) { +): ContactListEvent = withContext(Dispatchers.IO) { println("[UserProfile] Starting followUser: target=${pubKeyHex.take(8)}...") @@ -536,8 +543,9 @@ private suspend fun followUser( println("[UserProfile] ContactListEvent created, broadcasting...") relayManager.broadcastToAll(updatedEvent) println("[UserProfile] Follow broadcast complete") + + updatedEvent } -} /** * Unfollows a user by publishing an updated contact list event without them. @@ -547,7 +555,7 @@ private suspend fun unfollowUser( account: AccountState.LoggedIn, relayManager: DesktopRelayConnectionManager, currentContactList: ContactListEvent?, -) { +): ContactListEvent = withContext(Dispatchers.IO) { println("[UserProfile] Starting unfollowUser: target=${pubKeyHex.take(8)}...") @@ -560,8 +568,10 @@ private suspend fun unfollowUser( println("[UserProfile] ContactListEvent updated, broadcasting...") relayManager.broadcastToAll(updatedEvent) println("[UserProfile] Unfollow broadcast complete") + + updatedEvent } else { println("[UserProfile] Error: No contact list to unfollow from") + throw IllegalStateException("Cannot unfollow: No contact list available") } } -} From 307cff87e768e045bf39b73e19308a995f13515f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 2 Jan 2026 14:54:30 -0500 Subject: [PATCH 047/137] Adds more settings to pick different ways to build the SQL database --- .../nip01Core/store/sqlite/AddressableTest.kt | 166 ++--- .../nip01Core/store/sqlite/BaseDBTest.kt | 57 ++ .../nip01Core/store/sqlite/BasicTest.kt | 308 ++++---- .../nip01Core/store/sqlite/DeletionTest.kt | 664 +++++++++--------- .../nip01Core/store/sqlite/ExpirationTest.kt | 101 ++- .../store/sqlite/FilterMatcherTest.kt | 152 ++-- .../nip01Core/store/sqlite/LargeDBTests.kt | 2 + .../store/sqlite/QueryAssemblerTest.kt | 558 +++++++-------- .../nip01Core/store/sqlite/ReplaceableTest.kt | 219 +++--- .../store/sqlite/RightToVanishTest.kt | 163 ++--- .../nip01Core/store/sqlite/SearchTest.kt | 50 +- .../store/sqlite/EventIndexesModule.kt | 34 +- .../nip01Core/store/sqlite/EventStore.kt | 4 +- .../store/sqlite/IndexingStrategy.kt | 8 +- .../nip01Core/store/sqlite/QueryBuilder.kt | 238 ++++++- .../store/sqlite/SQLiteEventStore.kt | 6 +- .../nip01Core/store/sqlite/sql/Condition.kt | 2 + .../store/sqlite/sql/SqlSelectionBuilder.kt | 3 + .../store/sqlite/sql/WhereClauseBuilder.kt | 2 +- 19 files changed, 1452 insertions(+), 1285 deletions(-) create mode 100644 quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/BaseDBTest.kt diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/AddressableTest.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/AddressableTest.kt index 6214aecc6..d5b2b7ae7 100644 --- a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/AddressableTest.kt +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/AddressableTest.kt @@ -20,9 +20,7 @@ */ package com.vitorpamplona.quartz.nip01Core.store.sqlite -import android.content.Context import android.database.sqlite.SQLiteConstraintException -import androidx.test.core.app.ApplicationProvider import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent @@ -30,91 +28,91 @@ import com.vitorpamplona.quartz.utils.TimeUtils import junit.framework.TestCase import junit.framework.TestCase.assertEquals import junit.framework.TestCase.fail -import org.junit.After -import org.junit.Before import org.junit.Test -class AddressableTest { - private lateinit var db: EventStore - +class AddressableTest : BaseDBTest() { val signer = NostrSignerSync() - @Before - fun setup() { - val context = ApplicationProvider.getApplicationContext() - db = EventStore(context, null) - } - - @After - fun tearDown() { - db.close() - } - @Test - fun testReplacingAddressables() { - val time = TimeUtils.now() - val version1 = signer.sign(LongTextNoteEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time)) - val version2 = signer.sign(LongTextNoteEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1)) - val version3 = signer.sign(LongTextNoteEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2)) + fun testReplacingAddressables() = + forEachDB { db -> + val time = TimeUtils.now() + val version1 = signer.sign(LongTextNoteEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time)) + val version2 = signer.sign(LongTextNoteEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1)) + val version3 = signer.sign(LongTextNoteEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2)) - val addressableQuery = Filter(kinds = listOf(version1.kind), authors = listOf(version1.pubKey), tags = mapOf("d" to listOf(version1.dTag()))) + val addressableQuery = Filter(kinds = listOf(version1.kind), authors = listOf(version1.pubKey), tags = mapOf("d" to listOf(version1.dTag()))) - db.insert(version1) - - db.assertQuery(version1, Filter(ids = listOf(version1.id))) - db.assertQuery(version1, addressableQuery) - - db.insert(version2) - - db.assertQuery(null, Filter(ids = listOf(version1.id))) - db.assertQuery(version2, Filter(ids = listOf(version2.id))) - db.assertQuery(version2, addressableQuery) - - db.insert(version3) - - db.assertQuery(null, Filter(ids = listOf(version1.id))) - db.assertQuery(null, Filter(ids = listOf(version2.id))) - db.assertQuery(version3, Filter(ids = listOf(version3.id))) - db.assertQuery(version3, addressableQuery) - } - - @Test - fun testBlockingOldAddressables() { - val time = TimeUtils.now() - val version1 = signer.sign(LongTextNoteEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time)) - val version2 = signer.sign(LongTextNoteEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1)) - val version3 = signer.sign(LongTextNoteEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2)) - - val addressableQuery = Filter(kinds = listOf(version1.kind), authors = listOf(version1.pubKey), tags = mapOf("d" to listOf(version1.dTag()))) - - db.insert(version3) - - db.assertQuery(version3, Filter(ids = listOf(version3.id))) - - try { - db.insert(version2) - fail("It should not allow inserting an older version") - } catch (e: Exception) { - TestCase.assertTrue(e is SQLiteConstraintException) - } - - try { db.insert(version1) - fail("It should not allow inserting an older version") - } catch (e: Exception) { - TestCase.assertTrue(e is SQLiteConstraintException) + + db.assertQuery(version1, Filter(ids = listOf(version1.id))) + db.assertQuery(version1, addressableQuery) + + db.insert(version2) + + db.assertQuery(null, Filter(ids = listOf(version1.id))) + db.assertQuery(version2, Filter(ids = listOf(version2.id))) + db.assertQuery(version2, addressableQuery) + + db.insert(version3) + + db.assertQuery(null, Filter(ids = listOf(version1.id))) + db.assertQuery(null, Filter(ids = listOf(version2.id))) + db.assertQuery(version3, Filter(ids = listOf(version3.id))) + db.assertQuery(version3, addressableQuery) } - db.assertQuery(version3, Filter(ids = listOf(version3.id))) - db.assertQuery(version3, addressableQuery) - db.assertQuery(null, Filter(ids = listOf(version2.id))) - db.assertQuery(null, Filter(ids = listOf(version1.id))) - } + @Test + fun testBlockingOldAddressables() = + forEachDB { db -> + val time = TimeUtils.now() + val version1 = signer.sign(LongTextNoteEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time)) + val version2 = signer.sign(LongTextNoteEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1)) + val version3 = signer.sign(LongTextNoteEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2)) + + val addressableQuery = Filter(kinds = listOf(version1.kind), authors = listOf(version1.pubKey), tags = mapOf("d" to listOf(version1.dTag()))) + + db.insert(version3) + + db.assertQuery(version3, Filter(ids = listOf(version3.id))) + + try { + db.insert(version2) + fail("It should not allow inserting an older version") + } catch (e: Exception) { + TestCase.assertTrue(e is SQLiteConstraintException) + } + + try { + db.insert(version1) + fail("It should not allow inserting an older version") + } catch (e: Exception) { + TestCase.assertTrue(e is SQLiteConstraintException) + } + + db.assertQuery(version3, Filter(ids = listOf(version3.id))) + db.assertQuery(version3, addressableQuery) + db.assertQuery(null, Filter(ids = listOf(version2.id))) + db.assertQuery(null, Filter(ids = listOf(version1.id))) + } @Test - fun testTriggersIndexUsage() { - val explainer = - db.store.explainQuery( + fun testTriggersIndexUsage() = + forEachDB { db -> + val explainer = + db.store.explainQuery( + """ + SELECT * FROM event_headers + WHERE + event_headers.kind = 30000 AND + event_headers.pubkey = 'aa' AND + event_headers.d_tag = 'test-tag' AND + event_headers.created_at < 1766686500 AND + event_headers.kind >= 30000 AND event_headers.kind < 40000 + """.trimIndent(), + ) + + assertEquals( """ SELECT * FROM event_headers WHERE @@ -123,21 +121,9 @@ class AddressableTest { event_headers.d_tag = 'test-tag' AND event_headers.created_at < 1766686500 AND event_headers.kind >= 30000 AND event_headers.kind < 40000 + └── SEARCH event_headers USING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?) """.trimIndent(), + explainer, ) - - assertEquals( - """ - SELECT * FROM event_headers - WHERE - event_headers.kind = 30000 AND - event_headers.pubkey = 'aa' AND - event_headers.d_tag = 'test-tag' AND - event_headers.created_at < 1766686500 AND - event_headers.kind >= 30000 AND event_headers.kind < 40000 - └── SEARCH event_headers USING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?) - """.trimIndent(), - explainer, - ) - } + } } diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/BaseDBTest.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/BaseDBTest.kt new file mode 100644 index 000000000..a1fe2259a --- /dev/null +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/BaseDBTest.kt @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.store.sqlite + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import org.junit.After +import org.junit.Before + +open class BaseDBTest { + private lateinit var dbs: Map + + @Before + fun setup() { + val context = ApplicationProvider.getApplicationContext() + + dbs = + mapOf( + "Default" to EventStore(context, null, indexStrategy = DefaultIndexingStrategy(false, false)), + "IndexAll" to EventStore(context, null, indexStrategy = DefaultIndexingStrategy(true, false)), + "Order by ID" to EventStore(context, null, indexStrategy = DefaultIndexingStrategy(false, true)), + "IndexAll, Order by ID" to EventStore(context, null, indexStrategy = DefaultIndexingStrategy(true, true)), + ) + } + + @After + fun tearDown() { + dbs.forEach { it.value.close() } + } + + fun forEachDB(action: (EventStore) -> Unit) { + dbs.forEach { + println("--------------------") + println(it.key) + println("--------------------") + action(it.value) + } + } +} diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/BasicTest.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/BasicTest.kt index e0a6537bf..256689628 100644 --- a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/BasicTest.kt +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/BasicTest.kt @@ -20,8 +20,6 @@ */ package com.vitorpamplona.quartz.nip01Core.store.sqlite -import android.content.Context -import androidx.test.core.app.ApplicationProvider import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter @@ -30,18 +28,14 @@ import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtag import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue -import org.junit.Before import org.junit.Test -class BasicTest { - private lateinit var db: SQLiteEventStore - +class BasicTest : BaseDBTest() { val signer = NostrSignerSync() - companion object Companion { + companion object { val profile = MetadataEvent( id = "490d7439e530423f2540d4f2bdb73a0a2935f3df9e1f2a6f699a140c7db311fe", @@ -82,168 +76,166 @@ class BasicTest { ) } - @Before - fun setup() { - val context = ApplicationProvider.getApplicationContext() - db = SQLiteEventStore(context, null) - } - - @After - fun tearDown() { - db.close() - } - @Test - fun testInsertDeleteEvent() { - val note = signer.sign(TextNoteEvent.build("test1")) + fun testInsertDeleteEvent() = + forEachDB { db -> + val note = signer.sign(TextNoteEvent.build("test1")) - db.insertEvent(note) + db.store.insertEvent(note) - db.assertQuery(note, Filter(ids = listOf(note.id))) + db.store.assertQuery(note, Filter(ids = listOf(note.id))) - db.delete(note.id) + db.store.delete(note.id) - db.assertQuery(null, Filter(ids = listOf(note.id))) + db.store.assertQuery(null, Filter(ids = listOf(note.id))) - db.insertEvent(note) + db.store.insertEvent(note) - db.assertQuery(note, Filter(ids = listOf(note.id))) - } - - @Test - fun testEmptyFilter() { - val note1 = signer.sign(TextNoteEvent.build("test1", createdAt = 1)) - val note2 = signer.sign(TextNoteEvent.build("test2", createdAt = 2)) - - db.insertEvent(note1) - - db.assertQuery(note1, Filter()) - - db.insertEvent(note2) - - db.assertQuery(listOf(note2, note1), Filter()) - } - - @Test - fun testLimitFilter() { - val note1 = signer.sign(TextNoteEvent.build("test1", createdAt = 1)) - val note2 = signer.sign(TextNoteEvent.build("test2", createdAt = 2)) - val note3 = signer.sign(TextNoteEvent.build("test3", createdAt = 3)) - val note4 = signer.sign(TextNoteEvent.build("test4", createdAt = 4)) - - db.insertEvent(note1) - - db.assertQuery(note1, Filter(limit = 1)) - - db.insertEvent(note2) - db.insertEvent(note3) - db.insertEvent(note4) - - db.assertQuery(listOf(note4), Filter(limit = 1)) - } - - @Test - fun testPubkeyTag() { - db.insertEvent(comment) - db.insertEvent(profile) - - db.assertQuery( - comment, - Filter(authors = listOf(comment.pubKey), tags = mapOf("I" to listOf("geo:drt3n"))), - ) - } - - @Test - fun testTagOnly() { - db.insertEvent(comment) - db.insertEvent(profile) - - db.assertQuery(comment, Filter(tags = mapOf("I" to listOf("geo:drt3n")))) - } - - @Test - fun testTagWithSinceOnly() { - db.insertEvent(comment) - db.insertEvent(profile) - - db.assertQuery( - comment, - Filter(tags = mapOf("I" to listOf("geo:drt3n")), since = comment.createdAt - 1), - ) - db.assertQuery( - comment, - Filter(tags = mapOf("I" to listOf("geo:drt3n")), since = comment.createdAt), - ) - db.assertQuery( - null, - Filter(tags = mapOf("I" to listOf("geo:drt3n")), since = comment.createdAt + 1), - ) - } - - @Test - fun testTagWithUntilOnly() { - db.insertEvent(comment) - db.insertEvent(profile) - - db.assertQuery( - null, - Filter(tags = mapOf("I" to listOf("geo:drt3n")), until = comment.createdAt - 1), - ) - db.assertQuery( - comment, - Filter(tags = mapOf("I" to listOf("geo:drt3n")), until = comment.createdAt), - ) - db.assertQuery( - comment, - Filter(tags = mapOf("I" to listOf("geo:drt3n")), until = comment.createdAt + 1), - ) - } - - @Test - fun testTagWithUntilOnlyEmitting() { - db.insertEvent(comment) - db.insertEvent(profile) - - db.query(Filter(tags = mapOf("I" to listOf("geo:drt3n")))) { event -> - assertEquals(comment.toJson(), event.toJson()) + db.store.assertQuery(note, Filter(ids = listOf(note.id))) } - } @Test - fun hashCodeTest() { - val note1 = - signer.sign( - TextNoteEvent.build("test1") { - hashtag("AaAa") - }, - ) - val note2 = - signer.sign( - TextNoteEvent.build("test2") { - hashtag("AaAa") - }, - ) - val note3 = - signer.sign( - TextNoteEvent.build("test3") { - hashtag("BBBB") - }, - ) + fun testEmptyFilter() = + forEachDB { db -> + val note1 = signer.sign(TextNoteEvent.build("test1", createdAt = 1)) + val note2 = signer.sign(TextNoteEvent.build("test2", createdAt = 2)) - db.insertEvent(note1) - db.insertEvent(note2) - db.insertEvent(note3) + db.store.insertEvent(note1) - val list = - db.query( - Filter( - tags = mapOf("t" to listOf("AaAa")), - ), - ) + db.store.assertQuery(note1, Filter()) - assertEquals(2, list.size) - list.forEach { - assertTrue(it.isTaggedHash("AaAa")) + db.store.insertEvent(note2) + + db.store.assertQuery(listOf(note2, note1), Filter()) + } + + @Test + fun testLimitFilter() = + forEachDB { db -> + val note1 = signer.sign(TextNoteEvent.build("test1", createdAt = 1)) + val note2 = signer.sign(TextNoteEvent.build("test2", createdAt = 2)) + val note3 = signer.sign(TextNoteEvent.build("test3", createdAt = 3)) + val note4 = signer.sign(TextNoteEvent.build("test4", createdAt = 4)) + + db.store.insertEvent(note1) + + db.store.assertQuery(note1, Filter(limit = 1)) + + db.store.insertEvent(note2) + db.store.insertEvent(note3) + db.store.insertEvent(note4) + + db.store.assertQuery(listOf(note4), Filter(limit = 1)) + } + + @Test + fun testPubkeyTag() = + forEachDB { db -> + db.store.insertEvent(comment) + db.store.insertEvent(profile) + + db.store.assertQuery( + comment, + Filter(authors = listOf(comment.pubKey), tags = mapOf("I" to listOf("geo:drt3n"))), + ) + } + + @Test + fun testTagOnly() = + forEachDB { db -> + db.store.insertEvent(comment) + db.store.insertEvent(profile) + + db.store.assertQuery(comment, Filter(tags = mapOf("I" to listOf("geo:drt3n")))) + } + + @Test + fun testTagWithSinceOnly() = + forEachDB { db -> + db.store.insertEvent(comment) + db.store.insertEvent(profile) + + db.store.assertQuery( + comment, + Filter(tags = mapOf("I" to listOf("geo:drt3n")), since = comment.createdAt - 1), + ) + db.store.assertQuery( + comment, + Filter(tags = mapOf("I" to listOf("geo:drt3n")), since = comment.createdAt), + ) + db.store.assertQuery( + null, + Filter(tags = mapOf("I" to listOf("geo:drt3n")), since = comment.createdAt + 1), + ) + } + + @Test + fun testTagWithUntilOnly() = + forEachDB { db -> + db.store.insertEvent(comment) + db.store.insertEvent(profile) + + db.store.assertQuery( + null, + Filter(tags = mapOf("I" to listOf("geo:drt3n")), until = comment.createdAt - 1), + ) + db.store.assertQuery( + comment, + Filter(tags = mapOf("I" to listOf("geo:drt3n")), until = comment.createdAt), + ) + db.store.assertQuery( + comment, + Filter(tags = mapOf("I" to listOf("geo:drt3n")), until = comment.createdAt + 1), + ) + } + + @Test + fun testTagWithUntilOnlyEmitting() = + forEachDB { db -> + db.store.insertEvent(comment) + db.store.insertEvent(profile) + + db.store.query(Filter(tags = mapOf("I" to listOf("geo:drt3n")))) { event -> + assertEquals(comment.toJson(), event.toJson()) + } + } + + @Test + fun hashCodeTest() = + forEachDB { db -> + val note1 = + signer.sign( + TextNoteEvent.build("test1") { + hashtag("AaAa") + }, + ) + val note2 = + signer.sign( + TextNoteEvent.build("test2") { + hashtag("AaAa") + }, + ) + val note3 = + signer.sign( + TextNoteEvent.build("test3") { + hashtag("BBBB") + }, + ) + + db.store.insertEvent(note1) + db.store.insertEvent(note2) + db.store.insertEvent(note3) + + val list = + db.query( + Filter( + tags = mapOf("t" to listOf("AaAa")), + ), + ) + + assertEquals(2, list.size) + list.forEach { + assertTrue(it.isTaggedHash("AaAa")) + } } - } } diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionTest.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionTest.kt index 064024322..9d1ffc3c3 100644 --- a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionTest.kt +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionTest.kt @@ -20,9 +20,7 @@ */ package com.vitorpamplona.quartz.nip01Core.store.sqlite -import android.content.Context import android.database.sqlite.SQLiteConstraintException -import androidx.test.core.app.ApplicationProvider import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync @@ -33,380 +31,372 @@ import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.utils.TimeUtils import junit.framework.TestCase import junit.framework.TestCase.fail -import org.junit.After import org.junit.Assert.assertEquals -import org.junit.Before import org.junit.Test -class DeletionTest { - private lateinit var db: EventStore - +class DeletionTest : BaseDBTest() { val signer = NostrSignerSync() - @Before - fun setup() { - val context = ApplicationProvider.getApplicationContext() - db = EventStore(context, null) - } - - @After - fun tearDown() { - db.close() - } - @Test - fun testInsertDeleteEvent() { - val note1 = signer.sign(TextNoteEvent.build("test1")) - val note2 = signer.sign(TextNoteEvent.build("test2")) - val note3 = signer.sign(TextNoteEvent.build("test3")) + fun testInsertDeleteEvent() = + forEachDB { db -> + val note1 = signer.sign(TextNoteEvent.build("test1")) + val note2 = signer.sign(TextNoteEvent.build("test2")) + val note3 = signer.sign(TextNoteEvent.build("test3")) - db.insert(note1) - db.insert(note2) - db.insert(note3) - - db.assertQuery(note1, Filter(ids = listOf(note1.id))) - db.assertQuery(note2, Filter(ids = listOf(note2.id))) - db.assertQuery(note3, Filter(ids = listOf(note3.id))) - - val deletion = signer.sign(DeletionEvent.build(listOf(note1))) - - db.insert(deletion) - - db.assertQuery(deletion, Filter(ids = listOf(deletion.id))) - db.assertQuery(null, Filter(ids = listOf(note1.id))) - db.assertQuery(note2, Filter(ids = listOf(note2.id))) - db.assertQuery(note3, Filter(ids = listOf(note3.id))) - - // trying to insert again should fail. - try { db.insert(note1) - fail("Should not be able to insert a deleted event") - } catch (e: SQLiteConstraintException) { - assertEquals("blocked: a deletion event exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message) + db.insert(note2) + db.insert(note3) + + db.assertQuery(note1, Filter(ids = listOf(note1.id))) + db.assertQuery(note2, Filter(ids = listOf(note2.id))) + db.assertQuery(note3, Filter(ids = listOf(note3.id))) + + val deletion = signer.sign(DeletionEvent.build(listOf(note1))) + + db.insert(deletion) + + db.assertQuery(deletion, Filter(ids = listOf(deletion.id))) + db.assertQuery(null, Filter(ids = listOf(note1.id))) + db.assertQuery(note2, Filter(ids = listOf(note2.id))) + db.assertQuery(note3, Filter(ids = listOf(note3.id))) + + // trying to insert again should fail. + try { + db.insert(note1) + fail("Should not be able to insert a deleted event") + } catch (e: SQLiteConstraintException) { + assertEquals("blocked: a deletion event exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message) + } + + db.assertQuery(deletion, Filter(ids = listOf(deletion.id))) + db.assertQuery(null, Filter(ids = listOf(note1.id))) + db.assertQuery(note2, Filter(ids = listOf(note2.id))) + db.assertQuery(note3, Filter(ids = listOf(note3.id))) } - db.assertQuery(deletion, Filter(ids = listOf(deletion.id))) - db.assertQuery(null, Filter(ids = listOf(note1.id))) - db.assertQuery(note2, Filter(ids = listOf(note2.id))) - db.assertQuery(note3, Filter(ids = listOf(note3.id))) - } - @Test - fun testInsertDeleteEventOfAddressable() { - val time = TimeUtils.now() - val note1 = signer.sign(LongTextNoteEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time)) - val note2 = signer.sign(LongTextNoteEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1)) - val note3 = signer.sign(LongTextNoteEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2)) + fun testInsertDeleteEventOfAddressable() = + forEachDB { db -> + val time = TimeUtils.now() + val note1 = signer.sign(LongTextNoteEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time)) + val note2 = signer.sign(LongTextNoteEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1)) + val note3 = signer.sign(LongTextNoteEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2)) - db.insert(note1) - - db.assertQuery(note1, Filter(ids = listOf(note1.id))) - - db.insert(note2) - - db.assertQuery(null, Filter(ids = listOf(note1.id))) - db.assertQuery(note2, Filter(ids = listOf(note2.id))) - - db.insert(note3) - - db.assertQuery(null, Filter(ids = listOf(note1.id))) - db.assertQuery(null, Filter(ids = listOf(note2.id))) - db.assertQuery(note3, Filter(ids = listOf(note3.id))) - - val deletion = signer.sign(DeletionEvent.build(listOf(note1))) - - db.insert(deletion) - - db.assertQuery(deletion, Filter(ids = listOf(deletion.id))) - db.assertQuery(null, Filter(ids = listOf(note1.id))) - db.assertQuery(null, Filter(ids = listOf(note2.id))) - db.assertQuery(null, Filter(ids = listOf(note3.id))) - - // trying to insert again should fail. - try { db.insert(note1) - fail("Should not be able to insert a deleted event") - } catch (e: SQLiteConstraintException) { - assertEquals("blocked: a deletion event exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message) + + db.assertQuery(note1, Filter(ids = listOf(note1.id))) + + db.insert(note2) + + db.assertQuery(null, Filter(ids = listOf(note1.id))) + db.assertQuery(note2, Filter(ids = listOf(note2.id))) + + db.insert(note3) + + db.assertQuery(null, Filter(ids = listOf(note1.id))) + db.assertQuery(null, Filter(ids = listOf(note2.id))) + db.assertQuery(note3, Filter(ids = listOf(note3.id))) + + val deletion = signer.sign(DeletionEvent.build(listOf(note1))) + + db.insert(deletion) + + db.assertQuery(deletion, Filter(ids = listOf(deletion.id))) + db.assertQuery(null, Filter(ids = listOf(note1.id))) + db.assertQuery(null, Filter(ids = listOf(note2.id))) + db.assertQuery(null, Filter(ids = listOf(note3.id))) + + // trying to insert again should fail. + try { + db.insert(note1) + fail("Should not be able to insert a deleted event") + } catch (e: SQLiteConstraintException) { + assertEquals("blocked: a deletion event exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message) + } + + db.assertQuery(deletion, Filter(ids = listOf(deletion.id))) + db.assertQuery(null, Filter(ids = listOf(note1.id))) + db.assertQuery(null, Filter(ids = listOf(note2.id))) + db.assertQuery(null, Filter(ids = listOf(note3.id))) } - db.assertQuery(deletion, Filter(ids = listOf(deletion.id))) - db.assertQuery(null, Filter(ids = listOf(note1.id))) - db.assertQuery(null, Filter(ids = listOf(note2.id))) - db.assertQuery(null, Filter(ids = listOf(note3.id))) - } - @Test - fun testInsertDeleteEventOfAddressable2() { - val time = TimeUtils.now() - val note1 = signer.sign(LongTextNoteEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time)) - val note2 = signer.sign(LongTextNoteEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1)) - val note3 = signer.sign(LongTextNoteEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2)) + fun testInsertDeleteEventOfAddressable2() = + forEachDB { db -> + val time = TimeUtils.now() + val note1 = signer.sign(LongTextNoteEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time)) + val note2 = signer.sign(LongTextNoteEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1)) + val note3 = signer.sign(LongTextNoteEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2)) - db.insert(note1) - db.insert(note2) - db.insert(note3) - - db.assertQuery(null, Filter(ids = listOf(note1.id))) - db.assertQuery(null, Filter(ids = listOf(note2.id))) - db.assertQuery(note3, Filter(ids = listOf(note3.id))) - - val deletion = signer.sign(DeletionEvent.buildAddressOnly(listOf(note1))) - - db.insert(deletion) - - db.assertQuery(deletion, Filter(ids = listOf(deletion.id))) - db.assertQuery(null, Filter(ids = listOf(note1.id))) - db.assertQuery(null, Filter(ids = listOf(note2.id))) - db.assertQuery(null, Filter(ids = listOf(note3.id))) - - // trying to insert again should fail. - try { db.insert(note1) - fail("Should not be able to insert a deleted event") - } catch (e: SQLiteConstraintException) { - assertEquals("blocked: a deletion event exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message) + db.insert(note2) + db.insert(note3) + + db.assertQuery(null, Filter(ids = listOf(note1.id))) + db.assertQuery(null, Filter(ids = listOf(note2.id))) + db.assertQuery(note3, Filter(ids = listOf(note3.id))) + + val deletion = signer.sign(DeletionEvent.buildAddressOnly(listOf(note1))) + + db.insert(deletion) + + db.assertQuery(deletion, Filter(ids = listOf(deletion.id))) + db.assertQuery(null, Filter(ids = listOf(note1.id))) + db.assertQuery(null, Filter(ids = listOf(note2.id))) + db.assertQuery(null, Filter(ids = listOf(note3.id))) + + // trying to insert again should fail. + try { + db.insert(note1) + fail("Should not be able to insert a deleted event") + } catch (e: SQLiteConstraintException) { + assertEquals("blocked: a deletion event exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message) + } + + db.assertQuery(deletion, Filter(ids = listOf(deletion.id))) + db.assertQuery(null, Filter(ids = listOf(note1.id))) + db.assertQuery(null, Filter(ids = listOf(note2.id))) + db.assertQuery(null, Filter(ids = listOf(note3.id))) } - db.assertQuery(deletion, Filter(ids = listOf(deletion.id))) - db.assertQuery(null, Filter(ids = listOf(note1.id))) - db.assertQuery(null, Filter(ids = listOf(note2.id))) - db.assertQuery(null, Filter(ids = listOf(note3.id))) - } - @Test - fun testInsertDeleteWrap() { - val me = NostrSignerSync() - val myFriend = NostrSignerSync() + fun testInsertDeleteWrap() = + forEachDB { db -> + val me = NostrSignerSync() + val myFriend = NostrSignerSync() - val note1 = me.sign(TextNoteEvent.build("test1")) - val wrap1 = GiftWrapEvent.create(note1, me.pubKey) - val wrap2 = GiftWrapEvent.create(note1, myFriend.pubKey) + val note1 = me.sign(TextNoteEvent.build("test1")) + val wrap1 = GiftWrapEvent.create(note1, me.pubKey) + val wrap2 = GiftWrapEvent.create(note1, myFriend.pubKey) - db.insert(wrap1) - db.insert(wrap2) - - db.assertQuery(wrap1, Filter(ids = listOf(wrap1.id))) - db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id))) - - val randomDeletionToWrap = signer.sign(DeletionEvent.build(listOf(wrap1))) - - db.insert(randomDeletionToWrap) - - db.assertQuery(randomDeletionToWrap, Filter(ids = listOf(randomDeletionToWrap.id))) - db.assertQuery(wrap1, Filter(ids = listOf(wrap1.id))) - db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id))) - - val deletion = me.sign(DeletionEvent.build(listOf(wrap1))) - - db.insert(deletion) - - db.assertQuery(deletion, Filter(ids = listOf(deletion.id))) - db.assertQuery(null, Filter(ids = listOf(wrap1.id))) - db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id))) - - // trying to insert again should fail. - try { db.insert(wrap1) - fail("Should not be able to insert a deleted event") - } catch (e: SQLiteConstraintException) { - assertEquals("blocked: a deletion event exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message) + db.insert(wrap2) + + db.assertQuery(wrap1, Filter(ids = listOf(wrap1.id))) + db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id))) + + val randomDeletionToWrap = signer.sign(DeletionEvent.build(listOf(wrap1))) + + db.insert(randomDeletionToWrap) + + db.assertQuery(randomDeletionToWrap, Filter(ids = listOf(randomDeletionToWrap.id))) + db.assertQuery(wrap1, Filter(ids = listOf(wrap1.id))) + db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id))) + + val deletion = me.sign(DeletionEvent.build(listOf(wrap1))) + + db.insert(deletion) + + db.assertQuery(deletion, Filter(ids = listOf(deletion.id))) + db.assertQuery(null, Filter(ids = listOf(wrap1.id))) + db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id))) + + // trying to insert again should fail. + try { + db.insert(wrap1) + fail("Should not be able to insert a deleted event") + } catch (e: SQLiteConstraintException) { + assertEquals("blocked: a deletion event exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message) + } + + db.assertQuery(deletion, Filter(ids = listOf(deletion.id))) + db.assertQuery(null, Filter(ids = listOf(wrap1.id))) + db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id))) } - db.assertQuery(deletion, Filter(ids = listOf(deletion.id))) - db.assertQuery(null, Filter(ids = listOf(wrap1.id))) - db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id))) - } + @Test + fun testTriggersIndexUsage() = + forEachDB { db -> + val sql = + """ + SELECT 1 FROM event_tags + WHERE + event_tags.tag_hash IN (3221122, 223322) AND + event_tags.kind = 5 AND + event_tags.pubkey_hash = 22332323 AND + event_tags.created_at >= 1766686500 + """.trimIndent() + + val explainer = + db.store.explainQuery(sql) + + TestCase.assertEquals( + """ + ${sql.replace("\n","\n ")} + └── SEARCH event_tags USING COVERING INDEX query_by_tags_hash_kind_pubkey (tag_hash=? AND kind=? AND pubkey_hash=? AND created_at>?) + """.trimIndent(), + explainer, + ) + } @Test - fun testTriggersIndexUsage() { - val sql = - """ - SELECT 1 FROM event_tags - INNER JOIN event_headers - ON event_headers.row_id = event_tags.event_header_row_id - WHERE - event_tags.tag_hash IN (3221122, 223322) AND - event_tags.kind = 5 AND - event_tags.pubkey_hash = 22332323 AND - event_tags.created_at >= 1766686500 - """.trimIndent() + fun testDeleteById() = + forEachDB { db -> + val sql = + db.store.deletionModule + .deleteSQL( + pubkey = "key1", + idValues = listOf("ca29c211f", "ca29c211d"), + addresses = emptyList(), + hasher = TagNameValueHasher(0), + ).first() - val explainer = - db.store.explainQuery(sql) - - TestCase.assertEquals( - """ - ${sql.replace("\n","\n ")} - ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind_pubkey (tag_hash=? AND kind=? AND pubkey_hash=? AND created_at>?) - └── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - """.trimIndent(), - explainer, - ) - } + TestCase.assertEquals( + """ + DELETE FROM event_headers + WHERE + id IN ("ca29c211f","ca29c211d") AND + pubkey_owner_hash = "1573573083296714675" + ├── SEARCH event_headers USING INDEX event_headers_id (id=?) + ├── SEARCH event_vanish USING INTEGER PRIMARY KEY (rowid=?) + ├── SEARCH event_expirations USING INTEGER PRIMARY KEY (rowid=?) + └── SEARCH event_tags USING COVERING INDEX fk_event_tags_header_id (event_header_row_id=?) + """.trimIndent(), + db.store.explainQuery(sql.sql, sql.args), + ) + } @Test - fun testDeleteById() { - val sql = - db.store.deletionModule - .deleteSQL( - pubkey = "key1", - idValues = listOf("ca29c211f", "ca29c211d"), - addresses = emptyList(), - hasher = TagNameValueHasher(0), - ).first() + fun testDeleteAddressable() = + forEachDB { db -> + val sql = + db.store.deletionModule + .deleteSQL( + pubkey = "key1", + idValues = emptyList(), + addresses = + listOf( + Address(30000, "key1", "a"), + ), + hasher = TagNameValueHasher(0), + ).first() - TestCase.assertEquals( - """ - DELETE FROM event_headers - WHERE - id IN ("ca29c211f","ca29c211d") AND - pubkey_owner_hash = "1573573083296714675" - ├── SEARCH event_headers USING INDEX event_headers_id (id=?) - ├── SEARCH event_vanish USING INTEGER PRIMARY KEY (rowid=?) - ├── SEARCH event_expirations USING INTEGER PRIMARY KEY (rowid=?) - └── SEARCH event_tags USING COVERING INDEX fk_event_tags_header_id (event_header_row_id=?) - """.trimIndent(), - db.store.explainQuery(sql.sql, sql.args), - ) - } + TestCase.assertEquals( + """ + DELETE FROM event_headers + WHERE ( + (kind = "30000" AND pubkey = "key1" AND d_tag = "a") + ) AND + kind >= 30000 AND kind < 40000 + ├── SEARCH event_headers USING COVERING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?) + ├── SEARCH event_vanish USING INTEGER PRIMARY KEY (rowid=?) + ├── SEARCH event_expirations USING INTEGER PRIMARY KEY (rowid=?) + └── SEARCH event_tags USING COVERING INDEX fk_event_tags_header_id (event_header_row_id=?) + """.trimIndent(), + db.store.explainQuery(sql.sql, sql.args), + ) + } @Test - fun testDeleteAddressable() { - val sql = - db.store.deletionModule - .deleteSQL( - pubkey = "key1", - idValues = emptyList(), - addresses = - listOf( - Address(30000, "key1", "a"), - ), - hasher = TagNameValueHasher(0), - ).first() + fun testDeleteAddressablesSingleKind() = + forEachDB { db -> + val sql = + db.store.deletionModule + .deleteSQL( + pubkey = "key1", + idValues = emptyList(), + addresses = + listOf( + Address(30000, "key1", "a"), + Address(30000, "key1", "b"), + Address(30000, "key1", "c"), + Address(30000, "key1", "d"), + ), + hasher = TagNameValueHasher(0), + ).first() - TestCase.assertEquals( - """ - DELETE FROM event_headers - WHERE ( - (kind = "30000" AND pubkey = "key1" AND d_tag = "a") - ) AND - kind >= 30000 AND kind < 40000 - ├── SEARCH event_headers USING COVERING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?) - ├── SEARCH event_vanish USING INTEGER PRIMARY KEY (rowid=?) - ├── SEARCH event_expirations USING INTEGER PRIMARY KEY (rowid=?) - └── SEARCH event_tags USING COVERING INDEX fk_event_tags_header_id (event_header_row_id=?) - """.trimIndent(), - db.store.explainQuery(sql.sql, sql.args), - ) - } + TestCase.assertEquals( + """ + DELETE FROM event_headers + WHERE ( + (kind = "30000" AND pubkey = "key1" AND d_tag IN ("a","b","c","d")) + ) AND + kind >= 30000 AND kind < 40000 + ├── SEARCH event_headers USING COVERING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?) + ├── SEARCH event_vanish USING INTEGER PRIMARY KEY (rowid=?) + ├── SEARCH event_expirations USING INTEGER PRIMARY KEY (rowid=?) + └── SEARCH event_tags USING COVERING INDEX fk_event_tags_header_id (event_header_row_id=?) + """.trimIndent(), + db.store.explainQuery(sql.sql, sql.args), + ) + } @Test - fun testDeleteAddressablesSingleKind() { - val sql = - db.store.deletionModule - .deleteSQL( - pubkey = "key1", - idValues = emptyList(), - addresses = - listOf( - Address(30000, "key1", "a"), - Address(30000, "key1", "b"), - Address(30000, "key1", "c"), - Address(30000, "key1", "d"), - ), - hasher = TagNameValueHasher(0), - ).first() + fun testDeleteAddressablesMultipleKinds() = + forEachDB { db -> + val sql = + db.store.deletionModule + .deleteSQL( + pubkey = "key1", + idValues = emptyList(), + addresses = + listOf( + Address(30000, "key1", "a"), + Address(30000, "key1", "b"), + Address(30101, "key1", "c"), + Address(30101, "key1", "d"), + Address(30001, "key2", "e"), + Address(30001, "key2", "f"), + ), + hasher = TagNameValueHasher(0), + ).first() - TestCase.assertEquals( - """ - DELETE FROM event_headers - WHERE ( - (kind = "30000" AND pubkey = "key1" AND d_tag IN ("a","b","c","d")) - ) AND - kind >= 30000 AND kind < 40000 - ├── SEARCH event_headers USING COVERING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?) - ├── SEARCH event_vanish USING INTEGER PRIMARY KEY (rowid=?) - ├── SEARCH event_expirations USING INTEGER PRIMARY KEY (rowid=?) - └── SEARCH event_tags USING COVERING INDEX fk_event_tags_header_id (event_header_row_id=?) - """.trimIndent(), - db.store.explainQuery(sql.sql, sql.args), - ) - } + TestCase.assertEquals( + """ + DELETE FROM event_headers + WHERE ( + (kind = "30000" AND pubkey = "key1" AND d_tag IN ("a","b")) + OR + (kind = "30101" AND pubkey = "key1" AND d_tag IN ("c","d")) + ) AND + kind >= 30000 AND kind < 40000 + ├── MULTI-INDEX OR + │ ├── INDEX 1 + │ │ └── SEARCH event_headers USING COVERING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?) + │ └── INDEX 2 + │ └── SEARCH event_headers USING COVERING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?) + ├── SEARCH event_vanish USING INTEGER PRIMARY KEY (rowid=?) + ├── SEARCH event_expirations USING INTEGER PRIMARY KEY (rowid=?) + └── SEARCH event_tags USING COVERING INDEX fk_event_tags_header_id (event_header_row_id=?) + """.trimIndent(), + db.store.explainQuery(sql.sql, sql.args), + ) + } @Test - fun testDeleteAddressablesMultipleKinds() { - val sql = - db.store.deletionModule - .deleteSQL( - pubkey = "key1", - idValues = emptyList(), - addresses = - listOf( - Address(30000, "key1", "a"), - Address(30000, "key1", "b"), - Address(30101, "key1", "c"), - Address(30101, "key1", "d"), - Address(30001, "key2", "e"), - Address(30001, "key2", "f"), - ), - hasher = TagNameValueHasher(0), - ).first() + fun testDeleteReplaceables() = + forEachDB { db -> + val sql = + db.store.deletionModule + .deleteSQL( + pubkey = "key1", + idValues = emptyList(), + addresses = + listOf( + Address(10000, "key1", ""), + Address(10000, "key1", ""), + Address(10001, "key1", ""), + Address(10001, "key1", ""), + Address(10001, "key2", ""), + Address(10001, "key2", ""), + ), + hasher = TagNameValueHasher(0), + ).first() - TestCase.assertEquals( - """ - DELETE FROM event_headers - WHERE ( - (kind = "30000" AND pubkey = "key1" AND d_tag IN ("a","b")) - OR - (kind = "30101" AND pubkey = "key1" AND d_tag IN ("c","d")) - ) AND - kind >= 30000 AND kind < 40000 - ├── MULTI-INDEX OR - │ ├── INDEX 1 - │ │ └── SEARCH event_headers USING COVERING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?) - │ └── INDEX 2 - │ └── SEARCH event_headers USING COVERING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?) - ├── SEARCH event_vanish USING INTEGER PRIMARY KEY (rowid=?) - ├── SEARCH event_expirations USING INTEGER PRIMARY KEY (rowid=?) - └── SEARCH event_tags USING COVERING INDEX fk_event_tags_header_id (event_header_row_id=?) - """.trimIndent(), - db.store.explainQuery(sql.sql, sql.args), - ) - } - - @Test - fun testDeleteReplaceables() { - val sql = - db.store.deletionModule - .deleteSQL( - pubkey = "key1", - idValues = emptyList(), - addresses = - listOf( - Address(10000, "key1", ""), - Address(10000, "key1", ""), - Address(10001, "key1", ""), - Address(10001, "key1", ""), - Address(10001, "key2", ""), - Address(10001, "key2", ""), - ), - hasher = TagNameValueHasher(0), - ).first() - - TestCase.assertEquals( - """ - DELETE FROM event_headers - WHERE - kind IN ("10000","10001") AND - pubkey = "key1" AND - ((kind in (0,3)) OR (kind >= 10000 AND kind < 20000)) - ├── SEARCH event_headers USING COVERING INDEX replaceable_idx (kind=? AND pubkey=?) - ├── SEARCH event_vanish USING INTEGER PRIMARY KEY (rowid=?) - ├── SEARCH event_expirations USING INTEGER PRIMARY KEY (rowid=?) - └── SEARCH event_tags USING COVERING INDEX fk_event_tags_header_id (event_header_row_id=?) - """.trimIndent(), - db.store.explainQuery(sql.sql, sql.args), - ) - } + TestCase.assertEquals( + """ + DELETE FROM event_headers + WHERE + kind IN ("10000","10001") AND + pubkey = "key1" AND + ((kind in (0,3)) OR (kind >= 10000 AND kind < 20000)) + ├── SEARCH event_headers USING COVERING INDEX replaceable_idx (kind=? AND pubkey=?) + ├── SEARCH event_vanish USING INTEGER PRIMARY KEY (rowid=?) + ├── SEARCH event_expirations USING INTEGER PRIMARY KEY (rowid=?) + └── SEARCH event_tags USING COVERING INDEX fk_event_tags_header_id (event_header_row_id=?) + """.trimIndent(), + db.store.explainQuery(sql.sql, sql.args), + ) + } } diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/ExpirationTest.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/ExpirationTest.kt index 6197a8967..0b86746bc 100644 --- a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/ExpirationTest.kt +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/ExpirationTest.kt @@ -20,84 +20,69 @@ */ package com.vitorpamplona.quartz.nip01Core.store.sqlite -import android.content.Context import android.database.sqlite.SQLiteConstraintException -import androidx.test.core.app.ApplicationProvider import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip40Expiration.expiration import com.vitorpamplona.quartz.utils.TimeUtils import junit.framework.TestCase.fail -import org.junit.After import org.junit.Assert.assertTrue -import org.junit.Before import org.junit.Test -class ExpirationTest { - private lateinit var db: EventStore - +class ExpirationTest : BaseDBTest() { val signer = NostrSignerSync() - @Before - fun setup() { - val context = ApplicationProvider.getApplicationContext() - db = EventStore(context, null) - } - - @After - fun tearDown() { - db.close() - } - @Test - fun testDeletingExpiredEvents() { - val time = TimeUtils.now() + fun testDeletingExpiredEvents() = + forEachDB { db -> + val time = TimeUtils.now() - val noteSafe = - signer.sign( - TextNoteEvent.build("test1", createdAt = time + 1) { - expiration(time + 100) - }, - ) + val noteSafe = + signer.sign( + TextNoteEvent.build("test1", createdAt = time + 1) { + expiration(time + 100) + }, + ) - db.insert(noteSafe) + db.insert(noteSafe) - val noteToExpire = - signer.sign( - TextNoteEvent.build("test1", createdAt = time + 1) { - expiration(time + 1) - }, - ) + val noteToExpire = + signer.sign( + TextNoteEvent.build("test1", createdAt = time + 1) { + expiration(time + 1) + }, + ) - db.insert(noteToExpire) + db.insert(noteToExpire) - db.assertQuery(noteToExpire, Filter(ids = listOf(noteToExpire.id))) + db.assertQuery(noteToExpire, Filter(ids = listOf(noteToExpire.id))) - Thread.sleep(2000) + Thread.sleep(2000) - db.deleteExpiredEvents() + db.deleteExpiredEvents() - db.assertQuery(null, Filter(ids = listOf(noteToExpire.id))) - db.assertQuery(noteSafe, Filter(ids = listOf(noteSafe.id))) - } - - @Test - fun testInsertingExpiredEvents() { - val time = TimeUtils.now() - - val note1 = - signer.sign( - TextNoteEvent.build("test1", createdAt = time - 12) { - expiration(time - 10) - }, - ) - - try { - db.insert(note1) - fail("Should not be able to insert expired events") - } catch (e: Exception) { - assertTrue(e is SQLiteConstraintException) + db.assertQuery(null, Filter(ids = listOf(noteToExpire.id))) + db.assertQuery(noteSafe, Filter(ids = listOf(noteSafe.id))) + } + + @Test + fun testInsertingExpiredEvents() = + forEachDB { db -> + val time = TimeUtils.now() + + val note1 = + signer.sign( + TextNoteEvent.build("test1", createdAt = time - 12) { + expiration(time - 10) + }, + ) + + try { + db.insert(note1) + fail("Should not be able to insert expired events") + } catch (e: Exception) { + assertTrue(e is SQLiteConstraintException) + } } - } } diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/FilterMatcherTest.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/FilterMatcherTest.kt index 22f3b5c17..88a53e734 100644 --- a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/FilterMatcherTest.kt +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/FilterMatcherTest.kt @@ -20,28 +20,11 @@ */ package com.vitorpamplona.quartz.nip01Core.store.sqlite -import android.content.Context -import androidx.test.core.app.ApplicationProvider import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import org.junit.After -import org.junit.Before import org.junit.Test -class FilterMatcherTest { - private lateinit var db: EventStore - - @Before - fun setup() { - val context = ApplicationProvider.getApplicationContext() - db = EventStore(context, null) - } - - @After - fun tearDown() { - db.close() - } - +class FilterMatcherTest : BaseDBTest() { val id = "98b574c3527f0ffb30b7271084e3f07480733c7289f8de424d29eae82e36c758" val pubkey = "46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d" val createdAt: Long = 1683596206 @@ -83,91 +66,100 @@ class FilterMatcherTest { ) @Test - fun matchIds() { - db.insert(note) + fun matchIds() = + forEachDB { db -> + db.insert(note) - db.assertQuery(note, Filter(ids = listOf(id))) - db.assertQuery(note, Filter(ids = listOf(id, rootETag))) - db.assertQuery(null, Filter(ids = listOf(rootETag))) - } + db.assertQuery(note, Filter(ids = listOf(id))) + db.assertQuery(note, Filter(ids = listOf(id, rootETag))) + db.assertQuery(null, Filter(ids = listOf(rootETag))) + } @Test - fun matchPubkeys() { - db.insert(note) + fun matchPubkeys() = + forEachDB { db -> + db.insert(note) - db.assertQuery(note, Filter(authors = listOf(pubkey))) - db.assertQuery(note, Filter(authors = listOf(pubkey, rootETag))) - db.assertQuery(null, Filter(authors = listOf(rootETag))) - } + db.assertQuery(note, Filter(authors = listOf(pubkey))) + db.assertQuery(note, Filter(authors = listOf(pubkey, rootETag))) + db.assertQuery(null, Filter(authors = listOf(rootETag))) + } @Test - fun matchTags() { - db.insert(note) + fun matchTags() = + forEachDB { db -> + db.insert(note) - db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1)))) - db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1, pTag2, pTag3)))) - db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1, id)))) - db.assertQuery(null, Filter(tags = mapOf("p" to listOf(id)))) - } + db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1)))) + db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1, pTag2, pTag3)))) + db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1, id)))) + db.assertQuery(null, Filter(tags = mapOf("p" to listOf(id)))) + } @Test - fun matchDualTags() { - db.insert(note) + fun matchDualTags() = + forEachDB { db -> + db.insert(note) - db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1), "e" to listOf(rootETag)))) - db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1, pTag2, pTag3), "e" to listOf(rootETag, replyETag)))) - db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1, id), "e" to listOf(rootETag, replyETag)))) - db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1, id), "e" to listOf(rootETag, pubkey)))) - db.assertQuery(null, Filter(tags = mapOf("p" to listOf(pTag1, id), "e" to listOf(id, pubkey)))) - db.assertQuery(null, Filter(tags = mapOf("p" to listOf(id), "e" to listOf(rootETag)))) - } + db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1), "e" to listOf(rootETag)))) + db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1, pTag2, pTag3), "e" to listOf(rootETag, replyETag)))) + db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1, id), "e" to listOf(rootETag, replyETag)))) + db.assertQuery(note, Filter(tags = mapOf("p" to listOf(pTag1, id), "e" to listOf(rootETag, pubkey)))) + db.assertQuery(null, Filter(tags = mapOf("p" to listOf(pTag1, id), "e" to listOf(id, pubkey)))) + db.assertQuery(null, Filter(tags = mapOf("p" to listOf(id), "e" to listOf(rootETag)))) + } @Test - fun matchAllTags() { - db.insert(note) + fun matchAllTags() = + forEachDB { db -> + db.insert(note) - db.assertQuery(note, Filter(tagsAll = mapOf("p" to listOf(pTag1)))) - db.assertQuery(note, Filter(tagsAll = mapOf("p" to listOf(pTag1, pTag2, pTag3)))) - db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(pTag1, id)))) - db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(id)))) - } + db.assertQuery(note, Filter(tagsAll = mapOf("p" to listOf(pTag1)))) + db.assertQuery(note, Filter(tagsAll = mapOf("p" to listOf(pTag1, pTag2, pTag3)))) + db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(pTag1, id)))) + db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(id)))) + } @Test - fun matchDualAllTags() { - db.insert(note) + fun matchDualAllTags() = + forEachDB { db -> + db.insert(note) - db.assertQuery(note, Filter(tagsAll = mapOf("p" to listOf(pTag1), "e" to listOf(rootETag)))) - db.assertQuery(note, Filter(tagsAll = mapOf("p" to listOf(pTag1, pTag2, pTag3), "e" to listOf(rootETag, replyETag)))) - db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(pTag1, id), "e" to listOf(rootETag, replyETag)))) - db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(pTag1, id), "e" to listOf(rootETag, pubkey)))) - db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(pTag1, id), "e" to listOf(id, pubkey)))) - db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(id), "e" to listOf(rootETag)))) - } + db.assertQuery(note, Filter(tagsAll = mapOf("p" to listOf(pTag1), "e" to listOf(rootETag)))) + db.assertQuery(note, Filter(tagsAll = mapOf("p" to listOf(pTag1, pTag2, pTag3), "e" to listOf(rootETag, replyETag)))) + db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(pTag1, id), "e" to listOf(rootETag, replyETag)))) + db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(pTag1, id), "e" to listOf(rootETag, pubkey)))) + db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(pTag1, id), "e" to listOf(id, pubkey)))) + db.assertQuery(null, Filter(tagsAll = mapOf("p" to listOf(id), "e" to listOf(rootETag)))) + } @Test - fun matchKinds() { - db.insert(note) + fun matchKinds() = + forEachDB { db -> + db.insert(note) - db.assertQuery(note, Filter(kinds = listOf(kind))) - db.assertQuery(note, Filter(kinds = listOf(kind, 1221))) - db.assertQuery(null, Filter(kinds = listOf(1221))) - } + db.assertQuery(note, Filter(kinds = listOf(kind))) + db.assertQuery(note, Filter(kinds = listOf(kind, 1221))) + db.assertQuery(null, Filter(kinds = listOf(1221))) + } @Test - fun matchSince() { - db.insert(note) + fun matchSince() = + forEachDB { db -> + db.insert(note) - db.assertQuery(note, Filter(since = createdAt)) - db.assertQuery(note, Filter(since = createdAt - 1)) - db.assertQuery(null, Filter(since = createdAt + 1)) - } + db.assertQuery(note, Filter(since = createdAt)) + db.assertQuery(note, Filter(since = createdAt - 1)) + db.assertQuery(null, Filter(since = createdAt + 1)) + } @Test - fun matchUntil() { - db.insert(note) + fun matchUntil() = + forEachDB { db -> + db.insert(note) - db.assertQuery(note, Filter(until = createdAt)) - db.assertQuery(note, Filter(until = createdAt + 1)) - db.assertQuery(null, Filter(until = createdAt - 1)) - } + db.assertQuery(note, Filter(until = createdAt)) + db.assertQuery(note, Filter(until = createdAt + 1)) + db.assertQuery(null, Filter(until = createdAt - 1)) + } } diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/LargeDBTests.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/LargeDBTests.kt index a16e2909b..a62de74ee 100644 --- a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/LargeDBTests.kt +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/LargeDBTests.kt @@ -31,6 +31,7 @@ import com.vitorpamplona.quartz.nip40Expiration.isExpired import com.vitorpamplona.quartz.utils.Log import org.junit.After import org.junit.Before +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import java.util.zip.GZIPInputStream @@ -82,6 +83,7 @@ class LargeDBTests { } @Test + @Ignore("Not testing") fun insertDatabase() { events.forEach { event -> try { diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt index 05f30020c..52d4c5423 100644 --- a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.experimental.relationshipStatus.ContactCardEvent import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent @@ -38,59 +39,68 @@ import org.junit.Test class QueryAssemblerTest { val hasher = TagNameValueHasher(0) - val builder = QueryBuilder(FullTextSearchModule(), { hasher }) - val key1 = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d" val key2 = "f3ac434d61bc0f491a814782ccfdf9c439dae1f0bde9097ad4a245f4c495cd14" val key3 = "12ae0fd81c85e1e7d9ed096397dc3129849425fe6f8afce7213ebf38ddfc6ca9" - private lateinit var db: EventStore + private lateinit var dbAllIndexes: EventStore + private lateinit var dbNoKindIndexes: EventStore @Before fun setup() { val context = ApplicationProvider.getApplicationContext() - db = EventStore(context, null) + dbAllIndexes = EventStore(context, null, indexStrategy = DefaultIndexingStrategy(true)) + dbNoKindIndexes = EventStore(context, null, indexStrategy = DefaultIndexingStrategy(false)) } @After fun tearDown() { - db.close() + dbAllIndexes.close() + dbNoKindIndexes.close() } - fun explain(f: Filter) = builder.planQuery(f, hasher, db.store.readableDatabase) + fun EventStore.explain(f: Filter) = store.queryBuilder.planQuery(f, hasher, store.readableDatabase) - fun explain(f: List) = builder.planQuery(f, hasher, db.store.readableDatabase) + fun EventStore.explain(f: List) = store.queryBuilder.planQuery(f, hasher, store.readableDatabase) @Test fun testEmpty() { Assert.assertEquals( """ - SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers ORDER BY created_at DESC, id + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + ORDER BY created_at DESC └── SCAN event_headers USING INDEX query_by_created_at_id """.trimIndent(), - explain(Filter()), + dbAllIndexes.explain(Filter()), + ) + + Assert.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + ORDER BY created_at DESC + └── SCAN event_headers USING INDEX query_by_created_at_id + """.trimIndent(), + dbNoKindIndexes.explain(Filter()), ) } @Test fun testCheckDeletionEventExists() { - val query = - explain( - Filter( - kinds = listOf(5), - authors = listOf(key1), - tags = mapOf("e" to listOf(key2)), - since = 1750889190, - ), + val filter = + Filter( + kinds = listOf(5), + authors = listOf(key1), + tags = mapOf("e" to listOf(key2)), + since = 1750889190, ) TestCase.assertEquals( """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "2657743813502222172") AND (event_tags.kind = "5") AND (event_tags.pubkey_hash = "1730514094536529999") AND (event_tags.created_at >= "1750889190") + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "2657743813502222172") AND (event_tags.kind = "5") AND (event_tags.pubkey_hash = "1730514094536529999") AND (event_tags.created_at >= "1750889190") ) AS filtered ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC, id + ORDER BY created_at DESC ├── CO-ROUTINE filtered │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind_pubkey (tag_hash=? AND kind=? AND pubkey_hash=? AND created_at>?) │ └── USE TEMP B-TREE FOR DISTINCT @@ -98,44 +108,55 @@ class QueryAssemblerTest { ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) └── USE TEMP B-TREE FOR ORDER BY """.trimIndent(), - query, + dbAllIndexes.explain(filter), + ) + + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "2657743813502222172") AND (event_tags.kind = "5") AND (event_tags.pubkey_hash = "1730514094536529999") AND (event_tags.created_at >= "1750889190") + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY created_at DESC + ├── CO-ROUTINE filtered + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind_pubkey (tag_hash=? AND kind=? AND pubkey_hash=? AND created_at>?) + │ └── USE TEMP B-TREE FOR DISTINCT + ├── SCAN filtered + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + dbNoKindIndexes.explain(filter), ) } @Test fun testLimit() { - val explainer = explain(Filter(limit = 10)) + val filter = Filter(limit = 10) TestCase.assertEquals( """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - INNER JOIN ( - SELECT event_headers.row_id as row_id FROM event_headers WHERE 1 = 1 ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 10 - ) AS filtered - ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC, id - ├── CO-ROUTINE filtered - │ └── SCAN event_headers USING COVERING INDEX query_by_created_at_id - ├── SCAN filtered - ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - └── USE TEMP B-TREE FOR ORDER BY + ORDER BY created_at DESC + LIMIT 10 + └── SCAN event_headers USING INDEX query_by_created_at_id """.trimIndent(), - explainer, + dbAllIndexes.explain(filter), ) } @Test fun testLimits() { - val explainer = explain(listOf(Filter(limit = 10), Filter(limit = 30))) + val filter = listOf(Filter(limit = 10), Filter(limit = 30)) TestCase.assertEquals( """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers WHERE 1 = 1 ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 10) + SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers ORDER BY event_headers.created_at DESC LIMIT 10) UNION - SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers WHERE 1 = 1 ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 30) + SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers ORDER BY event_headers.created_at DESC LIMIT 30) ) AS filtered ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC, id + ORDER BY created_at DESC ├── CO-ROUTINE filtered │ └── COMPOUND QUERY │ ├── LEFT-MOST SUBQUERY @@ -150,37 +171,35 @@ class QueryAssemblerTest { ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) └── USE TEMP B-TREE FOR ORDER BY """.trimIndent(), - explainer, + dbAllIndexes.explain(filter), ) } @Test fun testAllFeatures() { - val sql = - explain( - listOf( - Filter(limit = 10), - Filter( - authors = listOf(key1), - kinds = listOf(1, 1111), - search = "keywords", - limit = 100, - ), - Filter(kinds = listOf(20), search = "cats", limit = 30), + val filter = + listOf( + Filter(limit = 10), + Filter( + authors = listOf(key1), + kinds = listOf(1, 1111), + search = "keywords", + limit = 100, ), + Filter(kinds = listOf(20), search = "cats", limit = 30), ) TestCase.assertEquals( """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers WHERE 1 = 1 ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 10) + SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers ORDER BY event_headers.created_at DESC LIMIT 10) UNION - SELECT row_id FROM (SELECT event_fts.event_header_row_id as row_id FROM event_fts INNER JOIN event_headers ON event_headers.row_id = event_fts.event_header_row_id WHERE (event_headers.kind IN ("1", "1111")) AND (event_headers.pubkey = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d") AND (event_fts MATCH "keywords") ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 100) + SELECT row_id FROM (SELECT event_fts.event_header_row_id as row_id FROM event_fts INNER JOIN event_headers ON event_headers.row_id = event_fts.event_header_row_id WHERE (event_headers.kind IN ("1", "1111")) AND (event_headers.pubkey = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d") AND (event_fts MATCH "keywords") ORDER BY event_headers.created_at DESC LIMIT 100) UNION - SELECT row_id FROM (SELECT event_fts.event_header_row_id as row_id FROM event_fts INNER JOIN event_headers ON event_headers.row_id = event_fts.event_header_row_id WHERE (event_headers.kind = "20") AND (event_fts MATCH "cats") ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 30) + SELECT row_id FROM (SELECT event_fts.event_header_row_id as row_id FROM event_fts INNER JOIN event_headers ON event_headers.row_id = event_fts.event_header_row_id WHERE (event_headers.kind = "20") AND (event_fts MATCH "cats") ORDER BY event_headers.created_at DESC LIMIT 30) ) AS filtered ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC, id + ORDER BY created_at DESC ├── CO-ROUTINE filtered │ └── COMPOUND QUERY │ ├── LEFT-MOST SUBQUERY @@ -195,99 +214,119 @@ class QueryAssemblerTest { │ │ └── SCAN (subquery-3) │ └── UNION USING TEMP B-TREE │ ├── CO-ROUTINE (subquery-5) - │ │ ├── SCAN event_fts VIRTUAL TABLE INDEX 4: - │ │ ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - │ │ └── USE TEMP B-TREE FOR ORDER BY + │ │ ├── SEARCH event_headers USING COVERING INDEX query_by_kind_created (kind=?) + │ │ └── SCAN event_fts VIRTUAL TABLE INDEX 4: │ └── SCAN (subquery-5) ├── SCAN filtered ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) └── USE TEMP B-TREE FOR ORDER BY """.trimIndent(), - sql, + dbAllIndexes.explain(filter), ) } @Test - fun testKind() { - val sql = - explain( - listOf( - Filter( - kinds = listOf(ContactListEvent.KIND), - limit = 30, - ), + fun testSingleFilterConversionToSimpleQuery() { + val filter = + listOf( + Filter( + kinds = listOf(ContactListEvent.KIND), + limit = 30, + ), + ) + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + WHERE kind = "3" + ORDER BY created_at DESC + LIMIT 30 + └── SEARCH event_headers USING INDEX query_by_kind_created (kind=?) + """.trimIndent(), + dbAllIndexes.explain(filter), + ) + } + + @Test + fun testKinds() { + val filter = + listOf( + Filter( + kinds = listOf(ContactListEvent.KIND), + limit = 30, + ), + Filter( + kinds = listOf(TextNoteEvent.KIND), + limit = 30, ), ) TestCase.assertEquals( """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT event_headers.row_id as row_id FROM event_headers WHERE event_headers.kind = "3" ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 30 + SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers WHERE event_headers.kind = "3" ORDER BY event_headers.created_at DESC LIMIT 30) + UNION + SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers WHERE event_headers.kind = "1" ORDER BY event_headers.created_at DESC LIMIT 30) ) AS filtered ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC, id + ORDER BY created_at DESC ├── CO-ROUTINE filtered - │ ├── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=?) - │ └── USE TEMP B-TREE FOR ORDER BY + │ └── COMPOUND QUERY + │ ├── LEFT-MOST SUBQUERY + │ │ ├── CO-ROUTINE (subquery-1) + │ │ │ └── SEARCH event_headers USING COVERING INDEX query_by_kind_created (kind=?) + │ │ └── SCAN (subquery-1) + │ └── UNION USING TEMP B-TREE + │ ├── CO-ROUTINE (subquery-3) + │ │ └── SEARCH event_headers USING COVERING INDEX query_by_kind_created (kind=?) + │ └── SCAN (subquery-3) ├── SCAN filtered ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) └── USE TEMP B-TREE FOR ORDER BY """.trimIndent(), - sql, + dbAllIndexes.explain(filter), ) } @Test fun testKindAndDTag() { - val sql = - explain( - listOf( - Filter( - kinds = listOf(ContactListEvent.KIND), - tags = mapOf("d" to listOf("")), - limit = 30, - ), + val filter = + listOf( + Filter( + kinds = listOf(ContactListEvent.KIND), + tags = mapOf("d" to listOf("")), + limit = 30, ), ) TestCase.assertEquals( """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - INNER JOIN ( - SELECT event_headers.row_id as row_id FROM event_headers WHERE (event_headers.kind = "3") AND (event_headers.d_tag = "") ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 30 - ) AS filtered - ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC, id - ├── CO-ROUTINE filtered - │ ├── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=?) - │ └── USE TEMP B-TREE FOR ORDER BY - ├── SCAN filtered - ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - └── USE TEMP B-TREE FOR ORDER BY + WHERE (kind = "3") AND (d_tag = "") + ORDER BY created_at DESC + LIMIT 30 + └── SEARCH event_headers USING INDEX query_by_kind_created (kind=?) """.trimIndent(), - sql, + dbAllIndexes.explain(filter), ) } @Test fun testFollowersOf() { - val sql = - explain( - listOf( - Filter( - kinds = listOf(ContactListEvent.KIND), - tags = mapOf("p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")), - limit = 30, - ), + val filter = + listOf( + Filter( + kinds = listOf(ContactListEvent.KIND), + tags = mapOf("p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")), + limit = 30, ), ) TestCase.assertEquals( """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") ORDER BY event_tags.created_at DESC LIMIT 30 + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") ORDER BY event_tags.created_at DESC LIMIT 30 ) AS filtered ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC, id + ORDER BY created_at DESC ├── CO-ROUTINE filtered │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=? AND kind=?) │ └── USE TEMP B-TREE FOR DISTINCT @@ -295,29 +334,27 @@ class QueryAssemblerTest { ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) └── USE TEMP B-TREE FOR ORDER BY """.trimIndent(), - sql, + dbAllIndexes.explain(filter), ) } @Test fun testNotificationsOf() { - val sql = - explain( - listOf( - Filter( - tags = mapOf("p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")), - limit = 30, - ), + val filter = + listOf( + Filter( + tags = mapOf("p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")), + limit = 30, ), ) TestCase.assertEquals( """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE event_tags.tag_hash = "-4551135004136952885" ORDER BY event_tags.created_at DESC LIMIT 30 + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE event_tags.tag_hash = "-4551135004136952885" ORDER BY event_tags.created_at DESC LIMIT 30 ) AS filtered ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC, id + ORDER BY created_at DESC ├── CO-ROUTINE filtered │ ├── SEARCH event_tags USING INDEX query_by_tags_hash (tag_hash=?) │ └── USE TEMP B-TREE FOR DISTINCT @@ -325,30 +362,28 @@ class QueryAssemblerTest { ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) └── USE TEMP B-TREE FOR ORDER BY """.trimIndent(), - sql, + dbAllIndexes.explain(filter), ) } @Test fun testTagsAndKinds() { - val sql = - explain( - listOf( - Filter( - kinds = listOf(ContactListEvent.KIND), - tags = mapOf("p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")), - limit = 30, - ), + val filter = + listOf( + Filter( + kinds = listOf(ContactListEvent.KIND), + tags = mapOf("p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")), + limit = 30, ), ) TestCase.assertEquals( """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") ORDER BY event_tags.created_at DESC LIMIT 30 + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") ORDER BY event_tags.created_at DESC LIMIT 30 ) AS filtered ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC, id + ORDER BY created_at DESC ├── CO-ROUTINE filtered │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=? AND kind=?) │ └── USE TEMP B-TREE FOR DISTINCT @@ -356,30 +391,28 @@ class QueryAssemblerTest { ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) └── USE TEMP B-TREE FOR ORDER BY """.trimIndent(), - sql, + dbAllIndexes.explain(filter), ) } @Test fun testTagsAndAuthors() { - val sql = - explain( - listOf( - Filter( - authors = listOf(key1), - tags = mapOf("p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")), - limit = 30, - ), + val filter = + listOf( + Filter( + authors = listOf(key1), + tags = mapOf("p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")), + limit = 30, ), ) TestCase.assertEquals( """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.pubkey_hash = "1730514094536529999") ORDER BY event_tags.created_at DESC LIMIT 30 + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.pubkey_hash = "1730514094536529999") ORDER BY event_tags.created_at DESC LIMIT 30 ) AS filtered ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC, id + ORDER BY created_at DESC ├── CO-ROUTINE filtered │ ├── SEARCH event_tags USING INDEX query_by_tags_hash (tag_hash=?) │ └── USE TEMP B-TREE FOR DISTINCT @@ -387,34 +420,32 @@ class QueryAssemblerTest { ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) └── USE TEMP B-TREE FOR ORDER BY """.trimIndent(), - sql, + dbAllIndexes.explain(filter), ) } @Test fun testTwoTags() { - val sql = - explain( - listOf( - Filter( - kinds = listOf(1), - tags = - mapOf( - "p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), - "t" to listOf("hashtag"), - ), - limit = 30, - ), + val filter = + listOf( + Filter( + kinds = listOf(1), + tags = + mapOf( + "p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + "t" to listOf("hashtag"), + ), + limit = 30, ), ) TestCase.assertEquals( """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_tags as event_tagsIn1 ON event_tagsIn1.event_header_row_id = event_tags.event_header_row_id AND event_tagsIn1.created_at = event_tags.created_at WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tagsIn1.tag_hash = "-6379614208644810021") AND (event_tags.kind = "1") ORDER BY event_tags.created_at DESC LIMIT 30 + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_tags as event_tagsIn1 ON event_tagsIn1.event_header_row_id = event_tags.event_header_row_id AND event_tagsIn1.created_at = event_tags.created_at WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tagsIn1.tag_hash = "-6379614208644810021") AND (event_tags.kind = "1") ORDER BY event_tags.created_at DESC LIMIT 30 ) AS filtered ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC, id + ORDER BY created_at DESC ├── CO-ROUTINE filtered │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=? AND kind=?) │ ├── SEARCH event_tagsIn1 USING INDEX query_by_tags_hash (tag_hash=? AND created_at=?) @@ -423,101 +454,87 @@ class QueryAssemblerTest { ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) └── USE TEMP B-TREE FOR ORDER BY """.trimIndent(), - sql, + dbAllIndexes.explain(filter), ) } @Test fun testIdQuery() { - val sql = explain(Filter(ids = listOf(key1))) + val filter = Filter(ids = listOf(key1)) TestCase.assertEquals( """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - INNER JOIN ( - SELECT event_headers.row_id as row_id FROM event_headers WHERE event_headers.id = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d" - ) AS filtered - ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC, id - ├── SEARCH event_headers USING COVERING INDEX event_headers_id (id=?) - └── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + WHERE id = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d" + ORDER BY created_at DESC + └── SEARCH event_headers USING INDEX event_headers_id (id=?) """.trimIndent(), - sql, + dbAllIndexes.explain(filter), ) } @Test fun testAuthors() { - val sql = explain(Filter(authors = listOf(key1, key2), kinds = listOf(1, 30023), limit = 300)) + val filter = Filter(authors = listOf(key1, key2), kinds = listOf(1, 30023), limit = 300) TestCase.assertEquals( """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - INNER JOIN ( - SELECT event_headers.row_id as row_id FROM event_headers WHERE (event_headers.kind IN ("1", "30023")) AND (event_headers.pubkey IN ("7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d", "f3ac434d61bc0f491a814782ccfdf9c439dae1f0bde9097ad4a245f4c495cd14")) ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 300 - ) AS filtered - ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC, id - ├── CO-ROUTINE filtered - │ ├── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=?) - │ └── USE TEMP B-TREE FOR ORDER BY - ├── SCAN filtered - ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + WHERE (kind IN ("1", "30023")) AND (pubkey IN ("7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d", "f3ac434d61bc0f491a814782ccfdf9c439dae1f0bde9097ad4a245f4c495cd14")) + ORDER BY created_at DESC + LIMIT 300 + ├── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=?) └── USE TEMP B-TREE FOR ORDER BY """.trimIndent(), - sql, + dbAllIndexes.explain(filter), ) } @Test fun testAuthorsAndSearch() { - val sql = explain(Filter(authors = listOf(key1, key2, key3), search = "keywords")) + val filter = Filter(authors = listOf(key1, key2, key3), search = "keywords") + println(dbAllIndexes.explain(filter)) TestCase.assertEquals( """ - SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - INNER JOIN ( - SELECT event_fts.event_header_row_id as row_id FROM event_fts INNER JOIN event_headers ON event_headers.row_id = event_fts.event_header_row_id WHERE (event_headers.pubkey IN ("7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d", "f3ac434d61bc0f491a814782ccfdf9c439dae1f0bde9097ad4a245f4c495cd14", "12ae0fd81c85e1e7d9ed096397dc3129849425fe6f8afce7213ebf38ddfc6ca9")) AND (event_fts MATCH "keywords") - ) AS filtered - ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC, id + SELECT event_headers.id, event_headers.pubkey, event_headers.created_at, event_headers.kind, event_headers.tags, event_headers.content, event_headers.sig FROM event_headers + INNER JOIN event_fts ON event_headers.row_id = event_fts.event_header_row_id + WHERE (event_fts MATCH "keywords") AND (event_headers.pubkey IN ("7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d", "f3ac434d61bc0f491a814782ccfdf9c439dae1f0bde9097ad4a245f4c495cd14", "12ae0fd81c85e1e7d9ed096397dc3129849425fe6f8afce7213ebf38ddfc6ca9")) + ORDER BY event_headers.created_at DESC ├── SCAN event_fts VIRTUAL TABLE INDEX 4: ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) └── USE TEMP B-TREE FOR ORDER BY """.trimIndent(), - sql, + dbAllIndexes.explain(filter), ) } @Test fun testKindAndSearch() { - val sql = explain(Filter(kinds = listOf(1, 1111, 10000), search = "keywords")) + val filter = Filter(kinds = listOf(1, 1111, 10000), search = "keywords") + println(dbAllIndexes.explain(filter)) TestCase.assertEquals( """ - SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - INNER JOIN ( - SELECT event_fts.event_header_row_id as row_id FROM event_fts INNER JOIN event_headers ON event_headers.row_id = event_fts.event_header_row_id WHERE (event_headers.kind IN ("1", "1111", "10000")) AND (event_fts MATCH "keywords") - ) AS filtered - ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC, id + SELECT event_headers.id, event_headers.pubkey, event_headers.created_at, event_headers.kind, event_headers.tags, event_headers.content, event_headers.sig FROM event_headers + INNER JOIN event_fts ON event_headers.row_id = event_fts.event_header_row_id + WHERE (event_fts MATCH "keywords") AND (event_headers.kind IN ("1", "1111", "10000")) + ORDER BY event_headers.created_at DESC ├── SCAN event_fts VIRTUAL TABLE INDEX 4: ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) └── USE TEMP B-TREE FOR ORDER BY """.trimIndent(), - sql, + dbAllIndexes.explain(filter), ) } @Test fun testAllTag() { - val sql = explain(Filter(tagsAll = mapOf("p" to listOf(key1, key2)))) + val filter = Filter(tagsAll = mapOf("p" to listOf(key1, key2))) TestCase.assertEquals( """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_tags as event_tagsAll0_1 ON event_tagsAll0_1.event_header_row_id = event_tags.event_header_row_id AND event_tagsAll0_1.created_at = event_tags.created_at WHERE (event_tags.tag_hash = "884286737453847614") AND (event_tagsAll0_1.tag_hash = "-4988851810256311323") + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_tags as event_tagsAll0_1 ON event_tagsAll0_1.event_header_row_id = event_tags.event_header_row_id AND event_tagsAll0_1.created_at = event_tags.created_at WHERE (event_tags.tag_hash = "884286737453847614") AND (event_tagsAll0_1.tag_hash = "-4988851810256311323") ) AS filtered ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC, id + ORDER BY created_at DESC ├── CO-ROUTINE filtered │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=?) │ ├── SEARCH event_tagsAll0_1 USING INDEX query_by_tags_hash (tag_hash=? AND created_at=?) @@ -526,32 +543,30 @@ class QueryAssemblerTest { ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) └── USE TEMP B-TREE FOR ORDER BY """.trimIndent(), - sql, + dbAllIndexes.explain(filter), ) } @Test fun testReportLikeFilter() { - val sql = - explain( - Filter( - kinds = listOf(ContactListEvent.KIND), - authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), - tags = - mapOf( - "p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), - ), - limit = 500, - ), + val filter = + Filter( + kinds = listOf(ContactListEvent.KIND), + authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + tags = + mapOf( + "p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + ), + limit = 500, ) assertEquals( """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") AND (event_tags.pubkey_hash = "5446767199141196776") ORDER BY event_tags.created_at DESC LIMIT 500 + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") AND (event_tags.pubkey_hash = "5446767199141196776") ORDER BY event_tags.created_at DESC LIMIT 500 ) AS filtered ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC, id + ORDER BY created_at DESC ├── CO-ROUTINE filtered │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind_pubkey (tag_hash=? AND kind=? AND pubkey_hash=?) │ └── USE TEMP B-TREE FOR DISTINCT @@ -559,32 +574,29 @@ class QueryAssemblerTest { ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) └── USE TEMP B-TREE FOR ORDER BY """.trimIndent(), - sql, + dbAllIndexes.explain(filter), ) } @Test fun testFollowersSinceNov2025() { - val sql = - explain( - Filter( - kinds = listOf(ContactListEvent.KIND), - tags = - mapOf( - "p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), - ), - since = 1764553447, // Nov 2025 - ), + val filter = + Filter( + kinds = listOf(ContactListEvent.KIND), + tags = + mapOf( + "p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + ), + since = 1764553447, // Nov 2025 ) - println(sql) assertEquals( """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") AND (event_tags.created_at >= "1764553447") + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") AND (event_tags.created_at >= "1764553447") ) AS filtered ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC, id + ORDER BY created_at DESC ├── CO-ROUTINE filtered │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=? AND kind=? AND created_at>?) │ └── USE TEMP B-TREE FOR DISTINCT @@ -592,119 +604,93 @@ class QueryAssemblerTest { ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) └── USE TEMP B-TREE FOR ORDER BY """.trimIndent(), - sql, + dbAllIndexes.explain(filter), ) } @Test fun testAllAddressablesOfAKindDownload() { - val sql = - explain( - Filter( - kinds = listOf(DraftWrapEvent.KIND), - authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), - since = 1764553447, // Nov 2025 - ), + val filter = + Filter( + kinds = listOf(DraftWrapEvent.KIND), + authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + since = 1764553447, // Nov 2025 ) - println(sql) assertEquals( """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - INNER JOIN ( - SELECT event_headers.row_id as row_id FROM event_headers WHERE (event_headers.kind = "31234") AND (event_headers.pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (event_headers.created_at >= "1764553447") AND ((event_headers.kind >= 30000 AND event_headers.kind < 40000)) - ) AS filtered - ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC, id - ├── SEARCH event_headers USING COVERING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=? AND created_at>?) - ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - └── USE TEMP B-TREE FOR ORDER BY + WHERE (kind = "31234") AND (pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (created_at >= "1764553447") + ORDER BY created_at DESC + └── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=? AND created_at>?) """.trimIndent(), - sql, + dbAllIndexes.explain(filter), ) } @Test fun testReplaceablesOfMultipleKindsDownloadByDateLimit() { - val sql = - explain( - Filter( - kinds = listOf(MetadataEvent.KIND, SearchRelayListEvent.KIND, AdvertisedRelayListEvent.KIND), - authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), - limit = 20, - since = 1764553447, // Nov 2025 - ), + val filter = + Filter( + kinds = listOf(MetadataEvent.KIND, SearchRelayListEvent.KIND, AdvertisedRelayListEvent.KIND), + authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + limit = 20, + since = 1764553447, // Nov 2025 ) assertEquals( """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - INNER JOIN ( - SELECT event_headers.row_id as row_id FROM event_headers WHERE (event_headers.kind IN ("0", "10007", "10002")) AND (event_headers.pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (event_headers.created_at >= "1764553447") ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT 20 - ) AS filtered - ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC, id - ├── CO-ROUTINE filtered - │ ├── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=? AND created_at>?) - │ └── USE TEMP B-TREE FOR ORDER BY - ├── SCAN filtered - ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + WHERE (kind IN ("0", "10007", "10002")) AND (pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (created_at >= "1764553447") + ORDER BY created_at DESC + LIMIT 20 + ├── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=? AND created_at>?) └── USE TEMP B-TREE FOR ORDER BY """.trimIndent(), - sql, + dbAllIndexes.explain(filter), ) } @Test fun testReplaceablesOfMultipleKindsDownload() { - val sql = - explain( - Filter( - kinds = listOf(MetadataEvent.KIND, SearchRelayListEvent.KIND, AdvertisedRelayListEvent.KIND), - authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), - ), + val filter = + Filter( + kinds = listOf(MetadataEvent.KIND, SearchRelayListEvent.KIND, AdvertisedRelayListEvent.KIND), + authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), ) + assertEquals( """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - INNER JOIN ( - SELECT event_headers.row_id as row_id FROM event_headers WHERE (event_headers.kind IN ("0", "10007", "10002")) AND (event_headers.pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") - ) AS filtered - ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC, id - ├── SEARCH event_headers USING COVERING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=?) - ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + WHERE (kind IN ("0", "10007", "10002")) AND (pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") + ORDER BY created_at DESC + ├── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=?) └── USE TEMP B-TREE FOR ORDER BY """.trimIndent(), - sql, + dbAllIndexes.explain(filter), ) } @Test fun testContactCardDownloadFromTrustedKeys() { - val sql = - explain( - Filter( - kinds = listOf(ContactCardEvent.KIND), - authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), - tags = - mapOf( - "d" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), - ), - since = 1764553447, // Nov 2025 - ), + val filter = + Filter( + kinds = listOf(ContactCardEvent.KIND), + authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + tags = + mapOf( + "d" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + ), + since = 1764553447, // Nov 2025 ) + assertEquals( """ SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - INNER JOIN ( - SELECT event_headers.row_id as row_id FROM event_headers WHERE (event_headers.kind = "30382") AND (event_headers.pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (event_headers.d_tag = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (event_headers.created_at >= "1764553447") AND ((event_headers.kind >= 30000 AND event_headers.kind < 40000)) - ) AS filtered - ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC, id + WHERE (kind = "30382") AND (pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (d_tag = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (created_at >= "1764553447") AND ((kind >= 30000 AND kind < 40000)) + ORDER BY created_at DESC ├── SEARCH event_headers USING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?) - ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) └── USE TEMP B-TREE FOR ORDER BY """.trimIndent(), - sql, + dbAllIndexes.explain(filter), ) } } diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/ReplaceableTest.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/ReplaceableTest.kt index 182414ef2..620b5301f 100644 --- a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/ReplaceableTest.kt +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/ReplaceableTest.kt @@ -20,9 +20,7 @@ */ package com.vitorpamplona.quartz.nip01Core.store.sqlite -import android.content.Context import android.database.sqlite.SQLiteConstraintException -import androidx.test.core.app.ApplicationProvider import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync @@ -30,136 +28,125 @@ import com.vitorpamplona.quartz.utils.TimeUtils import junit.framework.TestCase import junit.framework.TestCase.assertEquals import junit.framework.TestCase.fail -import org.junit.After -import org.junit.Before import org.junit.Test -class ReplaceableTest { - private lateinit var db: EventStore - +class ReplaceableTest : BaseDBTest() { val signer = NostrSignerSync() - @Before - fun setup() { - val context = ApplicationProvider.getApplicationContext() - db = EventStore(context, null) - } - - @After - fun tearDown() { - db.close() - } - @Test - fun testReplacing() { - val time = TimeUtils.now() - val version1 = signer.sign(MetadataEvent.createNew("Vitor 1", createdAt = time)) - val version2 = signer.sign(MetadataEvent.createNew("Vitor 2", createdAt = time + 1)) - val version3 = signer.sign(MetadataEvent.createNew("Vitor 3", createdAt = time + 2)) + fun testReplacing() = + forEachDB { db -> + val time = TimeUtils.now() + val version1 = signer.sign(MetadataEvent.createNew("Vitor 1", createdAt = time)) + val version2 = signer.sign(MetadataEvent.createNew("Vitor 2", createdAt = time + 1)) + val version3 = signer.sign(MetadataEvent.createNew("Vitor 3", createdAt = time + 2)) - val addressableQuery = Filter(kinds = listOf(version1.kind), authors = listOf(version1.pubKey), tags = mapOf("d" to listOf(version1.dTag()))) + val addressableQuery = Filter(kinds = listOf(version1.kind), authors = listOf(version1.pubKey), tags = mapOf("d" to listOf(version1.dTag()))) - db.insert(version1) - - db.assertQuery(version1, Filter(ids = listOf(version1.id))) - db.assertQuery(version1, addressableQuery) - - db.insert(version2) - - db.assertQuery(null, Filter(ids = listOf(version1.id))) - db.assertQuery(version2, Filter(ids = listOf(version2.id))) - db.assertQuery(version2, addressableQuery) - - db.insert(version3) - - db.assertQuery(null, Filter(ids = listOf(version1.id))) - db.assertQuery(null, Filter(ids = listOf(version2.id))) - db.assertQuery(version3, Filter(ids = listOf(version3.id))) - db.assertQuery(version3, addressableQuery) - } - - @Test - fun testBlockingOldVersions() { - val time = TimeUtils.now() - val version1 = signer.sign(MetadataEvent.createNew("Vitor 1", createdAt = time)) - val version2 = signer.sign(MetadataEvent.createNew("Vitor 2", createdAt = time + 1)) - val version3 = signer.sign(MetadataEvent.createNew("Vitor 3", createdAt = time + 2)) - - val addressableQuery = Filter(kinds = listOf(version1.kind), authors = listOf(version1.pubKey), tags = mapOf("d" to listOf(version1.dTag()))) - - db.insert(version3) - - db.assertQuery(version3, Filter(ids = listOf(version3.id))) - - try { - db.insert(version2) - fail("It should not allow inserting an older version") - } catch (e: Exception) { - TestCase.assertTrue(e is SQLiteConstraintException) - } - - try { db.insert(version1) - fail("It should not allow inserting an older version") - } catch (e: Exception) { - TestCase.assertTrue(e is SQLiteConstraintException) + + db.assertQuery(version1, Filter(ids = listOf(version1.id))) + db.assertQuery(version1, addressableQuery) + + db.insert(version2) + + db.assertQuery(null, Filter(ids = listOf(version1.id))) + db.assertQuery(version2, Filter(ids = listOf(version2.id))) + db.assertQuery(version2, addressableQuery) + + db.insert(version3) + + db.assertQuery(null, Filter(ids = listOf(version1.id))) + db.assertQuery(null, Filter(ids = listOf(version2.id))) + db.assertQuery(version3, Filter(ids = listOf(version3.id))) + db.assertQuery(version3, addressableQuery) } - db.assertQuery(version3, Filter(ids = listOf(version3.id))) - db.assertQuery(version3, addressableQuery) - db.assertQuery(null, Filter(ids = listOf(version2.id))) - db.assertQuery(null, Filter(ids = listOf(version1.id))) - } + @Test + fun testBlockingOldVersions() = + forEachDB { db -> + val time = TimeUtils.now() + val version1 = signer.sign(MetadataEvent.createNew("Vitor 1", createdAt = time)) + val version2 = signer.sign(MetadataEvent.createNew("Vitor 2", createdAt = time + 1)) + val version3 = signer.sign(MetadataEvent.createNew("Vitor 3", createdAt = time + 2)) + + val addressableQuery = Filter(kinds = listOf(version1.kind), authors = listOf(version1.pubKey), tags = mapOf("d" to listOf(version1.dTag()))) + + db.insert(version3) + + db.assertQuery(version3, Filter(ids = listOf(version3.id))) + + try { + db.insert(version2) + fail("It should not allow inserting an older version") + } catch (e: Exception) { + TestCase.assertTrue(e is SQLiteConstraintException) + } + + try { + db.insert(version1) + fail("It should not allow inserting an older version") + } catch (e: Exception) { + TestCase.assertTrue(e is SQLiteConstraintException) + } + + db.assertQuery(version3, Filter(ids = listOf(version3.id))) + db.assertQuery(version3, addressableQuery) + db.assertQuery(null, Filter(ids = listOf(version2.id))) + db.assertQuery(null, Filter(ids = listOf(version1.id))) + } @Test - fun testTriggersIndexUsageKind0() { - val sql = - """ - SELECT * FROM event_headers - WHERE - event_headers.kind = 0 AND - event_headers.pubkey = 'aa' AND - event_headers.created_at < 1766686500 - """.trimIndent() + fun testTriggersIndexUsageKind0() = + forEachDB { db -> + val sql = + """ + SELECT * FROM event_headers + WHERE + event_headers.kind = 0 AND + event_headers.pubkey = 'aa' AND + event_headers.created_at < 1766686500 + """.trimIndent() - val explainer = db.store.explainQuery(sql) + val explainer = db.store.explainQuery(sql) - assertEquals( - """ - SELECT * FROM event_headers - WHERE - event_headers.kind = 0 AND - event_headers.pubkey = 'aa' AND - event_headers.created_at < 1766686500 - └── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=? AND created_at + val sql = + """ + SELECT * FROM event_headers + WHERE + event_headers.kind = 3 AND + event_headers.pubkey = 'aa' AND + event_headers.created_at < 1766686500 + """.trimIndent() - val explainer = db.store.explainQuery(sql) + val explainer = db.store.explainQuery(sql) - assertEquals( - """ - SELECT * FROM event_headers - WHERE - event_headers.kind = 3 AND - event_headers.pubkey = 'aa' AND - event_headers.created_at < 1766686500 - └── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=? AND created_at() - db = EventStore(context, null, relayUrl = "testUrl") - } - - @After - fun tearDown() { - db.close() - } - @Test - fun testInsertDeleteEvent() { - val time = TimeUtils.now() - val note1 = signer.sign(TextNoteEvent.build("test1", createdAt = time)) - val note2 = signer.sign(TextNoteEvent.build("test2", createdAt = time + 1)) - val note3 = signer.sign(TextNoteEvent.build("test3", createdAt = time + 2)) + fun testInsertDeleteEvent() = + forEachDB { db -> + val time = TimeUtils.now() + val note1 = signer.sign(TextNoteEvent.build("test1", createdAt = time)) + val note2 = signer.sign(TextNoteEvent.build("test2", createdAt = time + 1)) + val note3 = signer.sign(TextNoteEvent.build("test3", createdAt = time + 2)) - db.insert(note1) - db.insert(note2) - db.insert(note3) - - db.assertQuery(note1, Filter(ids = listOf(note1.id))) - db.assertQuery(note2, Filter(ids = listOf(note2.id))) - db.assertQuery(note3, Filter(ids = listOf(note3.id))) - - val vanish = signer.sign(RequestToVanishEvent.build("testUrl", createdAt = time + 2)) - - db.insert(vanish) - - db.assertQuery(vanish, Filter(ids = listOf(vanish.id))) - db.assertQuery(null, Filter(ids = listOf(note1.id))) - db.assertQuery(null, Filter(ids = listOf(note2.id))) - db.assertQuery(note3, Filter(ids = listOf(note3.id))) - - // trying to insert again should fail. - try { db.insert(note1) - fail("Should not be able to insert a deleted event") - } catch (e: SQLiteConstraintException) { - assertEquals("blocked: a request to vanish event exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message) - } + db.insert(note2) + db.insert(note3) - db.assertQuery(vanish, Filter(ids = listOf(vanish.id))) - db.assertQuery(null, Filter(ids = listOf(note1.id))) - db.assertQuery(null, Filter(ids = listOf(note2.id))) - db.assertQuery(note3, Filter(ids = listOf(note3.id))) - } + db.assertQuery(note1, Filter(ids = listOf(note1.id))) + db.assertQuery(note2, Filter(ids = listOf(note2.id))) + db.assertQuery(note3, Filter(ids = listOf(note3.id))) + + val vanish = signer.sign(RequestToVanishEvent.build("wss://quartz.local", createdAt = time + 2)) + + db.insert(vanish) + + db.assertQuery(vanish, Filter(ids = listOf(vanish.id))) + db.assertQuery(null, Filter(ids = listOf(note1.id))) + db.assertQuery(null, Filter(ids = listOf(note2.id))) + db.assertQuery(note3, Filter(ids = listOf(note3.id))) + + // trying to insert again should fail. + try { + db.insert(note1) + fail("Should not be able to insert a deleted event") + } catch (e: SQLiteConstraintException) { + assertEquals("blocked: a request to vanish event exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message) + } + + db.assertQuery(vanish, Filter(ids = listOf(vanish.id))) + db.assertQuery(null, Filter(ids = listOf(note1.id))) + db.assertQuery(null, Filter(ids = listOf(note2.id))) + db.assertQuery(note3, Filter(ids = listOf(note3.id))) + } @Test - fun testInsertDeleteGiftWrap() { - val time = TimeUtils.now() + fun testInsertDeleteGiftWrap() = + forEachDB { db -> + val time = TimeUtils.now() - val me = NostrSignerSync() - val myFriend = NostrSignerSync() + val me = NostrSignerSync() + val myFriend = NostrSignerSync() - val note1 = me.sign(TextNoteEvent.build("test1", createdAt = time)) - val wrap1 = GiftWrapEvent.create(note1, me.pubKey) - val wrap2 = GiftWrapEvent.create(note1, myFriend.pubKey) + val note1 = me.sign(TextNoteEvent.build("test1", createdAt = time)) + val wrap1 = GiftWrapEvent.create(note1, me.pubKey) + val wrap2 = GiftWrapEvent.create(note1, myFriend.pubKey) - db.insert(wrap1) - db.insert(wrap2) - - db.assertQuery(wrap1, Filter(ids = listOf(wrap1.id))) - db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id))) - - val randomVanishToWrap = signer.sign(RequestToVanishEvent.build("testUrl", createdAt = time + 2)) - - db.insert(randomVanishToWrap) - - db.assertQuery(wrap1, Filter(ids = listOf(wrap1.id))) - db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id))) - - val vanish = me.sign(RequestToVanishEvent.build("testUrl", createdAt = time + 2)) - - db.insert(vanish) - - db.assertQuery(vanish, Filter(ids = listOf(vanish.id))) - db.assertQuery(null, Filter(ids = listOf(wrap1.id))) - db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id))) - - // trying to insert again should fail. - try { db.insert(wrap1) - fail("Should not be able to insert a deleted event") - } catch (e: SQLiteConstraintException) { - assertEquals("blocked: a request to vanish event exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message) - } + db.insert(wrap2) - db.assertQuery(vanish, Filter(ids = listOf(vanish.id))) - db.assertQuery(null, Filter(ids = listOf(wrap1.id))) - db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id))) - } + db.assertQuery(wrap1, Filter(ids = listOf(wrap1.id))) + db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id))) + + val randomVanishToWrap = signer.sign(RequestToVanishEvent.build("wss://quartz.local", createdAt = time + 2)) + + db.insert(randomVanishToWrap) + + db.assertQuery(wrap1, Filter(ids = listOf(wrap1.id))) + db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id))) + + val vanish = me.sign(RequestToVanishEvent.build("wss://quartz.local", createdAt = time + 2)) + + db.insert(vanish) + + db.assertQuery(vanish, Filter(ids = listOf(vanish.id))) + db.assertQuery(null, Filter(ids = listOf(wrap1.id))) + db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id))) + + // trying to insert again should fail. + try { + db.insert(wrap1) + fail("Should not be able to insert a deleted event") + } catch (e: SQLiteConstraintException) { + assertEquals("blocked: a request to vanish event exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message) + } + + db.assertQuery(vanish, Filter(ids = listOf(vanish.id))) + db.assertQuery(null, Filter(ids = listOf(wrap1.id))) + db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id))) + } } diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SearchTest.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SearchTest.kt index f73a3372d..851c95417 100644 --- a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SearchTest.kt +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SearchTest.kt @@ -20,20 +20,14 @@ */ package com.vitorpamplona.quartz.nip01Core.store.sqlite -import android.content.Context -import androidx.test.core.app.ApplicationProvider import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import org.junit.After -import org.junit.Before import org.junit.Test -class SearchTest { - private lateinit var db: SQLiteEventStore - - companion object Companion { +class SearchTest : BaseDBTest() { + companion object { val profile = MetadataEvent( id = "490d7439e530423f2540d4f2bdb73a0a2935f3df9e1f2a6f699a140c7db311fe", @@ -74,35 +68,25 @@ class SearchTest { ) } - @Before - fun setup() { - val context = ApplicationProvider.getApplicationContext() - db = SQLiteEventStore(context, null) - } - - @After - fun tearDown() { - db.close() - } - @Test - fun testTagWithSearch() { - db.insertEvent(comment) - db.insertEvent(profile) + fun testTagWithSearch() = + forEachDB { db -> + db.store.insertEvent(comment) + db.store.insertEvent(profile) - db.assertQuery(null, Filter(search = "testing1")) - db.assertQuery(comment, Filter(search = "testing")) - db.assertQuery(comment, Filter(kinds = listOf(CommentEvent.KIND), search = "testing")) - db.assertQuery(null, Filter(kinds = listOf(TextNoteEvent.KIND), search = "testing")) + db.assertQuery(null, Filter(search = "testing1")) + db.assertQuery(comment, Filter(search = "testing")) + db.assertQuery(comment, Filter(kinds = listOf(CommentEvent.KIND), search = "testing")) + db.assertQuery(null, Filter(kinds = listOf(TextNoteEvent.KIND), search = "testing")) - db.delete(comment.id) + db.store.delete(comment.id) - db.assertQuery(null, Filter(search = "testing")) - db.assertQuery(null, Filter(kinds = listOf(CommentEvent.KIND), search = "testing")) + db.assertQuery(null, Filter(search = "testing")) + db.assertQuery(null, Filter(kinds = listOf(CommentEvent.KIND), search = "testing")) - db.insertEvent(comment) + db.store.insertEvent(comment) - db.assertQuery(comment, Filter(search = "testing")) - db.assertQuery(comment, Filter(kinds = listOf(CommentEvent.KIND), search = "testing")) - } + db.assertQuery(comment, Filter(search = "testing")) + db.assertQuery(comment, Filter(kinds = listOf(CommentEvent.KIND), search = "testing")) + } } diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt index e96c9f93c..4dbe73aa4 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt @@ -29,7 +29,7 @@ import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent class EventIndexesModule( val hasher: (db: SQLiteDatabase) -> TagNameValueHasher, - val tagIndexStrategy: IndexingStrategy = DefaultIndexingStrategy(), + val indexStrategy: IndexingStrategy = DefaultIndexingStrategy(), ) : IModule { override fun create(db: SQLiteDatabase) { db.execSQL( @@ -67,18 +67,38 @@ class EventIndexesModule( // queries by ID (load events) db.execSQL("CREATE UNIQUE INDEX event_headers_id ON event_headers (id)") - // queries by limit (latest records), since, until (sync all) alone - db.execSQL("CREATE INDEX query_by_created_at_id ON event_headers (created_at DESC, id)") + val orderBy = + if (indexStrategy.useAndIndexIdOnOrderBy) { + "created_at DESC, id ASC" + } else { + "created_at DESC" + } + + // queries by limit (latest records), since, until (sync all) alone without any filter by kind.. rare + // serves as fall back for the lack of query_by_tags_hash index by default + db.execSQL("CREATE INDEX query_by_created_at_id ON event_headers ($orderBy)") + + // queries by kind only, mostly used in Global Feeds when author is not important. + db.execSQL("CREATE INDEX query_by_kind_created ON event_headers (kind, $orderBy)") // queries by kind + pubkey, but not d-tag, even if they are replaceables and addressables, by date. - db.execSQL("CREATE INDEX query_by_kind_pubkey_created ON event_headers (kind, pubkey, created_at DESC)") + db.execSQL("CREATE INDEX query_by_kind_pubkey_created ON event_headers (kind, pubkey, $orderBy)") // makes deletions on the event_header fast db.execSQL("CREATE INDEX fk_event_tags_header_id ON event_tags (event_header_row_id)") - // This is a very slow index to build (80% of the insert time goes here) but it is extremely effective. - db.execSQL("CREATE INDEX query_by_tags_hash ON event_tags (tag_hash, created_at DESC)") + // --------------------------------------------------------------- + // This are a very slow indexes (80% of the insert time goes here) + // --------------------------------------------------------------- + if (indexStrategy.indexTagQueriesWithoutKinds) { + // First one is only needed if the user is searching by tags without a kind. + db.execSQL("CREATE INDEX query_by_tags_hash ON event_tags (tag_hash, created_at DESC)") + } + + // This is the default index for most clients: tags by specific kinds that are supported by the client. db.execSQL("CREATE INDEX query_by_tags_hash_kind ON event_tags (tag_hash, kind, created_at DESC)") + + // this one is to allow search of tags by kind and author at the same time: DMs, reports, db.execSQL("CREATE INDEX query_by_tags_hash_kind_pubkey ON event_tags (tag_hash, kind, pubkey_hash, created_at DESC)") // Prevent updates to maintain immutability @@ -173,7 +193,7 @@ class EventIndexesModule( // rebalancing the tree every new insert val indexableTags = ArrayList() for (idx in event.tags.indices) { - if (tagIndexStrategy.shouldIndex(event.kind, event.tags[idx])) { + if (indexStrategy.shouldIndex(event.kind, event.tags[idx])) { indexableTags.add(hasher.hash(event.tags[idx][0], event.tags[idx][1])) } } diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventStore.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventStore.kt index a4c85102e..53653f55d 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventStore.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventStore.kt @@ -29,9 +29,9 @@ class EventStore( context: Context, dbName: String? = "events.db", val relayUrl: String? = "wss://quartz.local", - val tagIndexStrategy: IndexingStrategy = DefaultIndexingStrategy(), + val indexStrategy: IndexingStrategy = DefaultIndexingStrategy(), ) : IEventStore { - val store = SQLiteEventStore(context, dbName, relayUrl, tagIndexStrategy) + val store = SQLiteEventStore(context, dbName, relayUrl, indexStrategy) override fun insert(event: Event) = store.insertEvent(event) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/IndexingStrategy.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/IndexingStrategy.kt index 4e50b4225..1f5cf7745 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/IndexingStrategy.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/IndexingStrategy.kt @@ -23,6 +23,9 @@ package com.vitorpamplona.quartz.nip01Core.store.sqlite import com.vitorpamplona.quartz.nip01Core.core.Tag interface IndexingStrategy { + val indexTagQueriesWithoutKinds: Boolean + val useAndIndexIdOnOrderBy: Boolean + fun shouldIndex( kind: Int, tag: Tag, @@ -32,7 +35,10 @@ interface IndexingStrategy { /** * By default, we index all tags that have a single letter name and some value */ -class DefaultIndexingStrategy : IndexingStrategy { +class DefaultIndexingStrategy( + override val indexTagQueriesWithoutKinds: Boolean = false, + override val useAndIndexIdOnOrderBy: Boolean = false, +) : IndexingStrategy { override fun shouldIndex( kind: Int, tag: Tag, diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt index 29bbf3f54..b782ab5f5 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt @@ -23,6 +23,8 @@ package com.vitorpamplona.quartz.nip01Core.store.sqlite import android.database.Cursor import android.database.sqlite.SQLiteDatabase import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Kind import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper import com.vitorpamplona.quartz.nip01Core.core.isAddressable import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter @@ -34,6 +36,7 @@ import kotlin.collections.component2 class QueryBuilder( val fts: FullTextSearchModule, val hasher: (db: SQLiteDatabase) -> TagNameValueHasher, + val indexStrategy: IndexingStrategy, ) { // ------------ // Main methods @@ -110,13 +113,37 @@ class QueryBuilder( filter: Filter, hasher: TagNameValueHasher, ): QuerySpec { + val newFilter = filter.toFilterWithDTags() + + if (newFilter.isSimpleQuery()) { + return makeSimpleQuery( + ids = newFilter.ids, + authors = newFilter.authors, + kinds = newFilter.kinds, + dTags = newFilter.dTags, + since = newFilter.since, + until = newFilter.until, + limit = newFilter.limit, + ) + } + + if (newFilter.isSimpleSearch()) { + return makeSimpleSearch( + search = newFilter.search!!, + ids = newFilter.ids, + authors = newFilter.authors, + kinds = newFilter.kinds, + dTags = newFilter.dTags, + since = newFilter.since, + until = newFilter.until, + limit = newFilter.limit, + ) + } + val rowIdSubqueries = prepareRowIDSubQueries(filter, hasher) return if (rowIdSubqueries == null) { - QuerySpec( - makeEverythingQuery(), - emptyList(), - ) + QuerySpec(makeEverythingQuery()) } else { QuerySpec( makeQueryIn(rowIdSubqueries.sql), @@ -129,6 +156,8 @@ class QueryBuilder( filters: List, hasher: TagNameValueHasher, ): QuerySpec { + if (filters.size == 1) return toSql(filters.first(), hasher) + val rowIdSubqueries = unionSubqueriesIfNeeded(filters, hasher) return if (rowIdSubqueries == null) { @@ -144,7 +173,7 @@ class QueryBuilder( } } - private fun makeEverythingQuery() = "SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers ORDER BY created_at DESC, id" + private fun makeEverythingQuery() = "SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers ORDER BY created_at DESC${if (indexStrategy.useAndIndexIdOnOrderBy) ", id ASC" else ""}" private fun makeQueryIn(rowIdQuery: String) = """ @@ -153,7 +182,7 @@ class QueryBuilder( $rowIdQuery ) AS filtered ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC, id + ORDER BY created_at DESC${if (indexStrategy.useAndIndexIdOnOrderBy) ", id ASC" else ""} """.trimIndent() private fun SQLiteDatabase.runQuery(query: QuerySpec): List = @@ -359,12 +388,12 @@ class QueryBuilder( buildString { // always do tags if there are any if (reverseLookup) { - append("SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags ") + append("SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags") // it's quite rare to have 2 tags in the filter, but possible nonDTagsIn.keys.forEachIndexed { index, tagName -> if (defaultTagKey != null) { - append("INNER JOIN event_tags as event_tagsIn$index ON event_tagsIn$index.event_header_row_id = event_tags.event_header_row_id AND event_tagsIn$index.created_at = event_tags.created_at ") + append(" INNER JOIN event_tags as event_tagsIn$index ON event_tagsIn$index.event_header_row_id = event_tags.event_header_row_id AND event_tagsIn$index.created_at = event_tags.created_at") } else { defaultTagKey = TagNameForQuery.InTags(tagName) } @@ -373,7 +402,7 @@ class QueryBuilder( nonDTagsAll.keys.forEachIndexed { index, tagName -> nonDTagsAll[tagName]!!.forEachIndexed { valueIndex, tagValue -> if (defaultTagKey != null) { - append("INNER JOIN event_tags as event_tagsAll${index}_$valueIndex ON event_tagsAll${index}_$valueIndex.event_header_row_id = event_tags.event_header_row_id AND event_tagsAll${index}_$valueIndex.created_at = event_tags.created_at ") + append(" INNER JOIN event_tags as event_tagsAll${index}_$valueIndex ON event_tagsAll${index}_$valueIndex.event_header_row_id = event_tags.event_header_row_id AND event_tagsAll${index}_$valueIndex.created_at = event_tags.created_at") } else { defaultTagKey = TagNameForQuery.AllTags(tagName, valueIndex) } @@ -381,21 +410,21 @@ class QueryBuilder( } if (needHeaders) { - append("INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id ") + append(" INNER JOIN event_headers ON event_headers.row_id = event_tags.event_header_row_id") } if (mustJoinSearch) { - append("INNER JOIN ${fts.tableName} ON ${fts.tableName}.${fts.eventHeaderRowIdName} = event_tags.event_header_row_id ") + append(" INNER JOIN ${fts.tableName} ON ${fts.tableName}.${fts.eventHeaderRowIdName} = event_tags.event_header_row_id") } } else if (mustJoinSearch) { - append("SELECT ${fts.tableName}.${fts.eventHeaderRowIdName} as row_id FROM ${fts.tableName} ") + append("SELECT ${fts.tableName}.${fts.eventHeaderRowIdName} as row_id FROM ${fts.tableName}") if (hasHeaders) { - append("INNER JOIN event_headers ON event_headers.row_id = ${fts.tableName}.${fts.eventHeaderRowIdName}") + append(" INNER JOIN event_headers ON event_headers.row_id = ${fts.tableName}.${fts.eventHeaderRowIdName}") } } else { // no tags and no search. - append("SELECT event_headers.row_id as row_id FROM event_headers ") + append("SELECT event_headers.row_id as row_id FROM event_headers") } } @@ -480,22 +509,183 @@ class QueryBuilder( } } - val whereClause = - if (filter.limit != null) { - if (reverseLookup) { - "${clause.conditions} ORDER BY event_tags.created_at DESC LIMIT ${filter.limit}" - } else { - "${clause.conditions} ORDER BY event_headers.created_at DESC, event_headers.id ASC LIMIT ${filter.limit}" + val sql = + buildString { + append(projection) + if (clause.conditions.isNotEmpty()) { + append(" WHERE ${clause.conditions}") + } + if (filter.limit != null) { + if (reverseLookup) { + append(" ORDER BY event_tags.created_at DESC") + append(" LIMIT ") + append(filter.limit) + } else { + append(" ORDER BY event_headers.created_at DESC") + append(" LIMIT ") + append(filter.limit) + } } - } else { - clause.conditions } - return QuerySpec("$projection WHERE $whereClause", clause.args) + return QuerySpec(sql, clause.args) } + private fun makeSimpleSearch( + search: String, + ids: List? = null, + authors: List? = null, + kinds: List? = null, + dTags: List? = null, + since: Long? = null, + until: Long? = null, + limit: Int? = null, + ): QuerySpec { + val clause = + where { + // the order should match indexes + // ids reduce the filter the most + ids?.let { equalsOrIn("event_headers.id", it) } + + match(fts.tableName, search) + + kinds?.let { equalsOrIn("event_headers.kind", it) } + authors?.let { equalsOrIn("event_headers.pubkey", it) } + + // there are indexes for these, starting with tags. + dTags?.let { equalsOrIn("event_headers.d_tag", it) } + + since?.let { greaterThanOrEquals("event_headers.created_at", it) } + until?.let { lessThanOrEquals("event_headers.created_at", it) } + + // if this is a dTag filter, it is likely that all kinds are addressables + // and so force the use of the addressable index + if (dTags != null && kinds != null) { + if (kinds.all { it.isAddressable() }) { + // matches unique index kind >= 30000 AND kind < 40000 + raw("(event_headers.kind >= 30000 AND kind < 40000)") + } + } + } + + val sql = + buildString { + append("SELECT event_headers.id, event_headers.pubkey, event_headers.created_at, event_headers.kind, event_headers.tags, event_headers.content, event_headers.sig FROM event_headers") + append("\nINNER JOIN ${fts.tableName} ON event_headers.row_id = ${fts.tableName}.${fts.eventHeaderRowIdName}") + if (clause.conditions.isNotEmpty()) { + append("\nWHERE ${clause.conditions}") + } + append("\nORDER BY event_headers.created_at DESC") + if (indexStrategy.useAndIndexIdOnOrderBy) { + append(", event_headers.id ASC") + } + if (limit != null) { + append("\nLIMIT ") + append(limit) + } + } + + println(sql) + + return QuerySpec(sql, clause.args) + } + + private fun makeSimpleQuery( + ids: List? = null, + authors: List? = null, + kinds: List? = null, + dTags: List? = null, + since: Long? = null, + until: Long? = null, + limit: Int? = null, + ): QuerySpec { + val clause = + where { + // the order should match indexes + // ids reduce the filter the most + ids?.let { equalsOrIn("id", it) } + + kinds?.let { equalsOrIn("kind", it) } + authors?.let { equalsOrIn("pubkey", it) } + + // there are indexes for these, starting with tags. + dTags?.let { equalsOrIn("d_tag", it) } + + since?.let { greaterThanOrEquals("created_at", it) } + until?.let { lessThanOrEquals("created_at", it) } + + // if this is a dTag filter, it is likely that all kinds are addressables + // and so force the use of the addressable index + if (dTags != null && kinds != null) { + if (kinds.all { it.isAddressable() }) { + // matches unique index kind >= 30000 AND kind < 40000 + raw("(kind >= 30000 AND kind < 40000)") + } + } + } + + val sql = + buildString { + append("SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers") + if (clause.conditions.isNotEmpty()) { + append("\nWHERE ") + append(clause.conditions) + } + append("\nORDER BY created_at DESC") + if (indexStrategy.useAndIndexIdOnOrderBy) { + append(", event_headers.id ASC") + } + if (limit != null) { + append("\nLIMIT ") + append(limit) + } + } + + println(sql) + + return QuerySpec(sql, clause.args) + } + + class FilterWithDTags( + val ids: List? = null, + val authors: List? = null, + val kinds: List? = null, + val dTags: List? = null, + val nonDTagsIn: Map>? = null, + val nonDTagsAll: Map>? = null, + val since: Long? = null, + val until: Long? = null, + val limit: Int? = null, + val search: String? = null, + ) { + fun isSimpleSearch() = + search != null && search.isNotEmpty() && + (nonDTagsIn == null || nonDTagsIn.isEmpty()) && + (nonDTagsAll == null || nonDTagsAll.isEmpty()) + + // can be resolved with just event_headers + fun isSimpleQuery() = + (nonDTagsIn == null || nonDTagsIn.isEmpty()) && + (nonDTagsAll == null || nonDTagsAll.isEmpty()) && + (search == null || search.isEmpty()) + } + + fun Filter.toFilterWithDTags(): FilterWithDTags = + FilterWithDTags( + ids = ids, + authors = authors, + kinds = kinds, + dTags = tags?.get("d") ?: tagsAll?.get("d"), + nonDTagsIn = tags?.filter { it.key != "d" }?.ifEmpty { null }, + nonDTagsAll = tagsAll?.filter { it.key != "d" }?.ifEmpty { null }, + since = since, + until = until, + limit = limit, + search = search, + ) + data class QuerySpec( val sql: String, - val args: List, + val args: List = emptyList(), ) } diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt index bc9c837e4..ff02edf6b 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt @@ -41,7 +41,7 @@ class SQLiteEventStore( val context: Context, val dbName: String? = "events.db", val relayUrl: String? = null, - val tagIndexStrategy: IndexingStrategy = DefaultIndexingStrategy(), + val indexStrategy: IndexingStrategy = DefaultIndexingStrategy(), ) : SQLiteOpenHelper(context, dbName, null, DATABASE_VERSION) { companion object { const val DATABASE_VERSION = 2 @@ -50,7 +50,7 @@ class SQLiteEventStore( val seedModule = SeedModule() val fullTextSearchModule = FullTextSearchModule() - val eventIndexModule = EventIndexesModule(seedModule::hasher, tagIndexStrategy) + val eventIndexModule = EventIndexesModule(seedModule::hasher, indexStrategy) val replaceableModule = ReplaceableModule() val addressableModule = AddressableModule() @@ -60,7 +60,7 @@ class SQLiteEventStore( val expirationModule = ExpirationModule() val rightToVanishModule = RightToVanishModule(seedModule::hasher) - val queryBuilder = QueryBuilder(fullTextSearchModule, seedModule::hasher) + val queryBuilder = QueryBuilder(fullTextSearchModule, seedModule::hasher, indexStrategy) val modules = listOf( diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/Condition.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/Condition.kt index 738cef423..dd4eb90ca 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/Condition.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/Condition.kt @@ -85,4 +85,6 @@ sealed class Condition { data class Or( val conditions: List, ) : Condition() + + class Empty : Condition() } diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/SqlSelectionBuilder.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/SqlSelectionBuilder.kt index 78606720d..b109aff26 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/SqlSelectionBuilder.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/SqlSelectionBuilder.kt @@ -38,6 +38,9 @@ class SqlSelectionBuilder( */ private fun buildCondition(cond: Condition): String = when (cond) { + is Condition.Empty -> { + "" + } is Condition.Raw -> { cond.condition } diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/WhereClauseBuilder.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/WhereClauseBuilder.kt index 1a42921f6..0cb89f573 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/WhereClauseBuilder.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/WhereClauseBuilder.kt @@ -119,7 +119,7 @@ class WhereClauseBuilder { } fun where(block: WhereClauseBuilder.() -> Unit): WhereClause { - val condition = WhereClauseBuilder().apply(block).buildAnd() ?: Condition.And(emptyList()) + val condition = WhereClauseBuilder().apply(block).buildAnd() ?: Condition.Empty() return SqlSelectionBuilder(condition).build() } From 98ba083172c4ad8a6e5c2f327ad2eebeb929ea75 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 2 Jan 2026 19:56:51 +0000 Subject: [PATCH 048/137] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-zh-rCN/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index cb23aebc8..714143826 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -158,6 +158,7 @@ 录制消息 录制消息 点按以录制消息 + 重新录制 正在录制 正在录制 %1$s 上传中… From 48e791c634c9f989642015c5fb8513c83758c384 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 2 Jan 2026 16:17:58 -0500 Subject: [PATCH 049/137] removes logs --- .../quartz/nip01Core/store/sqlite/QueryBuilder.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt index b782ab5f5..39701d74c 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt @@ -585,8 +585,6 @@ class QueryBuilder( } } - println(sql) - return QuerySpec(sql, clause.args) } @@ -641,8 +639,6 @@ class QueryBuilder( } } - println(sql) - return QuerySpec(sql, clause.args) } From 989c63a71777573b1ffa91675123dd6e6a33b4ef Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 2 Jan 2026 16:57:49 -0500 Subject: [PATCH 050/137] Expose query plans with the current hasher --- .../quartz/nip01Core/store/sqlite/SQLiteEventStore.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt index ff02edf6b..515d492c4 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt @@ -214,6 +214,10 @@ class SQLiteEventStore( onEach: (RawEvent) -> Unit, ) = queryBuilder.rawQuery(filters, readableDatabase, onEach) + fun planQuery(filter: Filter) = queryBuilder.planQuery(filter, seedModule.hasher(readableDatabase), readableDatabase) + + fun planQuery(filters: List) = queryBuilder.planQuery(filters, seedModule.hasher(readableDatabase), readableDatabase) + fun count(filter: Filter): Int = queryBuilder.count(filter, readableDatabase) fun count(filters: List): Int = queryBuilder.count(filters, readableDatabase) From 51f9fdd58c514c6c58b293d81bd86786985950e9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 2 Jan 2026 16:58:59 -0500 Subject: [PATCH 051/137] Adds a simple query to the count filter --- .../nip01Core/store/sqlite/QueryBuilder.kt | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt index 39701d74c..53dd40202 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt @@ -117,6 +117,7 @@ class QueryBuilder( if (newFilter.isSimpleQuery()) { return makeSimpleQuery( + project = true, ids = newFilter.ids, authors = newFilter.authors, kinds = newFilter.kinds, @@ -250,6 +251,23 @@ class QueryBuilder( filter: Filter, db: SQLiteDatabase, ): Int { + val newFilter = filter.toFilterWithDTags() + + if (newFilter.isSimpleQuery()) { + val sql = + makeSimpleQuery( + project = false, + ids = newFilter.ids, + authors = newFilter.authors, + kinds = newFilter.kinds, + dTags = newFilter.dTags, + since = newFilter.since, + until = newFilter.until, + limit = newFilter.limit, + ) + return db.runCount(sql.sql, sql.args) + } + val rowIdSubQuery = prepareRowIDSubQueries(filter, hasher(db)) return if (rowIdSubQuery == null) { @@ -589,6 +607,7 @@ class QueryBuilder( } private fun makeSimpleQuery( + project: Boolean, ids: List? = null, authors: List? = null, kinds: List? = null, @@ -624,14 +643,20 @@ class QueryBuilder( val sql = buildString { - append("SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers") + if (project) { + append("SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers") + } else { + append("SELECT count(*) FROM event_headers") + } if (clause.conditions.isNotEmpty()) { append("\nWHERE ") append(clause.conditions) } - append("\nORDER BY created_at DESC") - if (indexStrategy.useAndIndexIdOnOrderBy) { - append(", event_headers.id ASC") + if (project) { + append("\nORDER BY created_at DESC") + if (indexStrategy.useAndIndexIdOnOrderBy) { + append(", event_headers.id ASC") + } } if (limit != null) { append("\nLIMIT ") From 7a34b8480e6b544e725620cd47a22745a00c20ed Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 2 Jan 2026 17:22:18 -0500 Subject: [PATCH 052/137] Fixes count not working for LIMIT queries --- .../quartz/nip01Core/store/sqlite/QueryBuilder.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt index 53dd40202..ef4467b84 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt @@ -265,7 +265,7 @@ class QueryBuilder( until = newFilter.until, limit = newFilter.limit, ) - return db.runCount(sql.sql, sql.args) + return db.countIn(sql.sql, sql.args) } val rowIdSubQuery = prepareRowIDSubQueries(filter, hasher(db)) @@ -646,7 +646,7 @@ class QueryBuilder( if (project) { append("SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers") } else { - append("SELECT count(*) FROM event_headers") + append("SELECT row_id FROM event_headers") } if (clause.conditions.isNotEmpty()) { append("\nWHERE ") From f800e20b05f727c731e9dc4a1e4aae171ed9c200 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Sat, 3 Jan 2026 06:15:37 +0200 Subject: [PATCH 053/137] reuse some filtering logic --- .../commons/filters/FilterBuilders.kt | 355 ++++++++++++ .../commons/filters/FilterBuildersTest.kt | 508 ++++++++++++++++++ .../commons/subscriptions/FeedSubscription.kt | 89 +++ .../subscriptions/ProfileSubscription.kt | 79 +++ .../subscriptions/SubscriptionUtils.kt | 114 ++++ desktopApp/build.gradle.kts | 9 + .../amethyst/desktop/ui/FeedScreen.kt | 137 ++--- .../desktop/ui/NotificationsScreen.kt | 129 ++--- .../amethyst/desktop/ui/UserProfileScreen.kt | 184 ++----- .../desktop/network/DesktopHttpClientTest.kt | 92 ++++ .../DesktopRelayConnectionManagerTest.kt | 51 ++ 11 files changed, 1427 insertions(+), 320 deletions(-) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuilders.kt create mode 100644 commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuildersTest.kt create mode 100644 commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FeedSubscription.kt create mode 100644 commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/ProfileSubscription.kt create mode 100644 commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/SubscriptionUtils.kt create mode 100644 desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopHttpClientTest.kt create mode 100644 desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopRelayConnectionManagerTest.kt diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuilders.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuilders.kt new file mode 100644 index 000000000..33bee5095 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuilders.kt @@ -0,0 +1,355 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.filters + +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter + +/** + * Type-safe builders for common Nostr filter patterns. + * Provides convenience functions for creating relay subscription filters. + */ +object FilterBuilders { + /** + * Creates a filter for text notes (kind 1) from all authors. + * + * @param limit Maximum number of events to request + * @param since Timestamp for events with publication time ≥ this value + * @param until Timestamp for events with publication time ≤ this value + * @return Filter for global text notes + */ + fun textNotesGlobal( + limit: Int? = null, + since: Long? = null, + until: Long? = null, + ): Filter = + Filter( + kinds = listOf(1), // TextNoteEvent.KIND + limit = limit, + since = since, + until = until, + ) + + /** + * Creates a filter for text notes (kind 1) from specific authors. + * + * @param authors List of author public keys (hex-encoded, 64 chars each) + * @param limit Maximum number of events to request + * @param since Timestamp for events with publication time ≥ this value + * @param until Timestamp for events with publication time ≤ this value + * @return Filter for text notes from specified authors + */ + fun textNotesFromAuthors( + authors: List, + limit: Int? = null, + since: Long? = null, + until: Long? = null, + ): Filter = + Filter( + kinds = listOf(1), // TextNoteEvent.KIND + authors = authors, + limit = limit, + since = since, + until = until, + ) + + /** + * Creates a filter for user metadata (kind 0) from a specific author. + * + * @param pubKeyHex Author public key (hex-encoded, 64 chars) + * @return Filter for user metadata (limit=1 since only latest is needed) + */ + fun userMetadata(pubKeyHex: String): Filter = + Filter( + kinds = listOf(0), // MetadataEvent.KIND + authors = listOf(pubKeyHex), + limit = 1, + ) + + /** + * Creates a filter for contact list (kind 3) from a specific author. + * + * @param pubKeyHex Author public key (hex-encoded, 64 chars) + * @return Filter for contact list (limit=1 since only latest is needed) + */ + fun contactList(pubKeyHex: String): Filter = + Filter( + kinds = listOf(3), // ContactListEvent.KIND + authors = listOf(pubKeyHex), + limit = 1, + ) + + /** + * Creates a filter for notifications (mentions, replies, reactions, reposts, zaps) for a user. + * + * Includes: + * - kind 1 (text notes mentioning user) + * - kind 7 (reactions) + * - kind 6 (reposts) + * - kind 16 (generic reposts) + * - kind 9735 (zaps) + * + * @param pubKeyHex User public key (hex-encoded, 64 chars) to filter notifications for + * @param limit Maximum number of events to request + * @param since Timestamp for events with publication time ≥ this value + * @return Filter for notifications targeting the specified user + */ + fun notificationsForUser( + pubKeyHex: String, + limit: Int? = null, + since: Long? = null, + ): Filter = + Filter( + kinds = + listOf( + 1, // TextNoteEvent.KIND (mentions/replies) + 7, // ReactionEvent.KIND + 6, // RepostEvent.KIND + 16, // GenericRepostEvent.KIND + 9735, // LnZapEvent.KIND + ), + tags = mapOf("p" to listOf(pubKeyHex)), + limit = limit, + since = since, + ) + + /** + * Creates a filter for specific event kinds. + * + * @param kinds List of event kinds to filter + * @param limit Maximum number of events to request + * @param since Timestamp for events with publication time ≥ this value + * @param until Timestamp for events with publication time ≤ this value + * @return Filter for specified event kinds + */ + fun byKinds( + kinds: List, + limit: Int? = null, + since: Long? = null, + until: Long? = null, + ): Filter = + Filter( + kinds = kinds, + limit = limit, + since = since, + until = until, + ) + + /** + * Creates a filter for events from specific authors. + * + * @param authors List of author public keys (hex-encoded, 64 chars each) + * @param kinds Optional list of event kinds to filter (if null, all kinds) + * @param limit Maximum number of events to request + * @param since Timestamp for events with publication time ≥ this value + * @param until Timestamp for events with publication time ≤ this value + * @return Filter for events from specified authors + */ + fun byAuthors( + authors: List, + kinds: List? = null, + limit: Int? = null, + since: Long? = null, + until: Long? = null, + ): Filter = + Filter( + authors = authors, + kinds = kinds, + limit = limit, + since = since, + until = until, + ) + + /** + * Creates a filter for events with specific event IDs. + * + * @param ids List of event IDs (hex-encoded, 64 chars each) + * @return Filter for specified event IDs + */ + fun byIds(ids: List): Filter = + Filter( + ids = ids, + ) + + /** + * Creates a filter for events tagged with specific p-tags (mentioning users). + * + * @param pubKeys List of public keys (hex-encoded, 64 chars each) to filter by + * @param kinds Optional list of event kinds to filter + * @param limit Maximum number of events to request + * @param since Timestamp for events with publication time ≥ this value + * @return Filter for events mentioning specified users + */ + fun byPTags( + pubKeys: List, + kinds: List? = null, + limit: Int? = null, + since: Long? = null, + ): Filter = + Filter( + tags = mapOf("p" to pubKeys), + kinds = kinds, + limit = limit, + since = since, + ) + + /** + * Creates a filter for events tagged with specific e-tags (referencing events). + * + * @param eventIds List of event IDs (hex-encoded, 64 chars each) to filter by + * @param kinds Optional list of event kinds to filter + * @param limit Maximum number of events to request + * @param since Timestamp for events with publication time ≥ this value + * @return Filter for events referencing specified events + */ + fun byETags( + eventIds: List, + kinds: List? = null, + limit: Int? = null, + since: Long? = null, + ): Filter = + Filter( + tags = mapOf("e" to eventIds), + kinds = kinds, + limit = limit, + since = since, + ) + + /** + * Creates a filter for events with custom tag filters. + * + * @param tags Map of tag names to value lists (e.g., {"p": ["pubkey1"], "t": ["bitcoin"]}) + * @param kinds Optional list of event kinds to filter + * @param limit Maximum number of events to request + * @param since Timestamp for events with publication time ≥ this value + * @param until Timestamp for events with publication time ≤ this value + * @return Filter with custom tag criteria + */ + fun byTags( + tags: Map>, + kinds: List? = null, + limit: Int? = null, + since: Long? = null, + until: Long? = null, + ): Filter = + Filter( + tags = tags, + kinds = kinds, + limit = limit, + since = since, + until = until, + ) +} + +/** + * DSL builder for creating custom filters with a fluent API. + * + * Example: + * ```kotlin + * val filter = buildFilter { + * kinds(1, 7) + * authors("pubkey1", "pubkey2") + * limit(50) + * since(System.currentTimeMillis() / 1000 - 86400) // Last 24 hours + * } + * ``` + */ +class FilterBuilder { + private var ids: List? = null + private var authors: List? = null + private var kinds: List? = null + private var tags: MutableMap>? = null + private var since: Long? = null + private var until: Long? = null + private var limit: Int? = null + private var search: String? = null + + fun ids(vararg ids: String) { + this.ids = ids.toList() + } + + fun ids(ids: List) { + this.ids = ids + } + + fun authors(vararg authors: String) { + this.authors = authors.toList() + } + + fun authors(authors: List) { + this.authors = authors + } + + fun kinds(vararg kinds: Int) { + this.kinds = kinds.toList() + } + + fun kinds(kinds: List) { + this.kinds = kinds + } + + fun tag( + name: String, + values: List, + ) { + if (tags == null) tags = mutableMapOf() + tags!![name] = values + } + + fun pTag(vararg pubKeys: String) { + tag("p", pubKeys.toList()) + } + + fun eTag(vararg eventIds: String) { + tag("e", eventIds.toList()) + } + + fun since(timestamp: Long) { + this.since = timestamp + } + + fun until(timestamp: Long) { + this.until = timestamp + } + + fun limit(limit: Int) { + this.limit = limit + } + + fun search(search: String) { + this.search = search + } + + fun build(): Filter = Filter(ids, authors, kinds, tags, since, until, limit, search) +} + +/** + * Creates a filter using the DSL builder. + * + * Example: + * ```kotlin + * val filter = buildFilter { + * kinds(1) + * authors("pubkey1", "pubkey2") + * limit(50) + * } + * ``` + */ +fun buildFilter(block: FilterBuilder.() -> Unit): Filter = FilterBuilder().apply(block).build() diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuildersTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuildersTest.kt new file mode 100644 index 000000000..655c4d99f --- /dev/null +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuildersTest.kt @@ -0,0 +1,508 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.filters + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class FilterBuildersTest { + private val testPubKey = "0000000000000000000000000000000000000000000000000000000000000001" + private val testPubKey2 = "0000000000000000000000000000000000000000000000000000000000000002" + private val testEventId = "1111111111111111111111111111111111111111111111111111111111111111" + + @Test + fun testTextNotesGlobal() { + val filter = FilterBuilders.textNotesGlobal(limit = 50) + + assertEquals(listOf(1), filter.kinds) + assertEquals(50, filter.limit) + assertNull(filter.authors) + assertNull(filter.tags) + assertNull(filter.since) + assertNull(filter.until) + } + + @Test + fun testTextNotesGlobalWithTimeRange() { + val since = 1609459200L // 2021-01-01 + val until = 1640995200L // 2022-01-01 + val filter = FilterBuilders.textNotesGlobal(limit = 100, since = since, until = until) + + assertEquals(listOf(1), filter.kinds) + assertEquals(100, filter.limit) + assertEquals(since, filter.since) + assertEquals(until, filter.until) + } + + @Test + fun testTextNotesFromAuthors() { + val authors = listOf(testPubKey, testPubKey2) + val filter = FilterBuilders.textNotesFromAuthors(authors, limit = 25) + + assertEquals(listOf(1), filter.kinds) + assertEquals(authors, filter.authors) + assertEquals(25, filter.limit) + assertNull(filter.tags) + } + + @Test + fun testTextNotesFromAuthorsWithTimeRange() { + val authors = listOf(testPubKey) + val since = 1609459200L + val filter = FilterBuilders.textNotesFromAuthors(authors, limit = 10, since = since) + + assertEquals(listOf(1), filter.kinds) + assertEquals(authors, filter.authors) + assertEquals(10, filter.limit) + assertEquals(since, filter.since) + } + + @Test + fun testUserMetadata() { + val filter = FilterBuilders.userMetadata(testPubKey) + + assertEquals(listOf(0), filter.kinds) + assertEquals(listOf(testPubKey), filter.authors) + assertEquals(1, filter.limit) + } + + @Test + fun testContactList() { + val filter = FilterBuilders.contactList(testPubKey) + + assertEquals(listOf(3), filter.kinds) + assertEquals(listOf(testPubKey), filter.authors) + assertEquals(1, filter.limit) + } + + @Test + fun testNotificationsForUser() { + val filter = FilterBuilders.notificationsForUser(testPubKey, limit = 100) + + assertEquals(listOf(1, 7, 6, 16, 9735), filter.kinds) + assertNotNull(filter.tags) + assertEquals(listOf(testPubKey), filter.tags!!["p"]) + assertEquals(100, filter.limit) + } + + @Test + fun testNotificationsForUserWithSince() { + val since = 1609459200L + val filter = FilterBuilders.notificationsForUser(testPubKey, limit = 50, since = since) + + assertEquals(listOf(1, 7, 6, 16, 9735), filter.kinds) + assertNotNull(filter.tags) + assertEquals(listOf(testPubKey), filter.tags!!["p"]) + assertEquals(50, filter.limit) + assertEquals(since, filter.since) + } + + @Test + fun testByKinds() { + val kinds = listOf(1, 7, 6) + val filter = FilterBuilders.byKinds(kinds, limit = 20) + + assertEquals(kinds, filter.kinds) + assertEquals(20, filter.limit) + assertNull(filter.authors) + assertNull(filter.tags) + } + + @Test + fun testByKindsWithTimeRange() { + val kinds = listOf(30023) // Long-form content + val since = 1609459200L + val until = 1640995200L + val filter = FilterBuilders.byKinds(kinds, limit = 5, since = since, until = until) + + assertEquals(kinds, filter.kinds) + assertEquals(5, filter.limit) + assertEquals(since, filter.since) + assertEquals(until, filter.until) + } + + @Test + fun testByAuthors() { + val authors = listOf(testPubKey, testPubKey2) + val filter = FilterBuilders.byAuthors(authors, limit = 30) + + assertEquals(authors, filter.authors) + assertEquals(30, filter.limit) + assertNull(filter.kinds) + } + + @Test + fun testByAuthorsWithKinds() { + val authors = listOf(testPubKey) + val kinds = listOf(1, 30023) + val filter = FilterBuilders.byAuthors(authors, kinds = kinds, limit = 15) + + assertEquals(authors, filter.authors) + assertEquals(kinds, filter.kinds) + assertEquals(15, filter.limit) + } + + @Test + fun testByIds() { + val ids = listOf(testEventId) + val filter = FilterBuilders.byIds(ids) + + assertEquals(ids, filter.ids) + assertNull(filter.kinds) + assertNull(filter.authors) + assertNull(filter.limit) + } + + @Test + fun testByPTags() { + val pubKeys = listOf(testPubKey, testPubKey2) + val filter = FilterBuilders.byPTags(pubKeys, limit = 40) + + assertNotNull(filter.tags) + assertEquals(pubKeys, filter.tags!!["p"]) + assertEquals(40, filter.limit) + assertNull(filter.kinds) + } + + @Test + fun testByPTagsWithKinds() { + val pubKeys = listOf(testPubKey) + val kinds = listOf(7) // Reactions + val filter = FilterBuilders.byPTags(pubKeys, kinds = kinds, limit = 25) + + assertNotNull(filter.tags) + assertEquals(pubKeys, filter.tags!!["p"]) + assertEquals(kinds, filter.kinds) + assertEquals(25, filter.limit) + } + + @Test + fun testByETags() { + val eventIds = listOf(testEventId) + val filter = FilterBuilders.byETags(eventIds, limit = 10) + + assertNotNull(filter.tags) + assertEquals(eventIds, filter.tags!!["e"]) + assertEquals(10, filter.limit) + assertNull(filter.kinds) + } + + @Test + fun testByETagsWithKinds() { + val eventIds = listOf(testEventId) + val kinds = listOf(1, 7) // Text notes and reactions + val filter = FilterBuilders.byETags(eventIds, kinds = kinds, limit = 20) + + assertNotNull(filter.tags) + assertEquals(eventIds, filter.tags!!["e"]) + assertEquals(kinds, filter.kinds) + assertEquals(20, filter.limit) + } + + @Test + fun testByTags() { + val tags = mapOf("p" to listOf(testPubKey), "t" to listOf("bitcoin", "nostr")) + val filter = FilterBuilders.byTags(tags, limit = 15) + + assertEquals(tags, filter.tags) + assertEquals(15, filter.limit) + assertNull(filter.kinds) + } + + @Test + fun testByTagsWithKinds() { + val tags = mapOf("t" to listOf("bitcoin")) + val kinds = listOf(1) + val filter = FilterBuilders.byTags(tags, kinds = kinds, limit = 50) + + assertEquals(tags, filter.tags) + assertEquals(kinds, filter.kinds) + assertEquals(50, filter.limit) + } + + // DSL Builder Tests + + @Test + fun testBuildFilterWithKinds() { + val filter = + buildFilter { + kinds(1, 7) + limit(50) + } + + assertEquals(listOf(1, 7), filter.kinds) + assertEquals(50, filter.limit) + } + + @Test + fun testBuildFilterWithAuthors() { + val filter = + buildFilter { + authors(testPubKey, testPubKey2) + limit(25) + } + + assertEquals(listOf(testPubKey, testPubKey2), filter.authors) + assertEquals(25, filter.limit) + } + + @Test + fun testBuildFilterWithPTag() { + val filter = + buildFilter { + kinds(7) + pTag(testPubKey) + limit(100) + } + + assertEquals(listOf(7), filter.kinds) + assertNotNull(filter.tags) + assertEquals(listOf(testPubKey), filter.tags!!["p"]) + assertEquals(100, filter.limit) + } + + @Test + fun testBuildFilterWithETag() { + val filter = + buildFilter { + kinds(1) + eTag(testEventId) + limit(10) + } + + assertEquals(listOf(1), filter.kinds) + assertNotNull(filter.tags) + assertEquals(listOf(testEventId), filter.tags!!["e"]) + assertEquals(10, filter.limit) + } + + @Test + fun testBuildFilterWithCustomTag() { + val filter = + buildFilter { + kinds(1) + tag("t", listOf("bitcoin", "nostr")) + limit(20) + } + + assertEquals(listOf(1), filter.kinds) + assertNotNull(filter.tags) + assertEquals(listOf("bitcoin", "nostr"), filter.tags!!["t"]) + assertEquals(20, filter.limit) + } + + @Test + fun testBuildFilterWithTimeRange() { + val since = 1609459200L + val until = 1640995200L + val filter = + buildFilter { + kinds(1) + since(since) + until(until) + limit(30) + } + + assertEquals(listOf(1), filter.kinds) + assertEquals(since, filter.since) + assertEquals(until, filter.until) + assertEquals(30, filter.limit) + } + + @Test + fun testBuildFilterComplex() { + val since = 1609459200L + val filter = + buildFilter { + kinds(1, 7, 6) + authors(testPubKey) + pTag(testPubKey2) + since(since) + limit(50) + } + + assertEquals(listOf(1, 7, 6), filter.kinds) + assertEquals(listOf(testPubKey), filter.authors) + assertNotNull(filter.tags) + assertEquals(listOf(testPubKey2), filter.tags!!["p"]) + assertEquals(since, filter.since) + assertEquals(50, filter.limit) + } + + @Test + fun testBuildFilterWithSearch() { + val filter = + buildFilter { + kinds(1) + search("bitcoin") + limit(10) + } + + assertEquals(listOf(1), filter.kinds) + assertEquals("bitcoin", filter.search) + assertEquals(10, filter.limit) + } + + @Test + fun testBuildFilterWithIds() { + val filter = + buildFilter { + ids(testEventId) + } + + assertEquals(listOf(testEventId), filter.ids) + assertNull(filter.kinds) + } + + @Test + fun testBuildFilterWithIdsList() { + val ids = listOf(testEventId, "2222222222222222222222222222222222222222222222222222222222222222") + val filter = + buildFilter { + ids(ids) + } + + assertEquals(ids, filter.ids) + } + + @Test + fun testBuildFilterWithAuthorsList() { + val authors = listOf(testPubKey, testPubKey2) + val filter = + buildFilter { + authors(authors) + limit(15) + } + + assertEquals(authors, filter.authors) + assertEquals(15, filter.limit) + } + + @Test + fun testBuildFilterWithKindsList() { + val kinds = listOf(1, 7, 6, 16, 9735) + val filter = + buildFilter { + kinds(kinds) + limit(100) + } + + assertEquals(kinds, filter.kinds) + assertEquals(100, filter.limit) + } + + // Integration Tests - Real-world scenarios + + @Test + fun testGlobalFeedScenario() { + val filter = FilterBuilders.textNotesGlobal(limit = 50) + + assertTrue(filter.isFilledFilter()) + assertEquals(listOf(1), filter.kinds) + assertEquals(50, filter.limit) + } + + @Test + fun testFollowingFeedScenario() { + val followedUsers = listOf(testPubKey, testPubKey2) + val filter = FilterBuilders.textNotesFromAuthors(followedUsers, limit = 50) + + assertTrue(filter.isFilledFilter()) + assertEquals(listOf(1), filter.kinds) + assertEquals(followedUsers, filter.authors) + assertEquals(50, filter.limit) + } + + @Test + fun testUserProfileScenario() { + val metadataFilter = FilterBuilders.userMetadata(testPubKey) + val postsFilter = FilterBuilders.textNotesFromAuthors(listOf(testPubKey), limit = 50) + val contactListFilter = FilterBuilders.contactList(testPubKey) + + assertTrue(metadataFilter.isFilledFilter()) + assertTrue(postsFilter.isFilledFilter()) + assertTrue(contactListFilter.isFilledFilter()) + + assertEquals(listOf(0), metadataFilter.kinds) + assertEquals(listOf(1), postsFilter.kinds) + assertEquals(listOf(3), contactListFilter.kinds) + } + + @Test + fun testNotificationsScenario() { + val filter = FilterBuilders.notificationsForUser(testPubKey, limit = 100) + + assertTrue(filter.isFilledFilter()) + assertEquals(listOf(1, 7, 6, 16, 9735), filter.kinds) + assertNotNull(filter.tags) + assertEquals(listOf(testPubKey), filter.tags!!["p"]) + assertEquals(100, filter.limit) + } + + @Test + fun testThreadViewScenario() { + // Getting replies to a specific event + val filter = + buildFilter { + kinds(1) + eTag(testEventId) + limit(100) + } + + assertTrue(filter.isFilledFilter()) + assertEquals(listOf(1), filter.kinds) + assertNotNull(filter.tags) + assertEquals(listOf(testEventId), filter.tags!!["e"]) + } + + @Test + fun testReactionsToEventScenario() { + val filter = + buildFilter { + kinds(7) // Reactions + eTag(testEventId) + limit(50) + } + + assertTrue(filter.isFilledFilter()) + assertEquals(listOf(7), filter.kinds) + assertNotNull(filter.tags) + assertEquals(listOf(testEventId), filter.tags!!["e"]) + } + + @Test + fun testHashtagFeedScenario() { + val filter = + buildFilter { + kinds(1) + tag("t", listOf("bitcoin")) + limit(50) + } + + assertTrue(filter.isFilledFilter()) + assertEquals(listOf(1), filter.kinds) + assertNotNull(filter.tags) + assertEquals(listOf("bitcoin"), filter.tags!!["t"]) + } +} diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FeedSubscription.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FeedSubscription.kt new file mode 100644 index 000000000..66454159c --- /dev/null +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FeedSubscription.kt @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.subscriptions + +import com.vitorpamplona.amethyst.commons.filters.FilterBuilders +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl + +/** + * Feed mode for feed subscriptions. + */ +enum class FeedMode { + GLOBAL, + FOLLOWING, +} + +/** + * Creates a subscription config for global feed (all text notes). + */ +fun createGlobalFeedSubscription( + relays: Set, + limit: Int = 50, + onEvent: (Event, Boolean, NormalizedRelayUrl, List?) -> Unit, + onEose: (NormalizedRelayUrl, List?) -> Unit = { _, _ -> }, +): SubscriptionConfig = + SubscriptionConfig( + subId = generateSubId("global-feed"), + filters = listOf(FilterBuilders.textNotesGlobal(limit = limit)), + relays = relays, + onEvent = onEvent, + onEose = onEose, + ) + +/** + * Creates a subscription config for following feed (text notes from followed users). + */ +fun createFollowingFeedSubscription( + relays: Set, + followedUsers: List, + limit: Int = 50, + onEvent: (Event, Boolean, NormalizedRelayUrl, List?) -> Unit, + onEose: (NormalizedRelayUrl, List?) -> Unit = { _, _ -> }, +): SubscriptionConfig? { + if (followedUsers.isEmpty()) return null + + return SubscriptionConfig( + subId = generateSubId("following-feed"), + filters = listOf(FilterBuilders.textNotesFromAuthors(followedUsers, limit = limit)), + relays = relays, + onEvent = onEvent, + onEose = onEose, + ) +} + +/** + * Creates a subscription config for contact list (kind 3). + */ +fun createContactListSubscription( + relays: Set, + pubKeyHex: String, + onEvent: (Event, Boolean, NormalizedRelayUrl, List?) -> Unit, + onEose: (NormalizedRelayUrl, List?) -> Unit = { _, _ -> }, +): SubscriptionConfig = + SubscriptionConfig( + subId = generateSubId("contact-list-$pubKeyHex"), + filters = listOf(FilterBuilders.contactList(pubKeyHex)), + relays = relays, + onEvent = onEvent, + onEose = onEose, + ) diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/ProfileSubscription.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/ProfileSubscription.kt new file mode 100644 index 000000000..b1cbdaea4 --- /dev/null +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/ProfileSubscription.kt @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.subscriptions + +import com.vitorpamplona.amethyst.commons.filters.FilterBuilders +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl + +/** + * Creates a subscription config for user metadata (kind 0). + */ +fun createMetadataSubscription( + relays: Set, + pubKeyHex: String, + onEvent: (Event, Boolean, NormalizedRelayUrl, List?) -> Unit, + onEose: (NormalizedRelayUrl, List?) -> Unit = { _, _ -> }, +): SubscriptionConfig = + SubscriptionConfig( + subId = generateSubId("profile-metadata-$pubKeyHex"), + filters = listOf(FilterBuilders.userMetadata(pubKeyHex)), + relays = relays, + onEvent = onEvent, + onEose = onEose, + ) + +/** + * Creates a subscription config for user posts (kind 1). + */ +fun createUserPostsSubscription( + relays: Set, + pubKeyHex: String, + limit: Int = 50, + onEvent: (Event, Boolean, NormalizedRelayUrl, List?) -> Unit, + onEose: (NormalizedRelayUrl, List?) -> Unit = { _, _ -> }, +): SubscriptionConfig = + SubscriptionConfig( + subId = generateSubId("profile-posts-$pubKeyHex"), + filters = listOf(FilterBuilders.textNotesFromAuthors(listOf(pubKeyHex), limit = limit)), + relays = relays, + onEvent = onEvent, + onEose = onEose, + ) + +/** + * Creates a subscription config for notifications (mentions, replies, reactions, reposts, zaps). + */ +fun createNotificationsSubscription( + relays: Set, + pubKeyHex: String, + limit: Int = 100, + onEvent: (Event, Boolean, NormalizedRelayUrl, List?) -> Unit, + onEose: (NormalizedRelayUrl, List?) -> Unit = { _, _ -> }, +): SubscriptionConfig = + SubscriptionConfig( + subId = generateSubId("notifications-$pubKeyHex"), + filters = listOf(FilterBuilders.notificationsForUser(pubKeyHex, limit = limit)), + relays = relays, + onEvent = onEvent, + onEose = onEose, + ) diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/SubscriptionUtils.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/SubscriptionUtils.kt new file mode 100644 index 000000000..269901810 --- /dev/null +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/SubscriptionUtils.kt @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.subscriptions + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.remember +import com.vitorpamplona.amethyst.commons.network.RelayConnectionManager +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl + +/** + * Represents an active relay subscription that can be unsubscribed. + */ +data class SubscriptionHandle( + val subId: String, + val unsubscribe: () -> Unit, +) + +/** + * Configuration for a relay subscription. + */ +data class SubscriptionConfig( + val subId: String, + val filters: List, + val relays: Set, + val onEvent: (Event, Boolean, NormalizedRelayUrl, List?) -> Unit, + val onEose: (NormalizedRelayUrl, List?) -> Unit = { _, _ -> }, +) + +/** + * Composable that remembers a subscription and automatically unsubscribes on dispose. + * + * Usage: + * ```kotlin + * rememberSubscription(relayStatuses, pubKeyHex) { + * SubscriptionConfig( + * subId = "my-sub-${System.currentTimeMillis()}", + * filters = listOf(Filter(kinds = listOf(1), limit = 50)), + * relays = relayStatuses.keys, + * onEvent = { event, _, _, _ -> events.add(event) } + * ) + * } + * ``` + */ +@Composable +fun rememberSubscription( + vararg keys: Any?, + relayManager: RelayConnectionManager, + config: () -> SubscriptionConfig?, +): SubscriptionHandle? { + val subscription = remember(*keys) { config() } + + DisposableEffect(*keys, subscription?.subId) { + subscription?.let { cfg -> + if (cfg.relays.isNotEmpty()) { + relayManager.subscribe( + subId = cfg.subId, + filters = cfg.filters, + relays = cfg.relays, + listener = + object : IRequestListener { + override fun onEvent( + event: Event, + isLive: Boolean, + relay: NormalizedRelayUrl, + forFilters: List?, + ) { + cfg.onEvent(event, isLive, relay, forFilters) + } + + override fun onEose( + relay: NormalizedRelayUrl, + forFilters: List?, + ) { + cfg.onEose(relay, forFilters) + } + }, + ) + } + } + + onDispose { + subscription?.let { relayManager.unsubscribe(it.subId) } + } + } + + return subscription?.let { SubscriptionHandle(it.subId, { relayManager.unsubscribe(it.subId) }) } +} + +/** + * Generates a unique subscription ID with timestamp. + */ +fun generateSubId(prefix: String): String = "$prefix-${System.currentTimeMillis()}" diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts index 5bd8c6c60..b82eb646b 100644 --- a/desktopApp/build.gradle.kts +++ b/desktopApp/build.gradle.kts @@ -11,6 +11,10 @@ sourceSets { kotlin.srcDir("src/jvmMain/kotlin") resources.srcDir("src/jvmMain/resources") } + test { + kotlin.srcDir("src/jvmTest/kotlin") + resources.srcDir("src/jvmTest/resources") + } } kotlin { @@ -40,6 +44,11 @@ dependencies { // Collections implementation(libs.kotlinx.collections.immutable) + + // Testing + testImplementation(libs.kotlin.test) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.okhttp) } compose.desktop { diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt index ef027ebbb..b162357e6 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt @@ -42,7 +42,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -54,16 +53,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.account.AccountState import com.vitorpamplona.amethyst.commons.state.EventCollectionState +import com.vitorpamplona.amethyst.commons.subscriptions.FeedMode +import com.vitorpamplona.amethyst.commons.subscriptions.createContactListSubscription +import com.vitorpamplona.amethyst.commons.subscriptions.createFollowingFeedSubscription +import com.vitorpamplona.amethyst.commons.subscriptions.createGlobalFeedSubscription +import com.vitorpamplona.amethyst.commons.subscriptions.rememberSubscription import com.vitorpamplona.amethyst.commons.ui.components.LoadingState import com.vitorpamplona.amethyst.commons.ui.note.NoteCard import com.vitorpamplona.amethyst.commons.util.toNoteDisplayData import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener -import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent /** * Note card with action buttons. @@ -95,11 +95,6 @@ fun FeedNoteCard( } } -enum class FeedMode { - GLOBAL, - FOLLOWING, -} - @Composable fun FeedScreen( relayManager: DesktopRelayConnectionManager, @@ -125,113 +120,47 @@ fun FeedScreen( var followedUsers by remember { mutableStateOf>(emptySet()) } // Load followed users for Following feed mode - DisposableEffect(relayStatuses, account, feedMode) { + rememberSubscription(relayStatuses, account, feedMode, relayManager = relayManager) { val configuredRelays = relayStatuses.keys if (configuredRelays.isNotEmpty() && account != null && feedMode == FeedMode.FOLLOWING) { - val contactListSubId = "feed-contacts-${account.pubKeyHex}-${System.currentTimeMillis()}" - relayManager.subscribe( - subId = contactListSubId, - filters = - listOf( - Filter( - kinds = listOf(ContactListEvent.KIND), - authors = listOf(account.pubKeyHex), - limit = 1, - ), - ), + createContactListSubscription( relays = configuredRelays, - listener = - object : IRequestListener { - override fun onEvent( - event: Event, - isLive: Boolean, - relay: NormalizedRelayUrl, - forFilters: List?, - ) { - if (event is ContactListEvent) { - followedUsers = event.verifiedFollowKeySet() - } - } - - override fun onEose( - relay: NormalizedRelayUrl, - forFilters: List?, - ) {} - }, + pubKeyHex = account.pubKeyHex, + onEvent = { event, _, _, _ -> + if (event is ContactListEvent) { + followedUsers = event.verifiedFollowKeySet() + } + }, ) - - onDispose { - relayManager.unsubscribe(contactListSubId) - } } else { - onDispose {} + null } } - DisposableEffect(relayStatuses, feedMode, followedUsers) { + // Clear events when feed mode changes + remember(feedMode) { eventState.clear() } + + // Subscribe to feed based on mode + rememberSubscription(relayStatuses, feedMode, followedUsers, relayManager = relayManager) { val configuredRelays = relayStatuses.keys - if (configuredRelays.isNotEmpty()) { - // Clear previous events when switching modes - eventState.clear() + if (configuredRelays.isEmpty()) return@rememberSubscription null - val subId = "${feedMode.name.lowercase()}-feed-${System.currentTimeMillis()}" - val filters = - when (feedMode) { - FeedMode.GLOBAL -> - listOf( - Filter( - kinds = listOf(TextNoteEvent.KIND), - limit = 50, - ), - ) - FeedMode.FOLLOWING -> - if (followedUsers.isNotEmpty()) { - listOf( - Filter( - kinds = listOf(TextNoteEvent.KIND), - authors = followedUsers.toList(), - limit = 50, - ), - ) - } else { - // No followed users yet, return empty filter - emptyList() - } - } - - if (filters.isNotEmpty()) { - relayManager.subscribe( - subId = subId, - filters = filters, + when (feedMode) { + FeedMode.GLOBAL -> + createGlobalFeedSubscription( relays = configuredRelays, - listener = - object : IRequestListener { - override fun onEvent( - event: Event, - isLive: Boolean, - relay: NormalizedRelayUrl, - forFilters: List?, - ) { - eventState.addItem(event) - } - - override fun onEose( - relay: NormalizedRelayUrl, - forFilters: List?, - ) { - // End of stored events - } - }, + onEvent = { event, _, _, _ -> eventState.addItem(event) }, ) - - onDispose { - relayManager.unsubscribe(subId) + FeedMode.FOLLOWING -> + if (followedUsers.isNotEmpty()) { + createFollowingFeedSubscription( + relays = configuredRelays, + followedUsers = followedUsers.toList(), + onEvent = { event, _, _, _ -> eventState.addItem(event) }, + ) + } else { + null } - } else { - onDispose {} - } - } else { - onDispose {} } } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt index 336496408..772f05219 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt @@ -39,7 +39,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -52,15 +51,14 @@ import com.vitorpamplona.amethyst.commons.icons.Reply import com.vitorpamplona.amethyst.commons.icons.Repost import com.vitorpamplona.amethyst.commons.icons.Zap import com.vitorpamplona.amethyst.commons.state.EventCollectionState +import com.vitorpamplona.amethyst.commons.subscriptions.createNotificationsSubscription +import com.vitorpamplona.amethyst.commons.subscriptions.rememberSubscription import com.vitorpamplona.amethyst.commons.ui.components.LoadingState import com.vitorpamplona.amethyst.commons.ui.feed.FeedHeader import com.vitorpamplona.amethyst.commons.util.toTimeAgo import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull -import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener -import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent @@ -122,96 +120,57 @@ fun NotificationsScreen( } val notifications by notificationState.items.collectAsState() - DisposableEffect(relayStatuses, account.pubKeyHex) { + // Subscribe to notifications + rememberSubscription(relayStatuses, account.pubKeyHex, relayManager = relayManager) { val configuredRelays = relayStatuses.keys if (configuredRelays.isNotEmpty()) { - val subId = "notifications-${account.pubKeyHex}-${System.currentTimeMillis()}" - val filters = - listOf( - // Mentions, replies, reactions, reposts, zaps - Filter( - kinds = - listOf( - TextNoteEvent.KIND, // 1 - mentions/replies - ReactionEvent.KIND, // 7 - reactions - RepostEvent.KIND, // 6 - reposts - GenericRepostEvent.KIND, // 16 - generic reposts - LnZapEvent.KIND, // 9735 - zaps - ), - tags = mapOf("p" to listOf(account.pubKeyHex)), // Events mentioning user - limit = 100, - ), - ) - - relayManager.subscribe( - subId = subId, - filters = filters, + createNotificationsSubscription( relays = configuredRelays, - listener = - object : IRequestListener { - override fun onEvent( - event: Event, - isLive: Boolean, - relay: NormalizedRelayUrl, - forFilters: List?, - ) { - // Skip events from the user themselves (except zaps) - if (event.pubKey == account.pubKeyHex && event !is LnZapEvent) { - return + pubKeyHex = account.pubKeyHex, + onEvent = { event, _, _, _ -> + // Skip events from the user themselves (except zaps) + if (event.pubKey == account.pubKeyHex && event !is LnZapEvent) { + return@createNotificationsSubscription + } + + val notification = + when (event) { + is ReactionEvent -> + NotificationItem.Reaction( + event = event, + timestamp = event.createdAt, + content = event.content, + ) + is RepostEvent, is GenericRepostEvent -> + NotificationItem.Repost( + event = event, + timestamp = event.createdAt, + ) + is LnZapEvent -> { + val amount = event.amount?.toLong() + NotificationItem.Zap( + event = event, + timestamp = event.createdAt, + amount = amount, + ) } - - val notification = - when (event) { - is ReactionEvent -> - NotificationItem.Reaction( - event = event, - timestamp = event.createdAt, - content = event.content, - ) - is RepostEvent, is GenericRepostEvent -> - NotificationItem.Repost( - event = event, - timestamp = event.createdAt, - ) - is LnZapEvent -> { - // Extract amount from zap (simplified - full parsing in production) - val amount = event.amount?.toLong() - NotificationItem.Zap( - event = event, - timestamp = event.createdAt, - amount = amount, - ) - } - is TextNoteEvent -> { - // Check if it's a reply (has e-tag) or mention - val eTags = event.tags.filter { it.size > 1 && it[0] == "e" } - val isReply = eTags.isNotEmpty() - if (isReply) { - NotificationItem.Reply(event, event.createdAt) - } else { - NotificationItem.Mention(event, event.createdAt) - } - } - else -> NotificationItem.Mention(event, event.createdAt) + is TextNoteEvent -> { + val eTags = event.tags.filter { it.size > 1 && it[0] == "e" } + val isReply = eTags.isNotEmpty() + if (isReply) { + NotificationItem.Reply(event, event.createdAt) + } else { + NotificationItem.Mention(event, event.createdAt) } - - notificationState.addItem(notification) + } + else -> NotificationItem.Mention(event, event.createdAt) } - override fun onEose( - relay: NormalizedRelayUrl, - forFilters: List?, - ) { - // End of stored events - } - }, + notificationState.addItem(notification) + }, ) - - onDispose { - relayManager.unsubscribe(subId) - } } else { - onDispose { } + null } } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt index 52ae95e56..41402280c 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt @@ -48,7 +48,6 @@ import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -64,15 +63,15 @@ import com.vitorpamplona.amethyst.commons.account.AccountState import com.vitorpamplona.amethyst.commons.actions.FollowAction import com.vitorpamplona.amethyst.commons.state.EventCollectionState import com.vitorpamplona.amethyst.commons.state.FollowState +import com.vitorpamplona.amethyst.commons.subscriptions.createContactListSubscription +import com.vitorpamplona.amethyst.commons.subscriptions.createMetadataSubscription +import com.vitorpamplona.amethyst.commons.subscriptions.createUserPostsSubscription +import com.vitorpamplona.amethyst.commons.subscriptions.rememberSubscription import com.vitorpamplona.amethyst.commons.ui.components.LoadingState import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull -import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener -import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip19Bech32.toNpub import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -124,148 +123,71 @@ fun UserProfileScreen( } // Load current user's contact list (for follow state) - DisposableEffect(relayStatuses, account) { + rememberSubscription(relayStatuses, account, relayManager = relayManager) { val configuredRelays = relayStatuses.keys if (configuredRelays.isNotEmpty() && account != null) { - val contactListSubId = "my-contacts-${account.pubKeyHex}-${System.currentTimeMillis()}" - relayManager.subscribe( - subId = contactListSubId, - filters = - listOf( - Filter( - kinds = listOf(ContactListEvent.KIND), // Kind 3 - authors = listOf(account.pubKeyHex), - limit = 1, - ), - ), + createContactListSubscription( relays = configuredRelays, - listener = - object : IRequestListener { - override fun onEvent( - event: Event, - isLive: Boolean, - relay: NormalizedRelayUrl, - forFilters: List?, - ) { - if (event is ContactListEvent) { - followState.updateContactList(event, pubKeyHex) - } - } - - override fun onEose( - relay: NormalizedRelayUrl, - forFilters: List?, - ) {} - }, + pubKeyHex = account.pubKeyHex, + onEvent = { event, _, _, _ -> + if (event is ContactListEvent) { + followState.updateContactList(event, pubKeyHex) + } + }, ) - - onDispose { - relayManager.unsubscribe(contactListSubId) - } } else { - onDispose {} + null } } - // Subscribe to user metadata and posts - DisposableEffect(relayStatuses, pubKeyHex, retryTrigger) { + // Clear posts when profile changes + remember(pubKeyHex, retryTrigger) { + eventState.clear() + postsLoading = true + postsError = null + } + + // Subscribe to user metadata + rememberSubscription(relayStatuses, pubKeyHex, retryTrigger, relayManager = relayManager) { val configuredRelays = relayStatuses.keys if (configuredRelays.isNotEmpty()) { - postsLoading = true - postsError = null - eventState.clear() - // Metadata subscription (kind 0) - val metadataSubId = "profile-metadata-$pubKeyHex-${System.currentTimeMillis()}" - relayManager.subscribe( - subId = metadataSubId, - filters = - listOf( - Filter( - kinds = listOf(0), // Metadata - authors = listOf(pubKeyHex), - limit = 1, - ), - ), + createMetadataSubscription( relays = configuredRelays, - listener = - object : IRequestListener { - override fun onEvent( - event: Event, - isLive: Boolean, - relay: NormalizedRelayUrl, - forFilters: List?, - ) { - // Parse metadata JSON (simplified - full parsing in production) - try { - val content = event.content - displayName = extractJsonField(content, "display_name") ?: extractJsonField(content, "name") - about = extractJsonField(content, "about") - picture = extractJsonField(content, "picture") - } catch (e: Exception) { - // Ignore parse errors - } - } - - override fun onEose( - relay: NormalizedRelayUrl, - forFilters: List?, - ) {} - }, - ) - - // Posts subscription (kind 1) - val postsSubId = "profile-posts-$pubKeyHex-${System.currentTimeMillis()}" - relayManager.subscribe( - subId = postsSubId, - filters = - listOf( - Filter( - kinds = listOf(TextNoteEvent.KIND), - authors = listOf(pubKeyHex), - limit = 50, - ), - ), - relays = configuredRelays, - listener = - object : IRequestListener { - override fun onEvent( - event: Event, - isLive: Boolean, - relay: NormalizedRelayUrl, - forFilters: List?, - ) { - eventState.addItem(event) - } - - override fun onEose( - relay: NormalizedRelayUrl, - forFilters: List?, - ) { - // At least one relay finished sending events - postsLoading = false - } - }, - ) - - // Set timeout for loading state - val timeoutJob = - kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.Default).launch { - kotlinx.coroutines.delay(10000) // 10 second timeout - if (postsLoading) { - postsError = "Request timed out. Check relay connections." - postsLoading = false + pubKeyHex = pubKeyHex, + onEvent = { event, _, _, _ -> + try { + val content = event.content + displayName = extractJsonField(content, "display_name") ?: extractJsonField(content, "name") + about = extractJsonField(content, "about") + picture = extractJsonField(content, "picture") + } catch (e: Exception) { + // Ignore parse errors } - } + }, + ) + } else { + null + } + } - onDispose { - timeoutJob.cancel() - relayManager.unsubscribe(metadataSubId) - relayManager.unsubscribe(postsSubId) - } + // Subscribe to user posts + rememberSubscription(relayStatuses, pubKeyHex, retryTrigger, relayManager = relayManager) { + val configuredRelays = relayStatuses.keys + if (configuredRelays.isNotEmpty()) { + createUserPostsSubscription( + relays = configuredRelays, + pubKeyHex = pubKeyHex, + onEvent = { event, _, _, _ -> + eventState.addItem(event) + }, + onEose = { _, _ -> + postsLoading = false + }, + ) } else { postsLoading = false postsError = "No relays configured" - onDispose {} + null } } diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopHttpClientTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopHttpClientTest.kt new file mode 100644 index 000000000..ef29957c0 --- /dev/null +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopHttpClientTest.kt @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.desktop.network + +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class DesktopHttpClientTest { + @Test + fun testGetHttpClientReturnsConfiguredClient() { + val url = NormalizedRelayUrl("wss://relay.damus.io") + val client = DesktopHttpClient.getHttpClient(url) + + assertNotNull(client) + assertEquals(30_000, client.connectTimeoutMillis) + assertEquals(30_000, client.readTimeoutMillis) + assertEquals(30_000, client.writeTimeoutMillis) + assertEquals(30_000, client.pingIntervalMillis) + assertTrue(client.retryOnConnectionFailure) + } + + @Test + fun testGetHttpClientReturnsSameInstance() { + val url1 = NormalizedRelayUrl("wss://relay.damus.io") + val url2 = NormalizedRelayUrl("wss://nos.lol") + + val client1 = DesktopHttpClient.getHttpClient(url1) + val client2 = DesktopHttpClient.getHttpClient(url2) + + // Should return the same singleton instance + assertEquals(client1, client2) + } + + @Test + fun testHttpClientHasExpectedTimeouts() { + val url = NormalizedRelayUrl("wss://relay.nostr.band") + val client = DesktopHttpClient.getHttpClient(url) + + // Verify all timeouts are 30 seconds + assertEquals(30_000, client.connectTimeoutMillis) + assertEquals(30_000, client.readTimeoutMillis) + assertEquals(30_000, client.writeTimeoutMillis) + assertEquals(30_000, client.pingIntervalMillis) + } + + @Test + fun testHttpClientHasRetryEnabled() { + val url = NormalizedRelayUrl("wss://relay.snort.social") + val client = DesktopHttpClient.getHttpClient(url) + + assertTrue(client.retryOnConnectionFailure, "Retry on connection failure should be enabled") + } + + @Test + fun testHttpClientIsLazyInitialized() { + // This test verifies the lazy initialization pattern + // The client should be created only once even with multiple calls + val url1 = NormalizedRelayUrl("wss://relay1.example.com") + val url2 = NormalizedRelayUrl("wss://relay2.example.com") + val url3 = NormalizedRelayUrl("wss://relay3.example.com") + + val client1 = DesktopHttpClient.getHttpClient(url1) + val client2 = DesktopHttpClient.getHttpClient(url2) + val client3 = DesktopHttpClient.getHttpClient(url3) + + // All should be the same instance due to lazy singleton + assertEquals(client1, client2) + assertEquals(client2, client3) + assertEquals(client1, client3) + } +} diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopRelayConnectionManagerTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopRelayConnectionManagerTest.kt new file mode 100644 index 000000000..3075e610a --- /dev/null +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopRelayConnectionManagerTest.kt @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.desktop.network + +import kotlin.test.Test +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class DesktopRelayConnectionManagerTest { + @Test + fun testRelayConnectionManagerCanBeInstantiated() { + val manager = DesktopRelayConnectionManager() + assertNotNull(manager) + } + + @Test + fun testRelayConnectionManagerHasNoActiveConnectionsInitially() { + val manager = DesktopRelayConnectionManager() + val connectedRelays = manager.connectedRelays.value + val availableRelays = manager.availableRelays.value + assertTrue(connectedRelays.isEmpty(), "Should have no connected relays on initialization") + assertTrue(availableRelays.isEmpty(), "Should have no available relays on initialization") + } + + @Test + fun testRelayConnectionManagerInheritsFromBaseClass() { + val manager = DesktopRelayConnectionManager() + assertTrue( + manager is com.vitorpamplona.amethyst.commons.network.RelayConnectionManager, + "DesktopRelayConnectionManager should extend RelayConnectionManager", + ) + } +} From 64eec9844bb31e4cff0ffc61b47112fca6e068ef Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Sat, 3 Jan 2026 13:00:02 +0200 Subject: [PATCH 054/137] bugfixes --- .../commons/state/EventCollectionState.kt | 2 +- .../amethyst/desktop/DebugConfig.kt | 59 ++++ .../vitorpamplona/amethyst/desktop/Main.kt | 40 ++- .../amethyst/desktop/ui/DevSettingsSection.kt | 261 ++++++++++++++++++ .../amethyst/desktop/DebugConfigTest.kt | 55 ++++ 5 files changed, 406 insertions(+), 11 deletions(-) create mode 100644 desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/DebugConfig.kt create mode 100644 desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/DevSettingsSection.kt create mode 100644 desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/DebugConfigTest.kt diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/state/EventCollectionState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/state/EventCollectionState.kt index 9307da691..daeabe2fe 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/state/EventCollectionState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/state/EventCollectionState.kt @@ -200,7 +200,7 @@ class EventCollectionState( // Sort if comparator provided, otherwise keep newest first (pending items already at end) val sorted = if (sortComparator != null) { - merged.sortedWith(sortComparator) + merged.sortedWith(sortComparator).distinctBy { getId(it) } } else { // Reverse so newest (pending) items come first (pendingItems.reversed() + _items.value).distinctBy { getId(it) } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/DebugConfig.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/DebugConfig.kt new file mode 100644 index 000000000..85a88fde5 --- /dev/null +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/DebugConfig.kt @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.desktop + +/** + * Debug configuration for the desktop application. + * + * To enable debug mode, set the environment variable: + * AMETHYST_DEBUG=true + * + * Or run with: + * ./gradlew :desktopApp:run -PamethystDebug=true + */ +object DebugConfig { + /** + * Enables developer settings and debug features. + * + * When true, shows: + * - Raw nsec/npub in settings + * - Additional logging + * - Debug UI elements + */ + val isDebugMode: Boolean by lazy { + // Check environment variable + val envDebug = System.getenv("AMETHYST_DEBUG")?.toBoolean() ?: false + + // Check system property (for gradle -PamethystDebug=true) + val propDebug = System.getProperty("amethyst.debug")?.toBoolean() ?: false + + envDebug || propDebug + } + + /** + * Log debug information. + */ + fun log(message: String) { + if (isDebugMode) { + println("[DEBUG] $message") + } + } +} diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt index 898dac477..b328301a6 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt @@ -91,21 +91,21 @@ private val isMacOS = System.getProperty("os.name").lowercase().contains("mac") * Desktop navigation state - extends AppScreen with dynamic destinations. */ sealed class DesktopScreen { - data object Feed : DesktopScreen() + object Feed : DesktopScreen() - data object Search : DesktopScreen() + object Search : DesktopScreen() - data object Messages : DesktopScreen() + object Messages : DesktopScreen() - data object Notifications : DesktopScreen() + object Notifications : DesktopScreen() - data object MyProfile : DesktopScreen() + object MyProfile : DesktopScreen() data class UserProfile( val pubKeyHex: String, ) : DesktopScreen() - data object Settings : DesktopScreen() + object Settings : DesktopScreen() } fun main() = @@ -365,7 +365,7 @@ fun MainContent( onScreenChange(DesktopScreen.UserProfile(pubKeyHex)) }, ) - DesktopScreen.Settings -> RelaySettingsScreen(relayManager) + DesktopScreen.Settings -> RelaySettingsScreen(relayManager, account) } } } @@ -405,17 +405,37 @@ fun ProfileScreen( } @Composable -fun RelaySettingsScreen(relayManager: DesktopRelayConnectionManager) { +fun RelaySettingsScreen( + relayManager: DesktopRelayConnectionManager, + account: AccountState.LoggedIn, +) { val relayStatuses by relayManager.relayStatuses.collectAsState() val connectedRelays by relayManager.connectedRelays.collectAsState() var newRelayUrl by remember { mutableStateOf("") } Column(modifier = Modifier.fillMaxSize()) { Text( - "Relay Settings", + "Settings", style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.onBackground, ) + + Spacer(Modifier.height(24.dp)) + + // Developer Settings Section (only in debug mode) + if (DebugConfig.isDebugMode) { + com.vitorpamplona.amethyst.desktop.ui + .DevSettingsSection(account = account) + Spacer(Modifier.height(24.dp)) + HorizontalDivider() + Spacer(Modifier.height(24.dp)) + } + + Text( + "Relay Settings", + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onBackground, + ) Spacer(Modifier.height(8.dp)) Row( @@ -470,7 +490,7 @@ fun RelaySettingsScreen(relayManager: DesktopRelayConnectionManager) { verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.weight(1f), ) { - items(relayStatuses.values.toList()) { status -> + items(relayStatuses.values.toList(), key = { it.url.url }) { status -> RelayStatusCard( status = status, onRemove = { relayManager.removeRelay(status.url) }, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/DevSettingsSection.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/DevSettingsSection.kt new file mode 100644 index 000000000..6c25bd22d --- /dev/null +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/DevSettingsSection.kt @@ -0,0 +1,261 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.desktop.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ContentCopy +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.account.AccountState +import java.awt.Toolkit +import java.awt.datatransfer.StringSelection + +/** + * Developer settings section - shows sensitive keys for debugging. + * Only enable in debug builds or with explicit debug flag. + */ +@Composable +fun DevSettingsSection( + account: AccountState.LoggedIn, + modifier: Modifier = Modifier, +) { + var showKeys by remember { mutableStateOf(false) } + + Column( + modifier = + modifier + .fillMaxWidth() + .border( + width = 2.dp, + color = Color(0xFFFF9800), // Orange warning color + shape = RoundedCornerShape(8.dp), + ).background( + color = Color(0xFF332200), // Dark orange tint + shape = RoundedCornerShape(8.dp), + ).padding(16.dp), + ) { + // Warning header + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + Icons.Default.Warning, + contentDescription = "Warning", + tint = Color(0xFFFF9800), + ) + Text( + "Developer Settings", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = Color(0xFFFF9800), + ) + } + + Spacer(Modifier.height(8.dp)) + + Text( + "Debug mode only - Handle keys with care", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + Spacer(Modifier.height(16.dp)) + + HorizontalDivider(color = Color(0xFFFF9800).copy(alpha = 0.3f)) + + Spacer(Modifier.height(16.dp)) + + // Toggle button + OutlinedButton( + onClick = { showKeys = !showKeys }, + colors = + ButtonDefaults.outlinedButtonColors( + contentColor = Color(0xFFFF9800), + ), + ) { + Text(if (showKeys) "Hide Keys" else "Show Keys") + } + + if (showKeys) { + Spacer(Modifier.height(16.dp)) + + // npub + KeyRow( + label = "Public Key (npub)", + value = account.npub, + isSensitive = false, + ) + + Spacer(Modifier.height(12.dp)) + + // nsec (only if available) + account.nsec?.let { nsec -> + KeyRow( + label = "Private Key (nsec)", + value = nsec, + isSensitive = true, + ) + } ?: run { + Text( + "Read-only mode - No private key available", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontStyle = androidx.compose.ui.text.font.FontStyle.Italic, + ) + } + + Spacer(Modifier.height(12.dp)) + + // Hex key + KeyRow( + label = "Public Key (hex)", + value = account.pubKeyHex, + isSensitive = false, + ) + } + } +} + +@Composable +private fun KeyRow( + label: String, + value: String, + isSensitive: Boolean, + modifier: Modifier = Modifier, +) { + var copiedRecently by remember { mutableStateOf(false) } + + Column(modifier = modifier.fillMaxWidth()) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + label, + style = MaterialTheme.typography.labelMedium, + color = + if (isSensitive) { + Color(0xFFFF5252) + } else { + MaterialTheme.colorScheme.onSurface + }, + fontWeight = FontWeight.Bold, + ) + + Button( + onClick = { + copyToClipboard(value) + copiedRecently = true + }, + colors = + ButtonDefaults.buttonColors( + containerColor = + if (copiedRecently) { + Color(0xFF4CAF50) + } else { + MaterialTheme.colorScheme.primaryContainer + }, + ), + ) { + Icon( + Icons.Default.ContentCopy, + contentDescription = "Copy", + modifier = Modifier.padding(end = 4.dp), + ) + Text(if (copiedRecently) "Copied!" else "Copy") + } + } + + Spacer(Modifier.height(4.dp)) + + // Display the key in monospace + Text( + value, + style = + MaterialTheme.typography.bodySmall.copy( + fontFamily = FontFamily.Monospace, + ), + color = + if (isSensitive) { + Color(0xFFFF5252) + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + modifier = + Modifier + .fillMaxWidth() + .background( + color = Color(0xFF1A1A1A), + shape = RoundedCornerShape(4.dp), + ).padding(8.dp), + ) + + // Reset copied state after a delay + if (copiedRecently) { + androidx.compose.runtime.LaunchedEffect(Unit) { + kotlinx.coroutines.delay(2000) + copiedRecently = false + } + } + } +} + +/** + * Copy text to system clipboard using AWT Toolkit. + */ +private fun copyToClipboard(text: String) { + try { + val clipboard = Toolkit.getDefaultToolkit().systemClipboard + val selection = StringSelection(text) + clipboard.setContents(selection, selection) + } catch (e: Exception) { + e.printStackTrace() + } +} diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/DebugConfigTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/DebugConfigTest.kt new file mode 100644 index 000000000..90e2b34d4 --- /dev/null +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/DebugConfigTest.kt @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.desktop + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class DebugConfigTest { + @Test + fun testDebugModeCanBeQueried() { + // This test verifies that DebugConfig.isDebugMode is accessible + // The actual value depends on environment/system properties + val debugMode = DebugConfig.isDebugMode + // Just verify it returns a boolean (true or false) + assertTrue(debugMode || !debugMode, "isDebugMode should return a boolean value") + } + + @Test + fun testDebugLogDoesNotThrow() { + // Verify debug logging doesn't throw exceptions + try { + DebugConfig.log("Test debug message") + assertTrue(true) + } catch (e: Exception) { + assertFalse(true, "Debug log should not throw exceptions: ${e.message}") + } + } + + @Test + fun testDebugConfigIsObject() { + // Verify DebugConfig is a singleton object + val instance1 = DebugConfig + val instance2 = DebugConfig + assertTrue(instance1 === instance2, "DebugConfig should be a singleton object") + } +} From c878b07458869d3d81394ec35fd5b94241a2ef5f Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Sat, 3 Jan 2026 19:54:51 +0000 Subject: [PATCH 055/137] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-hu-rHU/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index fda052418..b64c49bbf 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -158,6 +158,7 @@ Hangüzenet rögzítése Hangüzenet rögzítése Hangüzenet rögzítéséhez kattintson és tartsa lenyomva a gombot + Újrarögzítés Rögzítés %1$s rögzítése Feltöltés… From d7888c57fa0e8c59f1f9fd6b0650e58faa8d61ff Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 3 Jan 2026 18:18:36 -0500 Subject: [PATCH 056/137] Creating additional configuration options and amplifying tests to a combination of all those options --- .../nip01Core/store/sqlite/BaseDBTest.kt | 43 +- .../nip01Core/store/sqlite/DeletionTest.kt | 42 +- .../store/sqlite/QueryAssemblerTest.kt | 1578 ++++++++++------- .../store/sqlite/DeletionRequestModule.kt | 35 +- .../store/sqlite/EventIndexesModule.kt | 19 +- .../store/sqlite/IndexingStrategy.kt | 61 +- .../nip01Core/store/sqlite/QueryBuilder.kt | 2 +- 7 files changed, 1105 insertions(+), 675 deletions(-) diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/BaseDBTest.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/BaseDBTest.kt index a1fe2259a..6926af8de 100644 --- a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/BaseDBTest.kt +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/BaseDBTest.kt @@ -26,19 +26,46 @@ import org.junit.After import org.junit.Before open class BaseDBTest { - private lateinit var dbs: Map + private lateinit var dbs: MutableMap + + fun DefaultIndexingStrategy.name(): String = + """ + indexEventsByCreatedAtAlone=$indexEventsByCreatedAtAlone + indexTagsByCreatedAtAlone=$indexTagsByCreatedAtAlone + indexTagsWithKindAndPubkey=$indexTagsWithKindAndPubkey + useAndIndexIdOnOrderBy=$useAndIndexIdOnOrderBy + """.trimIndent() @Before fun setup() { val context = ApplicationProvider.getApplicationContext() - dbs = - mapOf( - "Default" to EventStore(context, null, indexStrategy = DefaultIndexingStrategy(false, false)), - "IndexAll" to EventStore(context, null, indexStrategy = DefaultIndexingStrategy(true, false)), - "Order by ID" to EventStore(context, null, indexStrategy = DefaultIndexingStrategy(false, true)), - "IndexAll, Order by ID" to EventStore(context, null, indexStrategy = DefaultIndexingStrategy(true, true)), - ) + val booleans = listOf(true, false) + + dbs = mutableMapOf() + + // tests all possible DBs + for (indexEventsByCreatedAtAlone in booleans) { + for (indexTagsByCreatedAtAlone in booleans) { + for (indexTagsWithKindAndPubkey in booleans) { + for (useAndIndexIdOnOrderBy in booleans) { + val indexStrategy = + DefaultIndexingStrategy( + indexEventsByCreatedAtAlone, + indexTagsByCreatedAtAlone, + indexTagsWithKindAndPubkey, + useAndIndexIdOnOrderBy, + ) + dbs[indexStrategy.name()] = + EventStore( + context = context, + dbName = null, + indexStrategy = indexStrategy, + ) + } + } + } + } } @After diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionTest.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionTest.kt index 9d1ffc3c3..0a0d9b90a 100644 --- a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionTest.kt +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionTest.kt @@ -208,26 +208,32 @@ class DeletionTest : BaseDBTest() { @Test fun testTriggersIndexUsage() = forEachDB { db -> - val sql = - """ - SELECT 1 FROM event_tags - WHERE - event_tags.tag_hash IN (3221122, 223322) AND - event_tags.kind = 5 AND - event_tags.pubkey_hash = 22332323 AND - event_tags.created_at >= 1766686500 - """.trimIndent() + var sql = db.store.deletionModule.rejectDeletedEventsSQLTemplate() - val explainer = - db.store.explainQuery(sql) + sql = sql.replace("NEW.etag_hash", "3221122") + sql = sql.replace("NEW.atag_hash", "223322") + sql = sql.replace("NEW.pubkey_owner_hash", "22332323") + sql = sql.replace("NEW.created_at", "1766686500") - TestCase.assertEquals( - """ - ${sql.replace("\n","\n ")} - └── SEARCH event_tags USING COVERING INDEX query_by_tags_hash_kind_pubkey (tag_hash=? AND kind=? AND pubkey_hash=? AND created_at>?) - """.trimIndent(), - explainer, - ) + val explainer = db.store.explainQuery(sql) + + if (db.indexStrategy.indexTagsWithKindAndPubkey) { + TestCase.assertEquals( + """ + |$sql + |└── SEARCH event_tags USING COVERING INDEX query_by_tags_hash_kind_pubkey (tag_hash=? AND kind=? AND pubkey_hash=? AND created_at>?) + """.trimMargin(), + explainer, + ) + } else { + TestCase.assertEquals( + """ + |$sql + |└── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=? AND kind=? AND created_at>?) + """.trimMargin(), + explainer, + ) + } } @Test diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt index 52d4c5423..3dd656c27 100644 --- a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt @@ -20,8 +20,6 @@ */ package com.vitorpamplona.quartz.nip01Core.store.sqlite -import android.content.Context -import androidx.test.core.app.ApplicationProvider import com.vitorpamplona.quartz.experimental.relationshipStatus.ContactCardEvent import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter @@ -32,665 +30,991 @@ import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import junit.framework.TestCase import junit.framework.TestCase.assertEquals -import org.junit.After import org.junit.Assert -import org.junit.Before import org.junit.Test -class QueryAssemblerTest { +class QueryAssemblerTest : BaseDBTest() { val hasher = TagNameValueHasher(0) val key1 = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d" val key2 = "f3ac434d61bc0f491a814782ccfdf9c439dae1f0bde9097ad4a245f4c495cd14" val key3 = "12ae0fd81c85e1e7d9ed096397dc3129849425fe6f8afce7213ebf38ddfc6ca9" - private lateinit var dbAllIndexes: EventStore - private lateinit var dbNoKindIndexes: EventStore - - @Before - fun setup() { - val context = ApplicationProvider.getApplicationContext() - dbAllIndexes = EventStore(context, null, indexStrategy = DefaultIndexingStrategy(true)) - dbNoKindIndexes = EventStore(context, null, indexStrategy = DefaultIndexingStrategy(false)) - } - - @After - fun tearDown() { - dbAllIndexes.close() - dbNoKindIndexes.close() - } - fun EventStore.explain(f: Filter) = store.queryBuilder.planQuery(f, hasher, store.readableDatabase) fun EventStore.explain(f: List) = store.queryBuilder.planQuery(f, hasher, store.readableDatabase) @Test - fun testEmpty() { - Assert.assertEquals( - """ - SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - ORDER BY created_at DESC - └── SCAN event_headers USING INDEX query_by_created_at_id - """.trimIndent(), - dbAllIndexes.explain(Filter()), - ) - - Assert.assertEquals( - """ - SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - ORDER BY created_at DESC - └── SCAN event_headers USING INDEX query_by_created_at_id - """.trimIndent(), - dbNoKindIndexes.explain(Filter()), - ) - } + fun testEmpty() = + forEachDB { db -> + val orderBy = if (db.indexStrategy.useAndIndexIdOnOrderBy) "created_at DESC, id ASC" else "created_at DESC" + if (db.indexStrategy.indexEventsByCreatedAtAlone) { + Assert.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + ORDER BY $orderBy + └── SCAN event_headers USING INDEX query_by_created_at_id + """.trimIndent(), + db.explain(Filter()), + ) + } else { + Assert.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + ORDER BY $orderBy + ├── SCAN event_headers + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(Filter()), + ) + } + } @Test - fun testCheckDeletionEventExists() { - val filter = - Filter( - kinds = listOf(5), - authors = listOf(key1), - tags = mapOf("e" to listOf(key2)), - since = 1750889190, - ) - TestCase.assertEquals( - """ - SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "2657743813502222172") AND (event_tags.kind = "5") AND (event_tags.pubkey_hash = "1730514094536529999") AND (event_tags.created_at >= "1750889190") - ) AS filtered - ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC - ├── CO-ROUTINE filtered - │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind_pubkey (tag_hash=? AND kind=? AND pubkey_hash=? AND created_at>?) - │ └── USE TEMP B-TREE FOR DISTINCT - ├── SCAN filtered - ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - └── USE TEMP B-TREE FOR ORDER BY - """.trimIndent(), - dbAllIndexes.explain(filter), - ) - - TestCase.assertEquals( - """ - SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "2657743813502222172") AND (event_tags.kind = "5") AND (event_tags.pubkey_hash = "1730514094536529999") AND (event_tags.created_at >= "1750889190") - ) AS filtered - ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC - ├── CO-ROUTINE filtered - │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind_pubkey (tag_hash=? AND kind=? AND pubkey_hash=? AND created_at>?) - │ └── USE TEMP B-TREE FOR DISTINCT - ├── SCAN filtered - ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - └── USE TEMP B-TREE FOR ORDER BY - """.trimIndent(), - dbNoKindIndexes.explain(filter), - ) - } - - @Test - fun testLimit() { - val filter = Filter(limit = 10) - TestCase.assertEquals( - """ - SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - ORDER BY created_at DESC - LIMIT 10 - └── SCAN event_headers USING INDEX query_by_created_at_id - """.trimIndent(), - dbAllIndexes.explain(filter), - ) - } - - @Test - fun testLimits() { - val filter = listOf(Filter(limit = 10), Filter(limit = 30)) - TestCase.assertEquals( - """ - SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - INNER JOIN ( - SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers ORDER BY event_headers.created_at DESC LIMIT 10) - UNION - SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers ORDER BY event_headers.created_at DESC LIMIT 30) - ) AS filtered - ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC - ├── CO-ROUTINE filtered - │ └── COMPOUND QUERY - │ ├── LEFT-MOST SUBQUERY - │ │ ├── CO-ROUTINE (subquery-1) - │ │ │ └── SCAN event_headers USING COVERING INDEX query_by_created_at_id - │ │ └── SCAN (subquery-1) - │ └── UNION USING TEMP B-TREE - │ ├── CO-ROUTINE (subquery-3) - │ │ └── SCAN event_headers USING COVERING INDEX query_by_created_at_id - │ └── SCAN (subquery-3) - ├── SCAN filtered - ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - └── USE TEMP B-TREE FOR ORDER BY - """.trimIndent(), - dbAllIndexes.explain(filter), - ) - } - - @Test - fun testAllFeatures() { - val filter = - listOf( - Filter(limit = 10), + fun testCheckDeletionEventExists() = + forEachDB { db -> + val filter = Filter( + kinds = listOf(5), authors = listOf(key1), - kinds = listOf(1, 1111), - search = "keywords", - limit = 100, - ), - Filter(kinds = listOf(20), search = "cats", limit = 30), - ) - TestCase.assertEquals( - """ - SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - INNER JOIN ( - SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers ORDER BY event_headers.created_at DESC LIMIT 10) - UNION - SELECT row_id FROM (SELECT event_fts.event_header_row_id as row_id FROM event_fts INNER JOIN event_headers ON event_headers.row_id = event_fts.event_header_row_id WHERE (event_headers.kind IN ("1", "1111")) AND (event_headers.pubkey = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d") AND (event_fts MATCH "keywords") ORDER BY event_headers.created_at DESC LIMIT 100) - UNION - SELECT row_id FROM (SELECT event_fts.event_header_row_id as row_id FROM event_fts INNER JOIN event_headers ON event_headers.row_id = event_fts.event_header_row_id WHERE (event_headers.kind = "20") AND (event_fts MATCH "cats") ORDER BY event_headers.created_at DESC LIMIT 30) - ) AS filtered - ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC - ├── CO-ROUTINE filtered - │ └── COMPOUND QUERY - │ ├── LEFT-MOST SUBQUERY - │ │ ├── CO-ROUTINE (subquery-1) - │ │ │ └── SCAN event_headers USING COVERING INDEX query_by_created_at_id - │ │ └── SCAN (subquery-1) - │ ├── UNION USING TEMP B-TREE - │ │ ├── CO-ROUTINE (subquery-3) - │ │ │ ├── SCAN event_fts VIRTUAL TABLE INDEX 4: - │ │ │ ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - │ │ │ └── USE TEMP B-TREE FOR ORDER BY - │ │ └── SCAN (subquery-3) - │ └── UNION USING TEMP B-TREE - │ ├── CO-ROUTINE (subquery-5) - │ │ ├── SEARCH event_headers USING COVERING INDEX query_by_kind_created (kind=?) - │ │ └── SCAN event_fts VIRTUAL TABLE INDEX 4: - │ └── SCAN (subquery-5) - ├── SCAN filtered - ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - └── USE TEMP B-TREE FOR ORDER BY - """.trimIndent(), - dbAllIndexes.explain(filter), - ) - } + tags = mapOf("e" to listOf(key2)), + since = 1750889190, + ) + val orderBy = if (db.indexStrategy.useAndIndexIdOnOrderBy) "created_at DESC, id ASC" else "created_at DESC" + if (db.indexStrategy.indexTagsWithKindAndPubkey) { + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "2657743813502222172") AND (event_tags.kind = "5") AND (event_tags.pubkey_hash = "1730514094536529999") AND (event_tags.created_at >= "1750889190") + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY $orderBy + ├── CO-ROUTINE filtered + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind_pubkey (tag_hash=? AND kind=? AND pubkey_hash=? AND created_at>?) + │ └── USE TEMP B-TREE FOR DISTINCT + ├── SCAN filtered + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } else { + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "2657743813502222172") AND (event_tags.kind = "5") AND (event_tags.pubkey_hash = "1730514094536529999") AND (event_tags.created_at >= "1750889190") + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY $orderBy + ├── CO-ROUTINE filtered + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=? AND kind=? AND created_at>?) + │ └── USE TEMP B-TREE FOR DISTINCT + ├── SCAN filtered + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } + } @Test - fun testSingleFilterConversionToSimpleQuery() { - val filter = - listOf( + fun testLimit() = + forEachDB { db -> + val filter = Filter(limit = 10) + val orderBy = if (db.indexStrategy.useAndIndexIdOnOrderBy) "created_at DESC, id ASC" else "created_at DESC" + + if (db.indexStrategy.indexEventsByCreatedAtAlone) { + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + ORDER BY $orderBy + LIMIT 10 + └── SCAN event_headers USING INDEX query_by_created_at_id + """.trimIndent(), + db.explain(filter), + ) + } else { + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + ORDER BY $orderBy + LIMIT 10 + ├── SCAN event_headers + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } + } + + @Test + fun testLimits() = + forEachDB { db -> + val filter = listOf(Filter(limit = 10), Filter(limit = 30)) + val orderBy = + if (db.indexStrategy.useAndIndexIdOnOrderBy) { + "created_at DESC, id ASC" + } else { + "created_at DESC" + } + if (db.indexStrategy.indexEventsByCreatedAtAlone) { + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers ORDER BY event_headers.created_at DESC LIMIT 10) + UNION + SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers ORDER BY event_headers.created_at DESC LIMIT 30) + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY $orderBy + ├── CO-ROUTINE filtered + │ └── COMPOUND QUERY + │ ├── LEFT-MOST SUBQUERY + │ │ ├── CO-ROUTINE (subquery-1) + │ │ │ └── SCAN event_headers USING COVERING INDEX query_by_created_at_id + │ │ └── SCAN (subquery-1) + │ └── UNION USING TEMP B-TREE + │ ├── CO-ROUTINE (subquery-3) + │ │ └── SCAN event_headers USING COVERING INDEX query_by_created_at_id + │ └── SCAN (subquery-3) + ├── SCAN filtered + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } else { + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers ORDER BY event_headers.created_at DESC LIMIT 10) + UNION + SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers ORDER BY event_headers.created_at DESC LIMIT 30) + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY $orderBy + ├── CO-ROUTINE filtered + │ └── COMPOUND QUERY + │ ├── LEFT-MOST SUBQUERY + │ │ ├── CO-ROUTINE (subquery-1) + │ │ │ ├── SCAN event_headers USING COVERING INDEX query_by_kind_created + │ │ │ └── USE TEMP B-TREE FOR ORDER BY + │ │ └── SCAN (subquery-1) + │ └── UNION USING TEMP B-TREE + │ ├── CO-ROUTINE (subquery-3) + │ │ ├── SCAN event_headers USING COVERING INDEX query_by_kind_created + │ │ └── USE TEMP B-TREE FOR ORDER BY + │ └── SCAN (subquery-3) + ├── SCAN filtered + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } + } + + @Test + fun testAllFeatures() = + forEachDB { db -> + val filter = + listOf( + Filter(limit = 10), + Filter( + authors = listOf(key1), + kinds = listOf(1, 1111), + search = "keywords", + limit = 100, + ), + Filter(kinds = listOf(20), search = "cats", limit = 30), + ) + val orderBy = if (db.indexStrategy.useAndIndexIdOnOrderBy) "created_at DESC, id ASC" else "created_at DESC" + if (db.indexStrategy.indexEventsByCreatedAtAlone) { + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers ORDER BY event_headers.created_at DESC LIMIT 10) + UNION + SELECT row_id FROM (SELECT event_fts.event_header_row_id as row_id FROM event_fts INNER JOIN event_headers ON event_headers.row_id = event_fts.event_header_row_id WHERE (event_headers.kind IN ("1", "1111")) AND (event_headers.pubkey = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d") AND (event_fts MATCH "keywords") ORDER BY event_headers.created_at DESC LIMIT 100) + UNION + SELECT row_id FROM (SELECT event_fts.event_header_row_id as row_id FROM event_fts INNER JOIN event_headers ON event_headers.row_id = event_fts.event_header_row_id WHERE (event_headers.kind = "20") AND (event_fts MATCH "cats") ORDER BY event_headers.created_at DESC LIMIT 30) + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY $orderBy + ├── CO-ROUTINE filtered + │ └── COMPOUND QUERY + │ ├── LEFT-MOST SUBQUERY + │ │ ├── CO-ROUTINE (subquery-1) + │ │ │ └── SCAN event_headers USING COVERING INDEX query_by_created_at_id + │ │ └── SCAN (subquery-1) + │ ├── UNION USING TEMP B-TREE + │ │ ├── CO-ROUTINE (subquery-3) + │ │ │ ├── SCAN event_fts VIRTUAL TABLE INDEX 4: + │ │ │ ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + │ │ │ └── USE TEMP B-TREE FOR ORDER BY + │ │ └── SCAN (subquery-3) + │ └── UNION USING TEMP B-TREE + │ ├── CO-ROUTINE (subquery-5) + │ │ ├── SEARCH event_headers USING COVERING INDEX query_by_kind_created (kind=?) + │ │ └── SCAN event_fts VIRTUAL TABLE INDEX 4: + │ └── SCAN (subquery-5) + ├── SCAN filtered + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } else { + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers ORDER BY event_headers.created_at DESC LIMIT 10) + UNION + SELECT row_id FROM (SELECT event_fts.event_header_row_id as row_id FROM event_fts INNER JOIN event_headers ON event_headers.row_id = event_fts.event_header_row_id WHERE (event_headers.kind IN ("1", "1111")) AND (event_headers.pubkey = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d") AND (event_fts MATCH "keywords") ORDER BY event_headers.created_at DESC LIMIT 100) + UNION + SELECT row_id FROM (SELECT event_fts.event_header_row_id as row_id FROM event_fts INNER JOIN event_headers ON event_headers.row_id = event_fts.event_header_row_id WHERE (event_headers.kind = "20") AND (event_fts MATCH "cats") ORDER BY event_headers.created_at DESC LIMIT 30) + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY $orderBy + ├── CO-ROUTINE filtered + │ └── COMPOUND QUERY + │ ├── LEFT-MOST SUBQUERY + │ │ ├── CO-ROUTINE (subquery-1) + │ │ │ ├── SCAN event_headers USING COVERING INDEX query_by_kind_created + │ │ │ └── USE TEMP B-TREE FOR ORDER BY + │ │ └── SCAN (subquery-1) + │ ├── UNION USING TEMP B-TREE + │ │ ├── CO-ROUTINE (subquery-3) + │ │ │ ├── SCAN event_fts VIRTUAL TABLE INDEX 4: + │ │ │ ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + │ │ │ └── USE TEMP B-TREE FOR ORDER BY + │ │ └── SCAN (subquery-3) + │ └── UNION USING TEMP B-TREE + │ ├── CO-ROUTINE (subquery-5) + │ │ ├── SEARCH event_headers USING COVERING INDEX query_by_kind_created (kind=?) + │ │ └── SCAN event_fts VIRTUAL TABLE INDEX 4: + │ └── SCAN (subquery-5) + ├── SCAN filtered + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } + } + + @Test + fun testSingleFilterConversionToSimpleQuery() = + forEachDB { db -> + val filter = + listOf( + Filter( + kinds = listOf(ContactListEvent.KIND), + limit = 30, + ), + ) + if (db.indexStrategy.useAndIndexIdOnOrderBy) { + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + WHERE kind = "3" + ORDER BY created_at DESC, id ASC + LIMIT 30 + └── SEARCH event_headers USING INDEX query_by_kind_created (kind=?) + """.trimIndent(), + db.explain(filter), + ) + } else { + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + WHERE kind = "3" + ORDER BY created_at DESC + LIMIT 30 + └── SEARCH event_headers USING INDEX query_by_kind_created (kind=?) + """.trimIndent(), + db.explain(filter), + ) + } + } + + @Test + fun testKinds() = + forEachDB { db -> + val filter = + listOf( + Filter( + kinds = listOf(ContactListEvent.KIND), + limit = 30, + ), + Filter( + kinds = listOf(TextNoteEvent.KIND), + limit = 30, + ), + ) + val orderBy = if (db.indexStrategy.useAndIndexIdOnOrderBy) "created_at DESC, id ASC" else "created_at DESC" + + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers WHERE event_headers.kind = "3" ORDER BY event_headers.created_at DESC LIMIT 30) + UNION + SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers WHERE event_headers.kind = "1" ORDER BY event_headers.created_at DESC LIMIT 30) + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY $orderBy + ├── CO-ROUTINE filtered + │ └── COMPOUND QUERY + │ ├── LEFT-MOST SUBQUERY + │ │ ├── CO-ROUTINE (subquery-1) + │ │ │ └── SEARCH event_headers USING COVERING INDEX query_by_kind_created (kind=?) + │ │ └── SCAN (subquery-1) + │ └── UNION USING TEMP B-TREE + │ ├── CO-ROUTINE (subquery-3) + │ │ └── SEARCH event_headers USING COVERING INDEX query_by_kind_created (kind=?) + │ └── SCAN (subquery-3) + ├── SCAN filtered + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } + + @Test + fun testKindAndDTag() = + forEachDB { db -> + val filter = + listOf( + Filter( + kinds = listOf(ContactListEvent.KIND), + tags = mapOf("d" to listOf("")), + limit = 30, + ), + ) + if (db.indexStrategy.useAndIndexIdOnOrderBy) { + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + WHERE (kind = "3") AND (d_tag = "") + ORDER BY created_at DESC, id ASC + LIMIT 30 + └── SEARCH event_headers USING INDEX query_by_kind_created (kind=?) + """.trimIndent(), + db.explain(filter), + ) + } else { + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + WHERE (kind = "3") AND (d_tag = "") + ORDER BY created_at DESC + LIMIT 30 + └── SEARCH event_headers USING INDEX query_by_kind_created (kind=?) + """.trimIndent(), + db.explain(filter), + ) + } + } + + @Test + fun testFollowersOf() = + forEachDB { db -> + val filter = + listOf( + Filter( + kinds = listOf(ContactListEvent.KIND), + tags = mapOf("p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")), + limit = 30, + ), + ) + val orderBy = if (db.indexStrategy.useAndIndexIdOnOrderBy) "created_at DESC, id ASC" else "created_at DESC" + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") ORDER BY event_tags.created_at DESC LIMIT 30 + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY $orderBy + ├── CO-ROUTINE filtered + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=? AND kind=?) + │ └── USE TEMP B-TREE FOR DISTINCT + ├── SCAN filtered + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } + + @Test + fun testNotificationsOf() = + forEachDB { db -> + val filter = + listOf( + Filter( + tags = mapOf("p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")), + limit = 30, + ), + ) + val orderBy = if (db.indexStrategy.useAndIndexIdOnOrderBy) "created_at DESC, id ASC" else "created_at DESC" + if (db.indexStrategy.indexTagsByCreatedAtAlone) { + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE event_tags.tag_hash = "-4551135004136952885" ORDER BY event_tags.created_at DESC LIMIT 30 + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY $orderBy + ├── CO-ROUTINE filtered + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash (tag_hash=?) + │ └── USE TEMP B-TREE FOR DISTINCT + ├── SCAN filtered + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } else { + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE event_tags.tag_hash = "-4551135004136952885" ORDER BY event_tags.created_at DESC LIMIT 30 + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY $orderBy + ├── CO-ROUTINE filtered + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=?) + │ ├── USE TEMP B-TREE FOR DISTINCT + │ └── USE TEMP B-TREE FOR ORDER BY + ├── SCAN filtered + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } + } + + @Test + fun testTagsAndKinds() = + forEachDB { db -> + val filter = + listOf( + Filter( + kinds = listOf(ContactListEvent.KIND), + tags = mapOf("p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")), + limit = 30, + ), + ) + val orderBy = if (db.indexStrategy.useAndIndexIdOnOrderBy) "created_at DESC, id ASC" else "created_at DESC" + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") ORDER BY event_tags.created_at DESC LIMIT 30 + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY $orderBy + ├── CO-ROUTINE filtered + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=? AND kind=?) + │ └── USE TEMP B-TREE FOR DISTINCT + ├── SCAN filtered + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } + + @Test + fun testTagsAndAuthors() = + forEachDB { db -> + val filter = + listOf( + Filter( + authors = listOf(key1), + tags = mapOf("p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")), + limit = 30, + ), + ) + val orderBy = if (db.indexStrategy.useAndIndexIdOnOrderBy) "created_at DESC, id ASC" else "created_at DESC" + if (db.indexStrategy.indexTagsByCreatedAtAlone) { + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.pubkey_hash = "1730514094536529999") ORDER BY event_tags.created_at DESC LIMIT 30 + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY $orderBy + ├── CO-ROUTINE filtered + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash (tag_hash=?) + │ └── USE TEMP B-TREE FOR DISTINCT + ├── SCAN filtered + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } else { + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.pubkey_hash = "1730514094536529999") ORDER BY event_tags.created_at DESC LIMIT 30 + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY $orderBy + ├── CO-ROUTINE filtered + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=?) + │ ├── USE TEMP B-TREE FOR DISTINCT + │ └── USE TEMP B-TREE FOR ORDER BY + ├── SCAN filtered + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } + } + + @Test + fun testTwoTags() = + forEachDB { db -> + val filter = + listOf( + Filter( + kinds = listOf(1), + tags = + mapOf( + "p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + "t" to listOf("hashtag"), + ), + limit = 30, + ), + ) + val orderBy = if (db.indexStrategy.useAndIndexIdOnOrderBy) "created_at DESC, id ASC" else "created_at DESC" + if (db.indexStrategy.indexTagsByCreatedAtAlone) { + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_tags as event_tagsIn1 ON event_tagsIn1.event_header_row_id = event_tags.event_header_row_id AND event_tagsIn1.created_at = event_tags.created_at WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tagsIn1.tag_hash = "-6379614208644810021") AND (event_tags.kind = "1") ORDER BY event_tags.created_at DESC LIMIT 30 + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY $orderBy + ├── CO-ROUTINE filtered + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=? AND kind=?) + │ ├── SEARCH event_tagsIn1 USING INDEX query_by_tags_hash (tag_hash=? AND created_at=?) + │ └── USE TEMP B-TREE FOR DISTINCT + ├── SCAN filtered + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } else { + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_tags as event_tagsIn1 ON event_tagsIn1.event_header_row_id = event_tags.event_header_row_id AND event_tagsIn1.created_at = event_tags.created_at WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tagsIn1.tag_hash = "-6379614208644810021") AND (event_tags.kind = "1") ORDER BY event_tags.created_at DESC LIMIT 30 + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY $orderBy + ├── CO-ROUTINE filtered + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=? AND kind=?) + │ ├── SEARCH event_tagsIn1 USING INDEX fk_event_tags_header_id (event_header_row_id=?) + │ └── USE TEMP B-TREE FOR DISTINCT + ├── SCAN filtered + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } + } + + @Test + fun testIdQuery() = + forEachDB { db -> + val filter = Filter(ids = listOf(key1)) + if (db.indexStrategy.useAndIndexIdOnOrderBy) { + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + WHERE id = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d" + ORDER BY created_at DESC, id ASC + └── SEARCH event_headers USING INDEX event_headers_id (id=?) + """.trimIndent(), + db.explain(filter), + ) + } else { + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + WHERE id = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d" + ORDER BY created_at DESC + └── SEARCH event_headers USING INDEX event_headers_id (id=?) + """.trimIndent(), + db.explain(filter), + ) + } + } + + @Test + fun testAuthors() = + forEachDB { db -> + val filter = Filter(authors = listOf(key1, key2), kinds = listOf(1, 30023), limit = 300) + if (db.indexStrategy.useAndIndexIdOnOrderBy) { + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + WHERE (kind IN ("1", "30023")) AND (pubkey IN ("7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d", "f3ac434d61bc0f491a814782ccfdf9c439dae1f0bde9097ad4a245f4c495cd14")) + ORDER BY created_at DESC, id ASC + LIMIT 300 + ├── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } else { + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + WHERE (kind IN ("1", "30023")) AND (pubkey IN ("7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d", "f3ac434d61bc0f491a814782ccfdf9c439dae1f0bde9097ad4a245f4c495cd14")) + ORDER BY created_at DESC + LIMIT 300 + ├── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } + } + + @Test + fun testAuthorsAndSearch() = + forEachDB { db -> + val filter = Filter(authors = listOf(key1, key2, key3), search = "keywords") + if (db.indexStrategy.useAndIndexIdOnOrderBy) { + TestCase.assertEquals( + """ + SELECT event_headers.id, event_headers.pubkey, event_headers.created_at, event_headers.kind, event_headers.tags, event_headers.content, event_headers.sig FROM event_headers + INNER JOIN event_fts ON event_headers.row_id = event_fts.event_header_row_id + WHERE (event_fts MATCH "keywords") AND (event_headers.pubkey IN ("7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d", "f3ac434d61bc0f491a814782ccfdf9c439dae1f0bde9097ad4a245f4c495cd14", "12ae0fd81c85e1e7d9ed096397dc3129849425fe6f8afce7213ebf38ddfc6ca9")) + ORDER BY event_headers.created_at DESC, event_headers.id ASC + ├── SCAN event_fts VIRTUAL TABLE INDEX 4: + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } else { + TestCase.assertEquals( + """ + SELECT event_headers.id, event_headers.pubkey, event_headers.created_at, event_headers.kind, event_headers.tags, event_headers.content, event_headers.sig FROM event_headers + INNER JOIN event_fts ON event_headers.row_id = event_fts.event_header_row_id + WHERE (event_fts MATCH "keywords") AND (event_headers.pubkey IN ("7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d", "f3ac434d61bc0f491a814782ccfdf9c439dae1f0bde9097ad4a245f4c495cd14", "12ae0fd81c85e1e7d9ed096397dc3129849425fe6f8afce7213ebf38ddfc6ca9")) + ORDER BY event_headers.created_at DESC + ├── SCAN event_fts VIRTUAL TABLE INDEX 4: + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } + } + + @Test + fun testKindAndSearch() = + forEachDB { db -> + val filter = Filter(kinds = listOf(1, 1111, 10000), search = "keywords") + val orderBy = if (db.indexStrategy.useAndIndexIdOnOrderBy) "event_headers.created_at DESC, event_headers.id ASC" else "event_headers.created_at DESC" + TestCase.assertEquals( + """ + SELECT event_headers.id, event_headers.pubkey, event_headers.created_at, event_headers.kind, event_headers.tags, event_headers.content, event_headers.sig FROM event_headers + INNER JOIN event_fts ON event_headers.row_id = event_fts.event_header_row_id + WHERE (event_fts MATCH "keywords") AND (event_headers.kind IN ("1", "1111", "10000")) + ORDER BY $orderBy + ├── SCAN event_fts VIRTUAL TABLE INDEX 4: + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } + + @Test + fun testAllTag() = + forEachDB { db -> + val filter = Filter(tagsAll = mapOf("p" to listOf(key1, key2))) + val orderBy = if (db.indexStrategy.useAndIndexIdOnOrderBy) "created_at DESC, id ASC" else "created_at DESC" + if (db.indexStrategy.indexTagsByCreatedAtAlone) { + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_tags as event_tagsAll0_1 ON event_tagsAll0_1.event_header_row_id = event_tags.event_header_row_id AND event_tagsAll0_1.created_at = event_tags.created_at WHERE (event_tags.tag_hash = "884286737453847614") AND (event_tagsAll0_1.tag_hash = "-4988851810256311323") + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY $orderBy + ├── CO-ROUTINE filtered + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=?) + │ ├── SEARCH event_tagsAll0_1 USING INDEX query_by_tags_hash (tag_hash=? AND created_at=?) + │ └── USE TEMP B-TREE FOR DISTINCT + ├── SCAN filtered + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } else { + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_tags as event_tagsAll0_1 ON event_tagsAll0_1.event_header_row_id = event_tags.event_header_row_id AND event_tagsAll0_1.created_at = event_tags.created_at WHERE (event_tags.tag_hash = "884286737453847614") AND (event_tagsAll0_1.tag_hash = "-4988851810256311323") + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY $orderBy + ├── CO-ROUTINE filtered + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=?) + │ ├── SEARCH event_tagsAll0_1 USING INDEX fk_event_tags_header_id (event_header_row_id=?) + │ └── USE TEMP B-TREE FOR DISTINCT + ├── SCAN filtered + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } + } + + @Test + fun testReportLikeFilter() = + forEachDB { db -> + val filter = Filter( kinds = listOf(ContactListEvent.KIND), - limit = 30, - ), - ) - TestCase.assertEquals( - """ - SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - WHERE kind = "3" - ORDER BY created_at DESC - LIMIT 30 - └── SEARCH event_headers USING INDEX query_by_kind_created (kind=?) - """.trimIndent(), - dbAllIndexes.explain(filter), - ) - } - - @Test - fun testKinds() { - val filter = - listOf( - Filter( - kinds = listOf(ContactListEvent.KIND), - limit = 30, - ), - Filter( - kinds = listOf(TextNoteEvent.KIND), - limit = 30, - ), - ) - TestCase.assertEquals( - """ - SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - INNER JOIN ( - SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers WHERE event_headers.kind = "3" ORDER BY event_headers.created_at DESC LIMIT 30) - UNION - SELECT row_id FROM (SELECT event_headers.row_id as row_id FROM event_headers WHERE event_headers.kind = "1" ORDER BY event_headers.created_at DESC LIMIT 30) - ) AS filtered - ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC - ├── CO-ROUTINE filtered - │ └── COMPOUND QUERY - │ ├── LEFT-MOST SUBQUERY - │ │ ├── CO-ROUTINE (subquery-1) - │ │ │ └── SEARCH event_headers USING COVERING INDEX query_by_kind_created (kind=?) - │ │ └── SCAN (subquery-1) - │ └── UNION USING TEMP B-TREE - │ ├── CO-ROUTINE (subquery-3) - │ │ └── SEARCH event_headers USING COVERING INDEX query_by_kind_created (kind=?) - │ └── SCAN (subquery-3) - ├── SCAN filtered - ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - └── USE TEMP B-TREE FOR ORDER BY - """.trimIndent(), - dbAllIndexes.explain(filter), - ) - } - - @Test - fun testKindAndDTag() { - val filter = - listOf( - Filter( - kinds = listOf(ContactListEvent.KIND), - tags = mapOf("d" to listOf("")), - limit = 30, - ), - ) - TestCase.assertEquals( - """ - SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - WHERE (kind = "3") AND (d_tag = "") - ORDER BY created_at DESC - LIMIT 30 - └── SEARCH event_headers USING INDEX query_by_kind_created (kind=?) - """.trimIndent(), - dbAllIndexes.explain(filter), - ) - } - - @Test - fun testFollowersOf() { - val filter = - listOf( - Filter( - kinds = listOf(ContactListEvent.KIND), - tags = mapOf("p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")), - limit = 30, - ), - ) - TestCase.assertEquals( - """ - SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") ORDER BY event_tags.created_at DESC LIMIT 30 - ) AS filtered - ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC - ├── CO-ROUTINE filtered - │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=? AND kind=?) - │ └── USE TEMP B-TREE FOR DISTINCT - ├── SCAN filtered - ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - └── USE TEMP B-TREE FOR ORDER BY - """.trimIndent(), - dbAllIndexes.explain(filter), - ) - } - - @Test - fun testNotificationsOf() { - val filter = - listOf( - Filter( - tags = mapOf("p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")), - limit = 30, - ), - ) - TestCase.assertEquals( - """ - SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE event_tags.tag_hash = "-4551135004136952885" ORDER BY event_tags.created_at DESC LIMIT 30 - ) AS filtered - ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC - ├── CO-ROUTINE filtered - │ ├── SEARCH event_tags USING INDEX query_by_tags_hash (tag_hash=?) - │ └── USE TEMP B-TREE FOR DISTINCT - ├── SCAN filtered - ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - └── USE TEMP B-TREE FOR ORDER BY - """.trimIndent(), - dbAllIndexes.explain(filter), - ) - } - - @Test - fun testTagsAndKinds() { - val filter = - listOf( - Filter( - kinds = listOf(ContactListEvent.KIND), - tags = mapOf("p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")), - limit = 30, - ), - ) - TestCase.assertEquals( - """ - SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") ORDER BY event_tags.created_at DESC LIMIT 30 - ) AS filtered - ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC - ├── CO-ROUTINE filtered - │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=? AND kind=?) - │ └── USE TEMP B-TREE FOR DISTINCT - ├── SCAN filtered - ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - └── USE TEMP B-TREE FOR ORDER BY - """.trimIndent(), - dbAllIndexes.explain(filter), - ) - } - - @Test - fun testTagsAndAuthors() { - val filter = - listOf( - Filter( - authors = listOf(key1), - tags = mapOf("p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")), - limit = 30, - ), - ) - TestCase.assertEquals( - """ - SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.pubkey_hash = "1730514094536529999") ORDER BY event_tags.created_at DESC LIMIT 30 - ) AS filtered - ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC - ├── CO-ROUTINE filtered - │ ├── SEARCH event_tags USING INDEX query_by_tags_hash (tag_hash=?) - │ └── USE TEMP B-TREE FOR DISTINCT - ├── SCAN filtered - ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - └── USE TEMP B-TREE FOR ORDER BY - """.trimIndent(), - dbAllIndexes.explain(filter), - ) - } - - @Test - fun testTwoTags() { - val filter = - listOf( - Filter( - kinds = listOf(1), + authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), tags = mapOf( "p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), - "t" to listOf("hashtag"), ), - limit = 30, - ), + limit = 500, + ) + val orderBy = if (db.indexStrategy.useAndIndexIdOnOrderBy) "created_at DESC, id ASC" else "created_at DESC" + if (db.indexStrategy.indexTagsWithKindAndPubkey) { + assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") AND (event_tags.pubkey_hash = "5446767199141196776") ORDER BY event_tags.created_at DESC LIMIT 500 + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY $orderBy + ├── CO-ROUTINE filtered + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind_pubkey (tag_hash=? AND kind=? AND pubkey_hash=?) + │ └── USE TEMP B-TREE FOR DISTINCT + ├── SCAN filtered + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } else { + assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") AND (event_tags.pubkey_hash = "5446767199141196776") ORDER BY event_tags.created_at DESC LIMIT 500 + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY $orderBy + ├── CO-ROUTINE filtered + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=? AND kind=?) + │ └── USE TEMP B-TREE FOR DISTINCT + ├── SCAN filtered + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } + } + + @Test + fun testFollowersSinceNov2025() = + forEachDB { db -> + val filter = + Filter( + kinds = listOf(ContactListEvent.KIND), + tags = + mapOf( + "p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + ), + since = 1764553447, // Nov 2025 + ) + val orderBy = if (db.indexStrategy.useAndIndexIdOnOrderBy) "created_at DESC, id ASC" else "created_at DESC" + assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ( + SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") AND (event_tags.created_at >= "1764553447") + ) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY $orderBy + ├── CO-ROUTINE filtered + │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=? AND kind=? AND created_at>?) + │ └── USE TEMP B-TREE FOR DISTINCT + ├── SCAN filtered + ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), ) - TestCase.assertEquals( - """ - SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_tags as event_tagsIn1 ON event_tagsIn1.event_header_row_id = event_tags.event_header_row_id AND event_tagsIn1.created_at = event_tags.created_at WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tagsIn1.tag_hash = "-6379614208644810021") AND (event_tags.kind = "1") ORDER BY event_tags.created_at DESC LIMIT 30 - ) AS filtered - ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC - ├── CO-ROUTINE filtered - │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=? AND kind=?) - │ ├── SEARCH event_tagsIn1 USING INDEX query_by_tags_hash (tag_hash=? AND created_at=?) - │ └── USE TEMP B-TREE FOR DISTINCT - ├── SCAN filtered - ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - └── USE TEMP B-TREE FOR ORDER BY - """.trimIndent(), - dbAllIndexes.explain(filter), - ) - } + } @Test - fun testIdQuery() { - val filter = Filter(ids = listOf(key1)) - TestCase.assertEquals( - """ - SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - WHERE id = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d" - ORDER BY created_at DESC - └── SEARCH event_headers USING INDEX event_headers_id (id=?) - """.trimIndent(), - dbAllIndexes.explain(filter), - ) - } - - @Test - fun testAuthors() { - val filter = Filter(authors = listOf(key1, key2), kinds = listOf(1, 30023), limit = 300) - TestCase.assertEquals( - """ - SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - WHERE (kind IN ("1", "30023")) AND (pubkey IN ("7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d", "f3ac434d61bc0f491a814782ccfdf9c439dae1f0bde9097ad4a245f4c495cd14")) - ORDER BY created_at DESC - LIMIT 300 - ├── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=?) - └── USE TEMP B-TREE FOR ORDER BY - """.trimIndent(), - dbAllIndexes.explain(filter), - ) - } - - @Test - fun testAuthorsAndSearch() { - val filter = Filter(authors = listOf(key1, key2, key3), search = "keywords") - println(dbAllIndexes.explain(filter)) - TestCase.assertEquals( - """ - SELECT event_headers.id, event_headers.pubkey, event_headers.created_at, event_headers.kind, event_headers.tags, event_headers.content, event_headers.sig FROM event_headers - INNER JOIN event_fts ON event_headers.row_id = event_fts.event_header_row_id - WHERE (event_fts MATCH "keywords") AND (event_headers.pubkey IN ("7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d", "f3ac434d61bc0f491a814782ccfdf9c439dae1f0bde9097ad4a245f4c495cd14", "12ae0fd81c85e1e7d9ed096397dc3129849425fe6f8afce7213ebf38ddfc6ca9")) - ORDER BY event_headers.created_at DESC - ├── SCAN event_fts VIRTUAL TABLE INDEX 4: - ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - └── USE TEMP B-TREE FOR ORDER BY - """.trimIndent(), - dbAllIndexes.explain(filter), - ) - } - - @Test - fun testKindAndSearch() { - val filter = Filter(kinds = listOf(1, 1111, 10000), search = "keywords") - println(dbAllIndexes.explain(filter)) - TestCase.assertEquals( - """ - SELECT event_headers.id, event_headers.pubkey, event_headers.created_at, event_headers.kind, event_headers.tags, event_headers.content, event_headers.sig FROM event_headers - INNER JOIN event_fts ON event_headers.row_id = event_fts.event_header_row_id - WHERE (event_fts MATCH "keywords") AND (event_headers.kind IN ("1", "1111", "10000")) - ORDER BY event_headers.created_at DESC - ├── SCAN event_fts VIRTUAL TABLE INDEX 4: - ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - └── USE TEMP B-TREE FOR ORDER BY - """.trimIndent(), - dbAllIndexes.explain(filter), - ) - } - - @Test - fun testAllTag() { - val filter = Filter(tagsAll = mapOf("p" to listOf(key1, key2))) - TestCase.assertEquals( - """ - SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags INNER JOIN event_tags as event_tagsAll0_1 ON event_tagsAll0_1.event_header_row_id = event_tags.event_header_row_id AND event_tagsAll0_1.created_at = event_tags.created_at WHERE (event_tags.tag_hash = "884286737453847614") AND (event_tagsAll0_1.tag_hash = "-4988851810256311323") - ) AS filtered - ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC - ├── CO-ROUTINE filtered - │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=?) - │ ├── SEARCH event_tagsAll0_1 USING INDEX query_by_tags_hash (tag_hash=? AND created_at=?) - │ └── USE TEMP B-TREE FOR DISTINCT - ├── SCAN filtered - ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - └── USE TEMP B-TREE FOR ORDER BY - """.trimIndent(), - dbAllIndexes.explain(filter), - ) - } - - @Test - fun testReportLikeFilter() { - val filter = - Filter( - kinds = listOf(ContactListEvent.KIND), - authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), - tags = - mapOf( - "p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), - ), - limit = 500, + fun testAllAddressablesOfAKindDownload() = + forEachDB { db -> + val filter = + Filter( + kinds = listOf(DraftWrapEvent.KIND), + authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + since = 1764553447, // Nov 2025 + ) + val orderBy = if (db.indexStrategy.useAndIndexIdOnOrderBy) "created_at DESC, id ASC" else "created_at DESC" + assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + WHERE (kind = "31234") AND (pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (created_at >= "1764553447") + ORDER BY $orderBy + └── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=? AND created_at>?) + """.trimIndent(), + db.explain(filter), ) - assertEquals( - """ - SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") AND (event_tags.pubkey_hash = "5446767199141196776") ORDER BY event_tags.created_at DESC LIMIT 500 - ) AS filtered - ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC - ├── CO-ROUTINE filtered - │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind_pubkey (tag_hash=? AND kind=? AND pubkey_hash=?) - │ └── USE TEMP B-TREE FOR DISTINCT - ├── SCAN filtered - ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - └── USE TEMP B-TREE FOR ORDER BY - """.trimIndent(), - dbAllIndexes.explain(filter), - ) - } + } @Test - fun testFollowersSinceNov2025() { - val filter = - Filter( - kinds = listOf(ContactListEvent.KIND), - tags = - mapOf( - "p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), - ), - since = 1764553447, // Nov 2025 - ) - assertEquals( - """ - SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - INNER JOIN ( - SELECT DISTINCT(event_tags.event_header_row_id) as row_id FROM event_tags WHERE (event_tags.tag_hash = "-4551135004136952885") AND (event_tags.kind = "3") AND (event_tags.created_at >= "1764553447") - ) AS filtered - ON event_headers.row_id = filtered.row_id - ORDER BY created_at DESC - ├── CO-ROUTINE filtered - │ ├── SEARCH event_tags USING INDEX query_by_tags_hash_kind (tag_hash=? AND kind=? AND created_at>?) - │ └── USE TEMP B-TREE FOR DISTINCT - ├── SCAN filtered - ├── SEARCH event_headers USING INTEGER PRIMARY KEY (rowid=?) - └── USE TEMP B-TREE FOR ORDER BY - """.trimIndent(), - dbAllIndexes.explain(filter), - ) - } + fun testReplaceablesOfMultipleKindsDownloadByDateLimit() = + forEachDB { db -> + val filter = + Filter( + kinds = listOf(MetadataEvent.KIND, SearchRelayListEvent.KIND, AdvertisedRelayListEvent.KIND), + authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + limit = 20, + since = 1764553447, // Nov 2025 + ) + if (db.indexStrategy.useAndIndexIdOnOrderBy) { + assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + WHERE (kind IN ("0", "10007", "10002")) AND (pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (created_at >= "1764553447") + ORDER BY created_at DESC, id ASC + LIMIT 20 + ├── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=? AND created_at>?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } else { + assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + WHERE (kind IN ("0", "10007", "10002")) AND (pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (created_at >= "1764553447") + ORDER BY created_at DESC + LIMIT 20 + ├── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=? AND created_at>?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } + } @Test - fun testAllAddressablesOfAKindDownload() { - val filter = - Filter( - kinds = listOf(DraftWrapEvent.KIND), - authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), - since = 1764553447, // Nov 2025 - ) - assertEquals( - """ - SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - WHERE (kind = "31234") AND (pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (created_at >= "1764553447") - ORDER BY created_at DESC - └── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=? AND created_at>?) - """.trimIndent(), - dbAllIndexes.explain(filter), - ) - } + fun testReplaceablesOfMultipleKindsDownload() = + forEachDB { db -> + val filter = + Filter( + kinds = listOf(MetadataEvent.KIND, SearchRelayListEvent.KIND, AdvertisedRelayListEvent.KIND), + authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + ) + + if (db.indexStrategy.useAndIndexIdOnOrderBy) { + assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + WHERE (kind IN ("0", "10007", "10002")) AND (pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") + ORDER BY created_at DESC, id ASC + ├── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } else { + assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + WHERE (kind IN ("0", "10007", "10002")) AND (pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") + ORDER BY created_at DESC + ├── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } + } @Test - fun testReplaceablesOfMultipleKindsDownloadByDateLimit() { - val filter = - Filter( - kinds = listOf(MetadataEvent.KIND, SearchRelayListEvent.KIND, AdvertisedRelayListEvent.KIND), - authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), - limit = 20, - since = 1764553447, // Nov 2025 - ) - assertEquals( - """ - SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - WHERE (kind IN ("0", "10007", "10002")) AND (pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (created_at >= "1764553447") - ORDER BY created_at DESC - LIMIT 20 - ├── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=? AND created_at>?) - └── USE TEMP B-TREE FOR ORDER BY - """.trimIndent(), - dbAllIndexes.explain(filter), - ) - } + fun testContactCardDownloadFromTrustedKeys() = + forEachDB { db -> + val filter = + Filter( + kinds = listOf(ContactCardEvent.KIND), + authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + tags = + mapOf( + "d" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + ), + since = 1764553447, // Nov 2025 + ) - @Test - fun testReplaceablesOfMultipleKindsDownload() { - val filter = - Filter( - kinds = listOf(MetadataEvent.KIND, SearchRelayListEvent.KIND, AdvertisedRelayListEvent.KIND), - authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), - ) - - assertEquals( - """ - SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - WHERE (kind IN ("0", "10007", "10002")) AND (pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") - ORDER BY created_at DESC - ├── SEARCH event_headers USING INDEX query_by_kind_pubkey_created (kind=? AND pubkey=?) - └── USE TEMP B-TREE FOR ORDER BY - """.trimIndent(), - dbAllIndexes.explain(filter), - ) - } - - @Test - fun testContactCardDownloadFromTrustedKeys() { - val filter = - Filter( - kinds = listOf(ContactCardEvent.KIND), - authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), - tags = - mapOf( - "d" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), - ), - since = 1764553447, // Nov 2025 - ) - - assertEquals( - """ - SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers - WHERE (kind = "30382") AND (pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (d_tag = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (created_at >= "1764553447") AND ((kind >= 30000 AND kind < 40000)) - ORDER BY created_at DESC - ├── SEARCH event_headers USING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?) - └── USE TEMP B-TREE FOR ORDER BY - """.trimIndent(), - dbAllIndexes.explain(filter), - ) - } + if (db.indexStrategy.useAndIndexIdOnOrderBy) { + assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + WHERE (kind = "30382") AND (pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (d_tag = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (created_at >= "1764553447") AND ((kind >= 30000 AND kind < 40000)) + ORDER BY created_at DESC, id ASC + ├── SEARCH event_headers USING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } else { + assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + WHERE (kind = "30382") AND (pubkey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (d_tag = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") AND (created_at >= "1764553447") AND ((kind >= 30000 AND kind < 40000)) + ORDER BY created_at DESC + ├── SEARCH event_headers USING INDEX addressable_idx (kind=? AND pubkey=? AND d_tag=?) + └── USE TEMP B-TREE FOR ORDER BY + """.trimIndent(), + db.explain(filter), + ) + } + } } diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionRequestModule.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionRequestModule.kt index deb4a061d..6905889e6 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionRequestModule.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionRequestModule.kt @@ -30,32 +30,45 @@ import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent class DeletionRequestModule( val hasher: (db: SQLiteDatabase) -> TagNameValueHasher, + val indexStrategy: IndexingStrategy = DefaultIndexingStrategy(), ) : IModule { + fun rejectDeletedEventsSQLTemplate(): String = + if (indexStrategy.indexTagsWithKindAndPubkey) { + """ + |SELECT 1 FROM event_tags + |WHERE + | event_tags.tag_hash IN (NEW.etag_hash, NEW.atag_hash) AND + | event_tags.kind = 5 AND + | event_tags.pubkey_hash = NEW.pubkey_owner_hash AND + | event_tags.created_at >= NEW.created_at + """.trimMargin() + } else { + """ + |SELECT 1 FROM event_tags + |WHERE + | event_tags.tag_hash IN (NEW.etag_hash, NEW.atag_hash) AND + | event_tags.kind = 5 AND + | event_tags.created_at >= NEW.created_at AND + | event_tags.pubkey_hash = NEW.pubkey_owner_hash + """.trimMargin() + } + /** * Creates a trigger to reject events that have been * deleted by ID or ATag including GiftWraps that * must be checked against the p-tag (pubkey_owner_hash) */ override fun create(db: SQLiteDatabase) { + val sql = rejectDeletedEventsSQLTemplate().replace("\n", "\n ") db.execSQL( """ CREATE TRIGGER reject_deleted_events BEFORE INSERT ON event_headers FOR EACH ROW BEGIN - -- Checks if this event hasn't already been deleted. If so, reject it. - - -- Highly optimized using hash + created_at (duplicated in tags) index: - ---- Expects tag_hash to be non-existant for new events (quick exit) - ---- Expects created-at >= event's to not exist (quick exit) SELECT RAISE(ABORT, 'blocked: a deletion event exists') WHERE EXISTS ( - SELECT 1 FROM event_tags - WHERE - event_tags.tag_hash IN (NEW.etag_hash, NEW.atag_hash) AND - event_tags.kind = 5 AND - event_tags.pubkey_hash = NEW.pubkey_owner_hash AND - event_tags.created_at >= NEW.created_at + $sql ); END; """.trimIndent(), diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt index 4dbe73aa4..8aefa83e4 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt @@ -75,8 +75,9 @@ class EventIndexesModule( } // queries by limit (latest records), since, until (sync all) alone without any filter by kind.. rare - // serves as fall back for the lack of query_by_tags_hash index by default - db.execSQL("CREATE INDEX query_by_created_at_id ON event_headers ($orderBy)") + if (indexStrategy.indexEventsByCreatedAtAlone) { + db.execSQL("CREATE INDEX query_by_created_at_id ON event_headers ($orderBy)") + } // queries by kind only, mostly used in Global Feeds when author is not important. db.execSQL("CREATE INDEX query_by_kind_created ON event_headers (kind, $orderBy)") @@ -87,10 +88,10 @@ class EventIndexesModule( // makes deletions on the event_header fast db.execSQL("CREATE INDEX fk_event_tags_header_id ON event_tags (event_header_row_id)") - // --------------------------------------------------------------- - // This are a very slow indexes (80% of the insert time goes here) - // --------------------------------------------------------------- - if (indexStrategy.indexTagQueriesWithoutKinds) { + // --------------------------------------------------------------------------- + // These next 3 are a very slow indexes (80% of the insert time goes here) + // --------------------------------------------------------------------------- + if (indexStrategy.indexTagsByCreatedAtAlone) { // First one is only needed if the user is searching by tags without a kind. db.execSQL("CREATE INDEX query_by_tags_hash ON event_tags (tag_hash, created_at DESC)") } @@ -98,8 +99,10 @@ class EventIndexesModule( // This is the default index for most clients: tags by specific kinds that are supported by the client. db.execSQL("CREATE INDEX query_by_tags_hash_kind ON event_tags (tag_hash, kind, created_at DESC)") - // this one is to allow search of tags by kind and author at the same time: DMs, reports, - db.execSQL("CREATE INDEX query_by_tags_hash_kind_pubkey ON event_tags (tag_hash, kind, pubkey_hash, created_at DESC)") + // this one is to allow search of tags by kind and author at the same time: NIP-04 DMs, reports, + if (indexStrategy.indexTagsWithKindAndPubkey) { + db.execSQL("CREATE INDEX query_by_tags_hash_kind_pubkey ON event_tags (tag_hash, kind, pubkey_hash, created_at DESC)") + } // Prevent updates to maintain immutability db.execSQL( diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/IndexingStrategy.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/IndexingStrategy.kt index 1f5cf7745..a0a6ea1d4 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/IndexingStrategy.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/IndexingStrategy.kt @@ -23,7 +23,62 @@ package com.vitorpamplona.quartz.nip01Core.store.sqlite import com.vitorpamplona.quartz.nip01Core.core.Tag interface IndexingStrategy { - val indexTagQueriesWithoutKinds: Boolean + /** + * Activate this if you see too many Filters with just LIMIT, SINCE and + * UNTIL filled up. + * + * Clients never support all kinds, so this is usually + * only done with syncing services that must download ALL kinds from + * ALL authors. + * + * The index will make these queries significantly faster, but maybe speed + * is not a requirement on Sync services. The size of this index is + * considerable. + * + * Keep in mind that activating too many indexes increases the size of the + * DB so much that the indexes themselves won't fit in memory, requiring + * frequent reloadings of the index itself from disk. + */ + val indexEventsByCreatedAtAlone: Boolean + + /** + * Activate this if you see too many Tag-centric Filters without + * kind, pubkey or id. + * + * Clients never support all kinds, so this is usually + * only done in rare usecases where the client supports all + * kinds. + * + * The index will make these queries significantly faster, but maybe speed + * is not a requirement on such services. Because this is an index in + * event tags, it becomes QUITE BIG. + * + * Keep in mind that activating too many indexes increases the size of the + * DB so much that the indexes themselves won't fit in memory, requiring + * frequent reloadings of the index itself from disk. + */ + val indexTagsByCreatedAtAlone: Boolean + + /** + * Activate this if you see too many Tag-centric Filters without + * kind AND pubkey at the same time. + * + * This is a rarely used index (reports by your follows or + * NIP-04 DMs for instance) that becomes quite large without + * major gains. + * + * Keep in mind that activating too many indexes increases the size of the + * DB so much that the indexes themselves won't fit in memory, requiring + * frequent reloadings of the index itself from disk. + */ + val indexTagsWithKindAndPubkey: Boolean + + /** + * Activate this to make sure queries are always in order when + * the same created_at exists. This will impact performance and + * the size of indexes, but it provides results that are compliant + * with the Nostr Spec + */ val useAndIndexIdOnOrderBy: Boolean fun shouldIndex( @@ -36,7 +91,9 @@ interface IndexingStrategy { * By default, we index all tags that have a single letter name and some value */ class DefaultIndexingStrategy( - override val indexTagQueriesWithoutKinds: Boolean = false, + override val indexEventsByCreatedAtAlone: Boolean = false, + override val indexTagsByCreatedAtAlone: Boolean = false, + override val indexTagsWithKindAndPubkey: Boolean = false, override val useAndIndexIdOnOrderBy: Boolean = false, ) : IndexingStrategy { override fun shouldIndex( diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt index ef4467b84..45e797cb4 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt @@ -655,7 +655,7 @@ class QueryBuilder( if (project) { append("\nORDER BY created_at DESC") if (indexStrategy.useAndIndexIdOnOrderBy) { - append(", event_headers.id ASC") + append(", id ASC") } } if (limit != null) { From 0d923b5caac0ecbbf941515d6a786c52892dcfd3 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 4 Jan 2026 12:19:00 +0100 Subject: [PATCH 057/137] optimise imports --- .../main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt | 1 - .../vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt | 2 -- .../kotlin/com/vitorpamplona/quartz/utils/SecureRandom.ios.kt | 1 - .../com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt | 1 - 4 files changed, 5 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt index 0473c743b..5e7ed2ff6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.service.relays import androidx.collection.LruCache import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relays.MutableTime import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl typealias SincePerRelayMap = MutableMap diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt index 45e797cb4..d4ab144f6 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt @@ -30,8 +30,6 @@ import com.vitorpamplona.quartz.nip01Core.core.isAddressable import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.store.sqlite.sql.where import com.vitorpamplona.quartz.utils.EventFactory -import kotlin.collections.component1 -import kotlin.collections.component2 class QueryBuilder( val fts: FullTextSearchModule, diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/SecureRandom.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/SecureRandom.ios.kt index 26f0ed41d..cd27fd9ad 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/SecureRandom.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/SecureRandom.ios.kt @@ -24,7 +24,6 @@ import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.refTo import platform.Security.SecRandomCopyBytes import platform.Security.kSecRandomDefault -import kotlin.random.Random.Default.nextBytes actual class SecureRandom { actual fun nextInt(): Int { diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt index a6ca2615b..77c56012a 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt @@ -59,7 +59,6 @@ import com.vitorpamplona.quartz.nip47WalletConnect.jackson.ResponseDeserializer import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor import com.vitorpamplona.quartz.nip59Giftwrap.rumors.jackson.RumorDeserializer import com.vitorpamplona.quartz.nip59Giftwrap.rumors.jackson.RumorSerializer -import kotlinx.serialization.json.JsonNull.content import java.io.InputStream class JacksonMapper { From a4925de4a19208641c24114a80d7322b9d45ca39 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 4 Jan 2026 12:27:07 +0100 Subject: [PATCH 058/137] cleaner code --- .../java/com/vitorpamplona/amethyst/LocalPreferences.kt | 6 ++---- .../ui/screen/loggedIn/home/ShortNotePostViewModel.kt | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index d2678ca68..18916ef15 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -248,10 +248,8 @@ object LocalPreferences { withContext(Dispatchers.IO) { val prefsDir = File(prefsDirPath) prefsDir.list()?.forEach { - if (it.contains(npub)) { - if (!File(prefsDir, it).delete()) { - Log.w("LocalPreferences", "Failed to delete preference file: $it") - } + if (it.contains(npub) && !File(prefsDir, it).delete()) { + Log.w("LocalPreferences", "Failed to delete preference file: $it") } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 652cc425f..cd479b134 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -972,7 +972,6 @@ open class ShortNotePostViewModel : val uploadErrorTitle = stringRes(appContext, R.string.upload_error_title) val uploadVoiceNip95NotSupported = stringRes(appContext, R.string.upload_error_voice_message_nip95_not_supported) val uploadVoiceFailed = stringRes(appContext, R.string.upload_error_voice_message_failed) - val uploadVoiceUnexpected = stringRes(appContext, R.string.upload_error_voice_message_unexpected_state) val uploadVoiceExceptionMessage: (String) -> String = { detail -> stringRes(appContext, R.string.upload_error_voice_message_exception, detail) } From 59fb17563715b8b295a9f6100336d5fd3b898797 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 4 Jan 2026 12:32:08 +0100 Subject: [PATCH 059/137] refactor VoiceMessagePreview.kt --- .../ui/actions/uploads/VoiceMessagePreview.kt | 229 +++++++++++------- 1 file changed, 135 insertions(+), 94 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt index a601837be..7633a47d8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt @@ -74,64 +74,23 @@ fun VoiceMessagePreview( var progress by remember { mutableFloatStateOf(0f) } var mediaPlayer by remember { mutableStateOf(null) } - // Initialize MediaPlayer - DisposableEffect(voiceMetadata.url, localFile) { - val player = createMediaPlayer(voiceMetadata.url, localFile) - player?.setOnCompletionListener { + ManageMediaPlayer( + voiceMetadata = voiceMetadata, + localFile = localFile, + onCompletion = { isPlaying = false progress = 0f - } - mediaPlayer = player + }, + onPlayerChanged = { mediaPlayer = it }, + onRelease = { isPlaying = false }, + ) - onDispose { - // Stop playback and clean up - try { - player?.stop() - } catch (e: IllegalStateException) { - // Player might already be stopped - Log.d("VoiceMessagePreview", "MediaPlayer stop failed (already stopped)", e) - } - player?.release() - mediaPlayer = null - isPlaying = false - } - } - - // Update progress while playing - LaunchedEffect(mediaPlayer, isPlaying) { - // Capture player reference to avoid reading volatile state repeatedly - val player = mediaPlayer - if (player != null && isPlaying) { - while (isActive) { - try { - if (player.isPlaying) { - val current = player.currentPosition.toFloat() - val duration = player.duration.toFloat() - // Validate values before calculating progress - val newProgress = - if (duration > 0 && current >= 0) { - (current / duration).coerceIn(0f, 1f) - } else { - 0f - } - // Only update if value is valid (not NaN or Infinity) - if (newProgress.isFinite()) { - progress = newProgress - } - } else { - // Player stopped, exit loop and let LaunchedEffect restart - break - } - } catch (e: IllegalStateException) { - // Player in invalid state, stop tracking - Log.w("VoiceMessagePreview", "MediaPlayer in invalid state during progress tracking", e) - isPlaying = false - break - } - delay(100) - } - } - } + TrackPlaybackProgress( + mediaPlayer = mediaPlayer, + isPlaying = isPlaying, + onProgressUpdate = { progress = it }, + onInvalidState = { isPlaying = false }, + ) Box( modifier = @@ -150,27 +109,13 @@ fun VoiceMessagePreview( // Play/Pause Button IconButton( onClick = { - val player = mediaPlayer - if (player != null) { - try { - if (isPlaying) { - player.pause() - isPlaying = false - } else { - // Validate progress before comparison - if (progress.isFinite() && progress >= 1f) { - player.seekTo(0) - progress = 0f - } - player.start() - isPlaying = true - } - } catch (e: IllegalStateException) { - // MediaPlayer in invalid state, ignore - Log.w("VoiceMessagePreview", "MediaPlayer operation failed in onClick handler", e) - isPlaying = false - } - } + handlePlayPauseClick( + mediaPlayer = mediaPlayer, + isPlaying = isPlaying, + progress = progress, + onProgressReset = { progress = 0f }, + onPlayingChanged = { isPlaying = it }, + ) }, modifier = Modifier.size(48.dp), ) { @@ -194,24 +139,11 @@ fun VoiceMessagePreview( waveformBrush = Brush.linearGradient(listOf(MaterialTheme.colorScheme.onSurfaceVariant, MaterialTheme.colorScheme.onSurfaceVariant)), progressBrush = Brush.linearGradient(listOf(MaterialTheme.colorScheme.primary, MaterialTheme.colorScheme.primary)), onProgressChange = { newProgress -> - // Validate incoming progress value - if (newProgress.isFinite() && newProgress >= 0f && newProgress <= 1f) { - val player = mediaPlayer - if (player != null) { - try { - val duration = player.duration - // Only seek if duration is valid - if (duration > 0) { - val newPosition = (newProgress * duration).toInt() - player.seekTo(newPosition) - progress = newProgress - } - } catch (e: IllegalStateException) { - // MediaPlayer in invalid state, ignore - Log.w("VoiceMessagePreview", "MediaPlayer seek failed in onProgressChange", e) - } - } - } + handleWaveformScrub( + newProgress = newProgress, + mediaPlayer = mediaPlayer, + onProgressChanged = { progress = it }, + ) }, ) @@ -240,6 +172,115 @@ fun VoiceMessagePreview( } } +@Composable +private fun ManageMediaPlayer( + voiceMetadata: AudioMeta, + localFile: File?, + onCompletion: () -> Unit, + onPlayerChanged: (MediaPlayer?) -> Unit, + onRelease: () -> Unit, +) { + DisposableEffect(voiceMetadata.url, localFile) { + val player = createMediaPlayer(voiceMetadata.url, localFile) + player?.setOnCompletionListener { onCompletion() } + onPlayerChanged(player) + + onDispose { + try { + player?.stop() + } catch (e: IllegalStateException) { + Log.d("VoiceMessagePreview", "MediaPlayer stop failed (already stopped)", e) + } + player?.release() + onPlayerChanged(null) + onRelease() + } + } +} + +@Composable +private fun TrackPlaybackProgress( + mediaPlayer: MediaPlayer?, + isPlaying: Boolean, + onProgressUpdate: (Float) -> Unit, + onInvalidState: () -> Unit, +) { + LaunchedEffect(mediaPlayer, isPlaying) { + val player = mediaPlayer ?: return@LaunchedEffect + if (!isPlaying) return@LaunchedEffect + + while (isActive) { + try { + if (!player.isPlaying) break + calculateProgress(player.currentPosition.toFloat(), player.duration.toFloat())?.let { onProgressUpdate(it) } + } catch (e: IllegalStateException) { + Log.w("VoiceMessagePreview", "MediaPlayer in invalid state during progress tracking", e) + onInvalidState() + break + } + delay(100) + } + } +} + +private fun calculateProgress( + current: Float, + duration: Float, +): Float? { + val progress = + if (duration > 0 && current >= 0) { + (current / duration).coerceIn(0f, 1f) + } else { + 0f + } + return progress.takeIf { it.isFinite() } +} + +private fun handlePlayPauseClick( + mediaPlayer: MediaPlayer?, + isPlaying: Boolean, + progress: Float, + onProgressReset: () -> Unit, + onPlayingChanged: (Boolean) -> Unit, +) { + val player = mediaPlayer ?: return + try { + if (isPlaying) { + player.pause() + onPlayingChanged(false) + return + } + if (progress.isFinite() && progress >= 1f) { + player.seekTo(0) + onProgressReset() + } + player.start() + onPlayingChanged(true) + } catch (e: IllegalStateException) { + Log.w("VoiceMessagePreview", "MediaPlayer operation failed in onClick handler", e) + onPlayingChanged(false) + } +} + +private fun handleWaveformScrub( + newProgress: Float, + mediaPlayer: MediaPlayer?, + onProgressChanged: (Float) -> Unit, +) { + if (!newProgress.isFinite() || newProgress < 0f || newProgress > 1f) return + val player = mediaPlayer ?: return + try { + val duration = player.duration + if (duration > 0) { + val newPosition = (newProgress * duration).toInt() + player.seekTo(newPosition) + onProgressChanged(newProgress) + } + } catch (e: IllegalStateException) { + Log.w("VoiceMessagePreview", "MediaPlayer seek failed in onProgressChange", e) + } +} + private fun createMediaPlayer( url: String, localFile: File?, From 69dd0b8e5e065cdba39974826eb9c53a19431251 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Sun, 4 Jan 2026 18:38:38 +0000 Subject: [PATCH 060/137] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-hu-rHU/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index b64c49bbf..b889daa52 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -481,8 +481,8 @@ Adatvédelmi beállítások Tor és Orbot beállítása Kapcsolódás az Orbot beállításain keresztül - Beállítás - Tor beállítások + Módosítás + Tor Beállítások Kapcsolat bontása az Orbottal / Torral? Az Ön adatai azonnal átkerülnek a normál hálózatra Igen From eeafeeff1739f06c6ba10d3b04090cf9dae1042c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 4 Jan 2026 14:40:45 -0500 Subject: [PATCH 061/137] Moves to new banner by Purple Painer --- .../res/drawable-xxxhdpi/profile_banner.jpg | Bin 186824 -> 542436 bytes .../drawable-xxxhdpi/profile_banner_old.jpg | Bin 0 -> 186824 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 amethyst/src/main/res/drawable-xxxhdpi/profile_banner_old.jpg diff --git a/amethyst/src/main/res/drawable-xxxhdpi/profile_banner.jpg b/amethyst/src/main/res/drawable-xxxhdpi/profile_banner.jpg index f0c93eb835a3c33919667b31b21819dc068d2097..126d9edf6dbbe009900f429fd0126adc67faa504 100644 GIT binary patch literal 542436 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�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�oyiq3fMBzI$}BcDBKXuD|M&D%fnd|4ewq_S(f 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

jc;w7pEHrk4a`IrkR(NSf%A4bL_+~^dG{J~ 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�>|rtdhQ3Yi#iL=!*Cb^*P7Mqtdnx{ak$C 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

87Uq+2{4i_7 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]adlyI9uJlQ((f<`hGxV;VHht$^R_36I5_J)?Y9Mc9gwp-;u5y@h(HmhI{CmaqJ+UK(0l)(e zCW5;gH`h?ND*(kU5@(%z4`04&KeOgYiTiN^{{VJCN6c7UUH6M)xmj? zmuwqC1`?&|ao}0|rF%U}_rGgAy93NfM>L00FA^F6s$`reOAE_p-8;%B-aB%aPBc)b zc4HvZPC&QP(O$!$p_VrK0L-gjt#WJ9Tet4`^W++(=LAdhf(M!cbEYf>D~f_gDLB{<6mnQVH4kB;_uaQ^^*++NOKZYE>K82C-Ej>+SSo-1{Z zh4udcy;g63qm+E2vN}hT^vGo#C~gU&Ef+|h>v(gSeqM0Bqpz~oL+yw%d{EbjwAGRic`0}9C(+s~gwrVK1Y#aa-Nb@G zNpLU30Zr3e8coHy9FUbKg?Q|1${y~o9Xv2XBo!TPwmq{eO>w`OTF`m~RsR6LKI~ur z0DPUM0Nm$KunEyEubLZ4-ew0KT{Q>Po|T5>mQ7jbm7WyKbWF)e5PY&OXSD79C)dR#1+Tb z8cb|{(aPO7APHELt)`3$5lqq23r9j!)In82yM$RZhE11?C2NqoW8fygSkr!vc4V5h zy^^$-=+EjW#G6m|C+<(;8zk?TBS#Ek4DPRP0hr^9U6kU}k<<|Y{vH_46IzRz2wta8 zZ`LS}c6CFEiTf@YWgaLHGSmIb*I&vy)g5CI%`Z3`Tb@auMA2^ODi*eG*Z175dLMQd6Y{>qH65(8cqv;4bq-%N2Ohe3tfXgO zy<6&_R8bHoQu9n3^PMO5TKj_Fvc$k7@>8cev!I{(YfNnBnp_exqbuk^X&e_Bg$9J%_SM!`J0yQOl=h+Bh?WK-6yuX$q<+Dy8ZxzZ zbbt5jd2)ee+144$xM9VvCTOPyHO%Rmk6j_S=AzHVOcbBNS3I4B*}der=3kA7vyxI~dS9Zd}vH6bA5|<#=y8d}D1U>%CKz z2NZ4ViyH3%hK8(bap=vE-oSNlpYm$b>AI2-39OY}r%UXL1W<)+lw?|P>+`>lRfFi> zywk3RRHkr^aL(cwdb4`7GXq%NCqtN9Y># zjTcw^4cUkzU5)euyPmK-jlwF+441d-CHUM6vLd0qm{DYlMeE3{)DbPHhJfevXt?~R zPjjGt_}Bdrqtu~3YX?jN_f#k~8sJ0&&y=RW?fu=JWMj_74&q2O7NzE{ ztw*p1Z6l>d57OWd^x23qd_IJD3c^4$Wkwq%LY`;lx>Q@NcOeA78Lr7QasOa^}kSAuj^)x4NF0ph8W22NvU@0rz z6<`SPU*bi-x~L;L0Cs>_Mn)CLh;m9o^$JZdtCTmY5r~nIl*T zcAG}3f`Ob}dK6zTo;B$cr!r0SfRIWBHhlJCC#O~to5Lo(&yFL+Wj?)yY|1IG&NxlM z1H->zM6J+-%e``n=Br9UYed4<$R&s7dOJL3f#F3-`5~i%AXkKMp`L*y1)ELh*kty1<#8yy7Mg}a&M=e-0UljXhR7I` zsLy3ASq$ZjHGdN4lly+J+ClzRv6fAKT`?`VV>e{P{EL0fpZV7w-L^-*SpNVd$4oF@f=)v zWrXQ%(WKz^LcyZ4-z}$?KHm9xX89Lj3pDI99A}`whOBiUS&*JbmTb29Y(4$nPnMzn ztUUF$s^FhV+-<1`m-A{PdN6@7vq!Tpy^-EZ`xyXrweQK(3HXM6C3Qq_km+IN*EVgj zA^ks?*&mI&UK|Gml9>yMq~$ z#yy_h{Xxic$UaM)_Hz(BJcWbQ>*dqfUH&YdJ1svOt??kVo$`N_AV&|cSiudk>u1HI<*D+?)SuE5+3zgDi`h%Ii4zE#lGF{z zZ-fl-5N=vS8*t~($nq>j{SY)nXOWln*7z-$LR_d_8Q}rw)bFT18Dpj zhTCGx7@1=_ef$mHz=`{~lCk@ch3$xccltQ8{>XNEEKiAbtd2bi{xkmoKY^v(pDcxU zzitD4zGt6}&%j}`@8E-A+m1&24#R!8fg`yrcGAF(wu5aTSnJtubMkoGT(>6-A1(d7 z<&V{cfjx3QtTE*9nf#}5{ierj2Ua&dY|a-Mvr4g)s74hawmxJ@P)jM!PI? z@L#r7)-qm}zK}UR@)YLBd-B2LBa0@^*X>|%l_cA|pHasNXDu+d>WS=m_ z6E9nAWM3q+*Y{x`;0{l_Vh@&Q2}A8F?YH*1{vn*dlpiA+yD{-8$flV*EFn4u86KE& zI5#C=S#sMJZ|1XR4c0KmduY+JJ9rTMuq7Ro z8HL}{>{|o$n;85c?#_Gv0K)28rFwsf*vlUo9hbKAkg3~0B+vf<9HrmM+tmKTI6~(B z1U+49#lL4-WD3Kbmh!;c@sZf&(P(2FEEaE)5^cFF*2^U+TMy#zv&Sb=AwITMo}7rU zv6uHb^YPp2yX<_E6}fKvIN>h24DG|o6J$4I)3N$zzD}M;a_0GclaszTW}Za;M=l8R z+14kr2bS;SXTD53df&J?P9Yl~E#PZ(304O#W{r&4FcBGG(<2OpO7v2fH?9k7AH*n?Bl&)SuxQ5qL?0SYSPfkFjX>(fp9UuSl}_CykxP@h(f{)O@=U z-@$fyAJ5~DfBbGs9z^NJGNf~`)L1aWLD7&fyy1AW$Udz30`o8BJUJbcxI!~N*gS*N zIOyBv*!7)qbYN3thr-4(LUPfp44zqRCANNZeUknbp28Pi-y-L;cck>#5!JoYdgBYN zxNN@?dfbizs($hVfWWFFpMSG>nD$sO(dY0z-|YUSIy>goJu5N#D#hR zc{i+vpCQd-F(dh#+~xLV=?lKEpQ{#s3v{oF{{T#Xhw^`mZoT|z_*wW{)u(2CgMz{? zTo8iAd*qJV>Muo$-?693HV2L4Eyc)M~@n!@CIbMp!LfcGmJgP|N1txpUU{^Xu>-eanX| z{CgNnzk{ewSzG1epZfsQwsQ_h$X*Lfku8<`90VbSxjkeLUj8-w2u(eV_b(K4R@(`j z+I|kA3?nudo(05v{;j_L0DF3ug^`C^sR%jVyw z?&)0meHmN%T%{?CH9r3U`dG`eO)^X`Ry=7rbT9X zAn_P(=D|d>dQUpvuwQpu$_`Em*Q0BV{5{K#>j%OC=i@B2N%H8}a|P>`ju#BHk@gGg zKZXOXug*ka)>egU3Ss04Ly|-&G1~NbVb~7FU?oy8T)r7?_DAX!eVguGdcZOPXf%|# zirx!^J8pxF*dFBd4~>h@B|3R+$FjS_KDpQK*T~WT0R9XChvHfG6FrVPNIbk(W|sF> zz^Y8;it^LBXt{H7G661fPR9Fu>>1_4mzO7#4m{#%yc5 zdYAgO)J{ENeEb_iu(knuU1s3B34{j=CkytFnA@+V>ch)G_`+JO`6TQT2V;Ijs_O&N zBlnl@F9d;RUp$m+mtYB89iLlAUdx6B^xrT+f|d9{ZZa-R%kW@z=`3zVv)|U-lpAZ` zlM?F5rHS}3gYpR2ZrvT6e}wbM-&?TW5GQ=J0SVy-+2%lv<325&*m5Cb8%LLmUmnd{ z=i!sf*mivU1NOuV4Z9WiV!_Owa{dcJE8!jQ+fU18X>dC`t zY;y2eh^zQNW7{X~0pzbAATJ~LZO8GY%!J;!bZ_{OSBK{BI%hfDeS5PqBfXo6p4-e`xF(E#-%^JdQ)^Iw22Bp0G~j0L&kd z9U=RIB!Y|zE&D^M9kaZT@?1TEz80|c8%^Z?qGyuGK2$a3!f z7dt8KjhwRmlh@!G9&EaVHlD2RLEc87Lv2%PK0(9F!!Dn@d=UvZKLe8Ot*mxLWEW52 z$!3Yn+?GKsjzK(b+S>95y%PfssyFb+(YLLK3xV{GI~A0W2AAuN<+dYPD`9#Q?Li=Y zgp}yd{^UDGxYhSA^8JZ_HA#}|Jxz($GuXTR*&)aMqlg&k{PGM8oCnA^ZY$D9vkiV) zK(hY;au!LTnanYQ6gu`d#CNeER(E>@X~YgK-rI6Bh!`Y3-5Z}I8YY&J-U}ux*V{vo^nwnQ^Njsk1g?O@V47|`NjzrNAq&r zl#a>law-`cyJkFtJ19Z+=JHp`d$8x14&SpNu=x&4%t75O`*CgVH++yeXgW_ExqdmF zmteJSM>{VTqmq8f6M6UoFtR!1TinS3W6KvM3+e>1b+YucJceE}-??Mz!<=D#WA{(* zpWkoYpWh#i>f6h1Bo%`Q7+ovC#%ttb0qj@V{BN%s@)T#-3F;d1;C-@4G0RVX9$($+ zdlALi_4orZW%%23Z-AD2%Q`Cuo9JhJW!gWbXs^o@PUoqdU4M!72Ez3}{{V3lx)@>KDe_As|&PCr=N%P@7P=UCIgT{#K5zCfO&9v zH1PRg>ps6OD*_kli{ziB<=8*4WIc~zBb#*A=^8gy-^v|Hx4zE<%NW4xKa;ZXvHj)! za<8x8@6ifuaQDf>O>8WVrw%yM7sy}>qy&M1Bl7h4+WPVo;NHs{pc;PQ-o*C_V;Hyy z*68J%z6#Z`7BErv?S#gjz zBs?3oIJ@b7jDw^m)&j=)<%#R~&6$}eaW_||4@o3|3EziG^$RPX0c2arVUi zmo^lQ-nVUmt&}=@8zpEiK5NFVb$D zHg3G%V2o`%$k``AN2v-saQS}W&}n@lV|QjDhBm`^xAcVMG3}A1!Pyg+rg@T|WJo?E zU5hs9$tdiu<1O`Kbz|iB+a2xt5LaU~BNF4EFWT;lpWW9Bci&_g}69#PY?M zY?z_R6Yvq&3Bi}HlGGxzWh}TL-u@Fzo=d;}pTp`?GAG;91I?iawzFAG@@9d86p*!Mb70S_=w<9SKFk*g z?`)iEUc%!&f$Z(EtJplY@z9$dSq;WT$y>VFMvccdQH+NN^Ew*_#K!u8aCf-2G6Ez# z`1x&TlQ26mbC3^b*z5?#S;ih0tRQY&xj(Ss#&E(CV!&K&`DR#nus$~Lv6$U51tQ_F z*9ilMmt8F#J-rWM(q4!MORbajW#O`I+4fsM2k^X3c-W7KKJ0<)hG=#>n2;l4_WteW zTum-~vz9z>eo3eEU~<6ZpOKjXFB?Bn3&}CaKYqY4!2ab|w%PQTFov+RlD{BqnJ4aY zZ%NhD55aqvZRW^^#PT-DKXex9v(0QZi+j1{1epPe+F;k2kFcjB7Z{3k7UU|tblTOU1{V!5Wak#b(Z;d0Ozt= z$(~vE1=!D$E3lW1g^3lzxgJET85NFR$=j9J+jJpHEtvL9T466X*nT$_-PXg#!}Viq zckoSy`F{`uhsnb8*)y{FHw%Mi$=E+0HtpB8I{Ys<##-6+xo61FVF|0L99qfo7gFDh zd&&Id;q8aX$$hz+lO^835{B4J$t@%q%a)h%zbzh5)H`$R_BFTdnP+A$*|tkf$#r8E z4&7}*`;d8K_&=G$$$yAo{u!9lZ}4mPVeVUnf<}HQ`rMrP9`o^igH~eT{w*(r{3kcv z=3QgU2aG$Ghb_^|&kyGDoMYRBruJV~8Srrl$qZKi0CFC7{V8+sU~K#QwEhx>m_+5w zte@f79Yglyf&vvl_7-D418|kymi>$DykX8rd?)#j2Fmq=?a^`> zjE_aJfb*8VSZ61Q+I#pf=Fdf<%(V78u|CIqj5%ZVX(L7qIpzIZ%lNbReeHL!9@)#+ zll;g|zFslCIcu`!-^6#8DFbu&JM^{vTb+}Bz%viRLVdk?bH?QJ^T3o($1N{}4(v_R z2uEMQ95#%99Y2k1NxhlF+nHv>)bekosmODY5xzjHui+d5hZKzZ5QiokJ&)0WG;=^euu4tW|~e|72oSf&SI zwx1?GBmA~+rWNXb{{T|-Tw-8d3vZsCz1kgTOTR8792;Za!1T1g#D}g}P4djtX7=9c z4@cMmI=GGXavd=S^^kf-aJ|Qa|2A-yg*7Kl092eZk7ehx^zy(jf~?V6omx&7Qb3moJJ&t)^{LQyU!sZra1 z8~Ir`r{TO_8h$ScW5~RIq~*igB8zs|r1AEMJ^&v32$S*8}zt^WQGV zm`UDuw&pe;VEXwdd-$e~$-G^AJHt2`0pu&bNbKvNivF(^ud`)IQ1$Q#Eln#uWZ#?01m#kLvSvyV%;U^#C&aOzz6 zmt43O=dt>{TAlnPYGG?~73Gn4%d=(u+~bA4M|&)^N?nrsJaTa>C9h@9lHYEV^e!PM+b0&}JD2nLI|nVxb+D3S;v37$Mp+K~ zA^0ENyA#qH{{Rf!kdWjPuPryhhwku8M+RR*b?i7=J|qKP#}^_)iv@GHHJHOGr%B!E zrp)8WkK$`tM_O7NFS{f54Eb$ri1Q2w6BC)W;BCGku8GbQMt`w4(*FQUGQ>Z=O4rB^ zth2iZr^~5s5ec612JgwdfH=k@L%$>)X9pfmUAWiF2XNoFLm>gVv4glFZcc(9A-iy4 zBM9?b_1R7Ab{pC6**Ui5y^KEo0C^c}0k9a`11*VU1iJEnPqv?KK0pj)20^xIWoHJ_ zwj>PCvF@_oN9y^@!gJTlc6lCT4;xRmJKLW;u#!%sA17gcmg6j*bh(IQu12jSuCaQ~ zpvQj#XD*%Xze@*Xe1x;Y!uWwsvUAxxyI}45+4E;cm+hADxF-)kH&(Xnn{s<91F~~r zC-I8|?7VD4V`*&PyX3tXeS^V<`rLi7>4WwkFn+?;qrOkLzhp_bq?oktS9$H6KZ~V| zo-n>G5Vgz3>2p&XFD#l_mVYcCKMFQCM89hf_YmyfvnC90gg@d?-0YJ5tsdpSyB~{1 zmUJ?M8_b+JDb27vm6?-{HbRq+eT?;qsc*vp<@1*CvKzNp&rO6qk`!^d;cT09I~q9- z%MT+*S^Yrr2go=0u}j7|Vz8}GZLbN$x^3yUh1HtmKV$cJScE;8zC-DC>o3#WRoOcj zY}J|h76D`&H_IND0CFFWKo%b)i~F}clL^*OgmC;RVD=r-53GN1=NaQmqibv*sn-@T zQ`yI7^6C-b-FEQHUhD(8vd*ttLE8bX&yyCfWz)AXQ)7PFvWK>7V#(GuT~>_l8GA11i$sI6Va#lMhGV}EBSma{()1G_5%n`<(~7}E^8cgs5` z_&{-dH=u2YWIvt{-Q?T2NP0kRL_>e89{huJms8K~--Y@keO-KkeUH`E$@V;)eS=Lc zWNF-&J0D9q$#%op82HKGlcyo+mkz=+kbhJB+2yLsuWwl<&l{)<%TRZ)-()nh_2;p+ zJs4q(NEerB82vy=M6^0wcJa%|J`?(H+U1Y~8^*(#_C`$nnrjVpzlE*iC*c}k76*_o zc_8~{mNN8Y>IseO@$=_o`SsdH0epG#(be)*vW9#up5Go1a$Qt_(;KzXc1wh@- zddT|7oip@-e>Rh-KEgTpIOW6x$+;HhlbS-0Q2h3P#Ds^~AJJfc5IruKkaS>m{{WcS zowMT-`Pt8`h3rOszT72Wckn(L1mth}u=?DX-9S8N!T@_Aoc^azEjrjYzDKYL+g;D~ zv97TP$kfR`K0pll47A+!aOL0hm;npay5w`S(;txX{mI)qD#FrgK!At*zZmw=bhWwU z&AAzWR%`gY^5dj*_Cm#nPQtwlN96O{^)AHt^7=qv_(@M`AJXJ4%qBDBC)ph!9>QCn zU^H)h@_sJ4Vb~8@CBp@5`8xT0C*sC1yZFnGVje;xZnSN9@?Db|V9bK(Wr5#rdQYP{ z5d9|z(+1vb4{|fdApBtQwkt357LP2F*M34t@=@i-vd?EP>T|4C1-a~D<)@N*u&^cU z16ddG6W0r@dh%#J$Yl51Gf5?e1|y7*2>LEcAF&wI*iRT}qxwr5OMDTz!q*b`H$F3n zeJ0iMvp;#^zqDfKs9fc~na$EVo$~jkyxYJ?V3ZEsNyBfp$KAYkP;s|ki3h)8c9ZkV zr?v*{Z`%@H_wn*jTlif!$U&^RwhG0?C{n-v^dE0|MY!yqll;y;SZIU$Shch(Fh2Im z;~0Ndo=EfT_TEk`y%*AY;{)x-mJVHUm*nw;7Sr-*#+Xbp(a6!Ze%@uSHDkQCPs{tf zVfFYx2;NFp$Ko>Aw(PrYV*&`$`U3V%zlvZ#A#q>_EHZp)KbBv&u3t@sag(_**ZPq2 zEgjnrCq{SDTlU`T$ZXN0GtHIeN}O$>u&1%BthJjXbJ_h-yfN%rw`Uyd=Vt;LCPB6b zVeT^iqhnL%kTcY1v@mg#yohoRE|zV^lJs&KcJJzVy;vtFW?9Rmg{Rw3v;0VS*zsq3 zXPMUsdHmfG0m?zhlY(FJ>%gurr-0DN*aoHWUkV%X{8#*+! zw=FzaT*VGHy`FvB45{Q7vJCZqQP?ZVc(Kt7(E}T7;$H}F;pLZHjX%L6!TMk1PCb`m zdw(y@NKCco5F{(7;Va!qv4oFDATL=NBQ%zI0r@TDhmGZ3al7KgOg-g-<9W%{=O7-k zC$D$wn^Y}V!`EFb+ zLm3WgK%L59FuqDXXMMVuwm;3I?7oQtHCmp-x9~{kAkUZR!)|5SUf;_im%BJ*@Z^Jt zlN(+f6M9$l-ZwPPfR%(=MhLWNSGL}t=j zWt({H`ExP@a^Ix9?5gtMms_P70iE@O?CIDK8CwU#Hf->*X2uQI*!J9aZAQA1JX;qt zmN%sLvTMs{rKQH7t0S=<-@?i5+=cM`L+LNe2r}$pV%dk0AF080Dd*rCSFDKsc$ahJ z1uXVJkz*Zc41UvPdkF){K3+S1<+q-%BCLJ){cnOvbc6%tr#0JO>`6=|M<9!?Lmjcf zYn;E;oIh4!zh;DGF~~Cs6QUJzVQ!w>1p|{Mg24VS@ZFPaGBcCpOg^KxR%Dy_4S63Q zY!mshy_53U4Du-KbnCFcW_^rYA>{JrZ-% z@o3D3aI!mc2kegW3gIgwr{V}v+4FeDcj#v~AaI&_=lVdqIB>lFtgj;&@Sg@+!(Fxw zhChsAPm50&VaT&vLC8;5P;y)C-ZJ|v+efxtE|z}%kn71D^LE#_(E)RFhTpI!7}2A6 zHcF6h*>KscHV-XZ^1#G~G;ac3cE_#Vf!pA>Tq9ooAQAjr_Q!>@*OQB|EXGfzkHdG9 z^hW3PT{$g}sR9Au*!c%U>5bHJj}*P-#9QyY^7xnPKNh~qy|6Hx5(v=b{97-Qr?b)* zt>wq+2-JhszeES9G`E}h?e@h!BD}H_)2>$IN++ahD{XUP3P~ z;>#}0SoDwDaq?eVD(h{hyv9?U$2D5G-*NuCZL%&Vw@aVlg@YA$Po7`6{+B%4zDV9c z&u4|y`^yGF?BqVf9Dp2$>i64%RW|gV^|8Ncu0nnucEdmpN^A$L%^&XDqi)5M{p0t2 zFw6I|Tq8V>;GX#dVP=>2V{AS|hmcpWLi;K1NM8w4VHls=Bdwdjp23pfp5jGz2?e_ z!X?Kt;@+9|4;W-tOnQL!TmlMt7{SfF=_osH&);sEMs+CSIre%=j;!4y;t1jSIFq_V zeYheY3zyjg?3=8#CGXj*7Dj6U@_*t7pDz9K=cIZYk;H+G#3v;q{_|!)Ms4;-eS|C) zzCQ&XUiG*?6RX}wk`6&&Hs`V(gO}$lvV`>HvU82mjR0rr^^$5!nBU6g1f0+2&F#Ky zdmgdKK9RO5xqh2Tl)bq2)ns-*h5N>bWUnla^|Y36t^3rT5FIbugqkClx=J|8^+aa+LcsI-K*08$}Xgeu8_{{cS@pmzkbdqcBq3bDPT455} zayr3$)(@y*um`z35Cd}H=lVtc4Vj!xxEpkotT-2wr^9MMdL+JGcGqvY8?t2fT(fB$ z_cwgAa@%{-(j=&9X}d7_Bs5Pkj!ruxC^( znZti1`avfwohA@FlAbY{&ysjXxoCzXx3HaynjiaUy^5Q|?2f$sN`C(U0tp}8gOn*L7v(UUa!kfUZPN*5mfp6{{ZhT>dny3Sa!xI(ov#9#r%7GWG^7jBy*Am zbse0!VqTJxQ00^yfj^lMIzoJ3u~@Hpmb-o1GpyqnVYbg0UoG)q^t;@|*hkWOY;^6m zzgEEWDhppr7caRuOAKTL-5=szBw+)DLmM7X9JBZM#>Q4N9$5P+<$&@_H_OKu0sPCo zwaFfQnZXy?Bsn+WX#`^{t|RF_lE^97q?qAohCoh}_9NkxZd<>MkU@xeu~0jbVV?HU z#Dji74nvV~Z2nAi-Vex|0v{yD>oMJdo0m4p_W)(CI#mWn*<{_R!K# z+l{K?_ZCJH?YEA=$S1M~C4HRs9K^EDJjVI@Tz{K04gJbE-8q)0en$H~mqcf|3;IjG z$jXlpiHrOH0A^HpVxJ?uWE{8LKBx9WnP&s744ro%n_C0MDHUqO=rxMOPJ3&W)~J!9 zHA?BaEwy*m9<`I$qc*KkDz~L|`&Db!CN^n|8YL0L2#Q!ih>U*uGk?D4J@3gm&v~Bb zSDIV~`O7iitYPwe{q9iYc&ov88q~82m0Bh%bIr98$s4{M?3U_w=+;*9y^;w~-B#-j6qdA3)s8z6)aaL~!XbO^8i# zci6e{6^_#9iL$;ur(d zD9vAJyI4gIUOWG;N=r(neoKM5I-VN#3%P<3lD4ysy5uV!z>d9hhLS4H)KY_$tHS(& zik`?6pgFvb{)f#xR%xkcAeTcywA^0BQH9e2D^c0!2E^#?Ab4rMQ`6y??|Drc+STh? zd`7m!v+7lRyZ2^UP%B-Yn}2cE_dQx#-PCfb7N#B;1C38;|tl3?H z^`5U=90MnT^+UUN#)ch~!G5QxcZScfh9OK_r_S~b$$W#n3H&{C8eStnVH+hACePHK;TxvWQ@z8AfZDVViGC4v6@KPNCf{tO+d$sy8@v1Vz%W2kGw(fPdC)wKw^w-;r)I0xWPL|R zLV?U3Z$A-zochz>yu$jff%74>?WPxMcuKPgAiwR(Hm@eM44N&W*pz`td*=cx2`n>9m!ntSV&O&ubKu*pCP zdtbhPDRTy*A^^)n*I64n)%vPrzyFJ>suKH}a^Ew)zN}O3R5|CVQipQae7Hqw;Q)hY zC^KT_)MWvtaY$|2SLG#X{e83VyWX{A!?~tANs&vtwS1grTeFS!xl&_ziTy@*q7on= ziD*{Iu}0yjM4fU?( zMt1F9A<*)ZhNjY(#n`-5u7k=Mdfm~#VfpZ2j7!;|f5v7iR_S*4w`H^~^nWx_LmrSy zNa|>6pJ3!4HeUB=1a0^g#dG&({eqnILPx&M%unLX)fTCou#WBDfeGgxDH}^Gn!Rwt z9F2GDs>iG?#b2MiU_!Oc7E;nE+dRb+hBL>yJF;9aM0v1^_#4G0(W~GZpYNMw_0=#+ zkOq3(b;D0qnCHK_lgX*%uhqNpT#N!Nj7x9Kj8L5U0fHN3PKA7C`pVti*Sf#<1#7C3BMSCD%?yX#~g9d*Z(0vAm|-x5`aYY@Y+HK+Et^^~8f^G;jF7 z3L5C5;ZM918k6c~wfdeOqlQ+3w(J;5+0|T!mkM6y`rpaaLM1;se-p3jqL$fm7d!g^ z_W(OSNA@{!q!ecmZUlG@Mh7@jT__9W!EHDa0)Pqz=IVFm3THG853O>~j7f)5%Qm3! z+=5l<3eVrJO2l#XEO>P?jqTiTj{2XP3(Xe(#J3K?O8Z6lOV0IusK}RCr)s$0LIJUGi z%W7de?58qtTwzINtbKlPmp$APx@P-V-pSUM;%W!O>?Bh}&7^2SZhXF=S8UdDLG3Y4 zwj9oObS*L0*}K?;vXqHzT|Ix46qb8o4HZ8Ay)X0puy+wZ`B8|Z4BAWd*de-hJla}x zmH+y?Xl(%_y@+13-M$HAN3C+3@xHwT_g?9|RnQ#5*UanwVd|2WH*;3&s~hNE$y(($ z@A%-qfi%cTh%|*~pnEyctL)XT5IFbtqdT~i+Y3Hi%?_xCxXQZPfN!@>7xi{fJP}D>XP$%zD^bIT5aEdv66z1bS&pbU zHOUhfU%$@evs=76lfg3~Ymg8^bkQcuWKBiLGUcRD-$fEAt+00;;lia(|0Y7Q);VuJ zsS)8pi(sF;H?c}yC6MA z_r)4G8l)<@`w{@)z@xN;{EcR-R2PA}bMqzzHIkK*kKyl-x!qCup$&tTIq3&vz)m|+ zRdWKCR~g;AI2RKdrBq|Y)M{I-t>TH> zw8Vs-PLn2YNs3h6WHQC8QAf*2K6sP;*LXM3!yHCuFMf8-b4;}6%p-=>@!l;rTj1Gc zJ=ON;uk?HEU+R@5Lkm=wYk4A0BTiRhFJ zJI#B`NfwUU0%EPP*Li6H_v_>grIJHjS!k2MCxM^KHeC~IkCNP<;i~^U!TN6266MTPWE>y~zv&4Q`$85+zs zF?X&bF|mTQJvOhFSML)iv6fmlc10JuqV%%h&AXSaVz|chJO6{Zl{4rUV}?Uzpo*1~v-iuCK%FP|~mnHpk+?Sx(e zTH58ti-|T*bIrn!+dL;_<-e-Oxf#1x1Kf4=3Yi1CejbNO@^h-V&J^yjF*a`xUHIsuE64>~&{ehgwx;c|%0%)mi8hor5G@ zIZhw)F~}jl-Nk$Ncu&K-!tJ@ya^v_sUAX2jc%!LYE-DW+7*v+TJ^y zOhFf+kh)jjS-U>QbxUWA;iB#4uX>9YfR}}!uaH0^9MyMg8=o!O9it)cv%I{7xzA<3M`Go7WQ_F ze4Zfth++h)E6?xI)!nI?e~C3VAU3TQoF!cIXfZmF8&%wF`EA^+ZK_2N7k_a#*gIwU z-1iU*lIK*DlEB!$d9y-S1vgM3)U{(vOt`-)IP(`^XP6N1-t4eh#UqlHN4VFbJ^Bte zlpD_3N^Da>@oq#?(bWEH@=J<)(wD)%5(+?CV%43p&)0G`L!GWo zC$S~D*g{=sFzv(ULic|8;UATbf|$Be$y5mu?Cs|vcME_U*uom6Ep+U{yFBfFxr5xJ z7B4?9pfa`GOnr*qufje`D>JQ{!Oo! zpI{y;Y;0}jC^v>Z9}If{AnV(TfQ-9 zWJj!EPCzN%P(eva+q*S8Ju;i6_aN!oj}@juF_OKIm(sRsHgABrb-r!l4{*s zxkh}fkRxl7x(K%_OworU!htO9SZ# zTI3~#18iSLCVwpb`-We}J)pN?U;EYn8Vtm*Lf%Z`a*{B|AoX?I+uQCEAHroZQqRwS zJ=P2kSB1*#JAX$trt*R96J;3lj_-1Ls`HB2#tT#`^8R(P$Xaw^iM# z(3H<|%^!)W1hJN(i+O|Ec!X$8#+-;=F<9Jy<`POy5y8v){Zs%Z7xo*yf^@QIG#s^( zl~EY5hqtOOd1@M(Ou*P^tk$|`9yfWYBQ#Ts>ItK2?~?%42bhRlZX2H`KFVHM$C9Vo z)Ahn+q0u*24RHr0G0GyAe4IqG<>Ww5(E?{!TNLDV;kuFDO6-nD9mX8dVOk3K$MsxJ zUHd9yH+)qvwvKzSQa_n`wHU^#_VpJeNZJJ&@0o?}n`tK=5fR2;Ayo4nA-L&O=)p?(DP}y))KjV9t1zdZsiMNU4P^CwoYysxc+BXmhVtIyF-4AB{)l<`;b6(~eA) z=?;bB>1gG#j};G{?%}6%k7VZBiF#B0V%K6TV1B`Fj9i9{nLURPM`v9K+f#}-$d-lU z<{Zfd%i^(?9ab^-{d5l!J14*_(^41|wCc3w zb|)CA_gJOs#K6Xn$ZG|Ghx<5)@ zWx?$JJMcWzJ&*F3JhXPTnba#xDN1vwM3(QaO-F?Z5R@F#!^lJWxd#xjyfZDeTF7IL zFB#{_rw_A|sL^kWc9l;`0~d<6HfP{yHDEqGJ)o!NzIPFfF4gK2nZnl6aFY+I$ztM3emvuIr@yG4PxH8-(iwf_pVx>*K~fxc@ytCDJBwHvshx+hUm z{TZMIdV|4Q@}$$oF5Am<{1BYnN(uXbT5KnHo6NToCfN_X08pR|FCQkXK@OssJ+=t%apgTNm_U zu8?CfcwjzFH*6D)PM&4EYsRG99`a^ zISttjkSIS7_ZQmP1_w>|``sw_Q(baO{6;&UF5TYKv^c1;1FOl!BsV7^F(n1!g@Pw6 z@n9LIf!l^K>`W}RZ`MgIvy)DBoLdlD4}fuSZJ9=w zlf4x_n*fPZ(<61>GRQtM&t9YGAhJQcp1@XG-3{?Qx!8p^!li}hMHiGgMH1p)yPDSh1I1f_o;Skzm zmxy{|1^FP#)B^6n&TWzpDo8z1=X0iDMhUy}Di1IiuI-3b!$FqV_VEY<{R}}fxH&S` z?^3U#okCu+6UCoc2JoOVPQD}|qyWkLnGymL;v5a*KNhLa-==_W9gnq+*8T4QCw+}M z+7!9idr(w`sasejtI5szgWw108{=u!(Iaj~Mom zRDLtMxvr%f=JGdx)%ZPQpuw?Ocr#Tc$|r!_Jvq6p=K~y6GgOQMt8<9!sPIO}=wt4f zFJh}|BV8DNOq@ELEo_O{ybmGdza-qe*++`JO+pJR_x%eeA^;xhtcdK3jY!9><$8;@1r>~x_f^}hBz@Ai1Wd;# z{to_{Mq6}^zeTZMuVZ9AcdGI=_Y1f)fe$J=85 z!$(-kxHzxwHC8GLe_wT@#39}3gB9TOU**#z@QOqF;C#|qTa>W5(cF=LCKn2hrYHcIeEK&Jokv` z{}|uBsFx)LI%r_CCM3g}QAbPCU?0zKQ1cQQ??Z4+Zdc0^6N8EjGVSH~!;QAjuT5`c z6Iv6%N5_9GUUt}%`U zId~ojY~GZ-Tc&)g8IozyLcQ596W|&)`6T4y8^!(uVP;MeG_HKkwT6=5@QsJC zd%Vc}NGmPr6W9JUApk`6vjv2baX|Uv3O6YtbpvCuKSkF+$?$lodpu& zi$`3=EY1x@HGyt*1USodawEPZC{#0^#fZc2l{`#oF(R)ErBnTIUREFM{Q_lz{3QP8 zt5Y^n8rde~@OpQy%f#`&5FTtL=nq@QL?I)tr#>1NIvp!?v!RerRIuXuA2$2M6=@v9 zm_D1zBs>Nu$h~{6CE!uF_WspLMtLPg$VRxf3x7K^_)@rJH# z)_$NWJSe|?T1@y3Hc&2H}@$>;XBnm66adRF%p09Kni`ZI`O1asS(lh^x4Q&IH3B&HUYz>h* zM#~dGq@}*I{6$(&A?ky7KU3l631it{@8nbvnLXE{GE4qQ7z|wXr}O0{Fr-idGLQJRP zseA7^8a9m&8!iu*H2u%jkM0v-?=|o@3~BJ%puI3fT}n~ExtXQ=>jTY@?{7!!}Tpu1;g7SvRq{ zN%kXBrUsa-jqz@q9BZzr=4cV_oB>XgX2*B8p^lrHAlBEPj)cHk$WUMB{L2p$#)3*H zGWM1sero>0VhIOo-+U;0x9i+KNyYQ|Ia92UU2EjEWN+yaQTlKhfhkmrgKiM}n;Gbe z$Rk%c0#TeSZMNrfLgab)+rFD9w8ddrag@2N%=iMHS6v!^yWs5xtX~h=w`M^%gs^gV zMtXG)>kF!*9aW+24Yq2vqNOt7I7vE`LU2HcU=h`Oa=zMjq~3c}1%_emLg>-mL%FMR zB6lcrCGsy@TDsJ=^MY!DTc(5lJ-?+;xA0DV!z^@STkH9Cm6j0sj9OJSW!O7{#=O7e zP(aD^a9zpl{8L+m!fMGS)B`EMAyoT)L(A2AGYh6~StjLgfOLmwS`=VD^*Z&X_3bH5 zxOKEh%OAE%Z$$kN-11poy=%258pc0B3W*>(36lV{`Kb?~mco$u0i z>~NjQv`9_99ojvO&%sZ$wF^i!Cky*}@xaB&>YyRZ&;I9;Y&riGiRAq*8m&00$Bni} zsPOd8X=i#wTrXH5)F=ydBcm?#eL&aoqgZplRSD>4DAu z_zYDOXiS3S*R!u3dCaDkBg8sv!lI}4X`zo*XzjQh$U_K6D$!PJLCoxL8Do}^7wq)M zj~jf8zFyM`F))vO-}$6VjZ%L-E>ip$c-$z}^SnEt!>hS9zYpzfk-g{|YgAm{Y2&qS z9HWW8R-hc-H5M9maY)W#Z>dA?MiTp4qTFI$z)CAc$y@}_F*y~N(%rl@95fVJfAPT7 z9lPGtYUgtAahp`?P^JnAfWcgzmHpI9xi%AY;(XwDKq%pU-XPR#;L0H?S{bWjgo$2R zo1XG)wI|-){B7VIsLq?0Tn#{1+nudU2m8a??~?kkRf_cF0si&P#~Z{`H6L z-*(Sds)ylwww_Odx*pfanmxWz=kz3&JvZ6i-DU%LfwE~Wodx&#$FTY|NhzwhDp)n| z@K_dmqGB`Yo?b|=618iBg2^UKNgn-%O?XO7$~xX;C@V`KgsbL2Zvdj%BDI|H5yLfV zuHSOEESn(6rKy&^lP><9D&LA>A5lx%!1(}NR_+G}zDZoilR3JJj?|XWzRyLKtFfTY ztk_y1ujYgSVg~LTyd*fi?SS{xr`LpbH>fzL`M&DE?wmi4*^@%KN6}!2N_D2b2*`Go$zLs%$V7ok)wCgE6&KUWDv7A=&Ns&$W)} zmhYT;O(k4cC(SG#MTM4wKlADF+*79cr_2jB-{XC1LHzaQ62&lq<7y&~t3BXm!KmQb&08YpoGN!2Q)F2ycu_u*TJk-!+O3QTPP zB>!zkx}Gk1-9W|q4_nEJVXi?ZUT!C`e5Z;}KbYyRIZK01=LnxE!=2G#;d;XW>8!}` zeIocj=hiNcx?*sG59&1sUUCCHXjzqeUtQ9AcoLA!3=(=FMR7XZ_fxBTv()CxuQ50N zl;y}yH;7T6g8WyytX=O1*~4Cp(~Hi&eN8p4`M1+2e{6~Z|6!|7{Y_yrDB8p2)-A8F zku}$ssrf&{r3d?#ksqGj`W7R7llX_NDXem_bZ^Uhvv_-aBAUzD_$X~v*e@QH)}p8q z@O9`5T6@zCYHRIRNH2WKkw1&HuT)PI-&;|Y73BLo9ne(mY=^`)YA=GL@@H93GZ9vAj415Am+m>ZGA|*QeaUatnGTM=azU3hfVe-PPhB*cqC`#RO1gF^?;93L|*=qp9b{-*xU(C1Op- z<7MC39B@V3N)~uwZSraST$i4-npcc0g*hjP-Vigapegyv7w@ZeM;<+NUaW(=l!*^F=tBC zY|5mSWcY6=EQ|1Yxj}LzAwH0=Ft$7!wQjzXwf?FPS;aKcJ16;jp?dIMS5}Qy@Efv6 zB#!oXT>L& z;y9!F{;;**GjK6mmOr!F2n@`xx>yI5Vt*Iy05>}3Aat)Dw61*-R*@LGo{1W1Q~krn z-=HhBVrblEbnaiLarE9lUSqkZ{jPLD{)C7odYyQ2m26b*)BYU)t2lZ2We%D!SFeHJ z#lgpNHq*^JgIq-}dS{3^W3Ws4h;yTw|GF>z)`(UjZcvUOK(ACEbIUFC7k z5(ymNvzm8gTd@jEB{Me+n+%(pv@zNlFdjn@i>(=bA$E-rl~(G#d<1fAx#{6;Im_~q zP=kKQr%}B(FfPLSE&|5RDuzZxqPh*_f8uxe5gatw_nw>A8VZrFxE7CWlLDeTA22`C z@6kp<(!508%%XwHPS4EYx8A_Mh;nSSeOTXeV+70Bk>vcf0#(GU;`IFxnn%_4ecE)E z2$eM5HpC;k`5l>%)xP6v1wN8|vEZC_TU6;IwFzHT4lRD@lnfQTE=6UM_ln%8BqX`#25yki*7b@<97uX-1gpa8REnWm5k4y9}GY*elkvWsU?% zK~`XHG#tKs{MxFn;6x{`0f4n{&_48EHE6m4VYGn2EU*sI0t8y<^Zwm(8F!oj@khCF z^Iju|oWA#pOx4lSLA_}TF7O*V2vFS)Un5w_Hu=Q5@QIyj%jU`S`E{UFKMrj8X>xEt zTDJWqlSUx;{-RKO%j8;-^MjEKH5yjp4iJEHB;^qQPo%B)p+sM=w6MXy!L)+o##B`w z0}HxUl)#=Kh34vt=K?C6^%xjuIdx`MsA zpbpqGviJ$|NboFgLV{Iw(elyN&s2DSd9b&)PVD(a-xqZG^ns0^|6*Q8$>|1_# zm=AyC(q#-98!3HRwEHqmyy8UUM&ss-{(VFH-%d$PhT@n=>WLr+<%yKa+ad>r)twfG z+{?C`CbWhe8JXD3tAAhqY+tcyi&*16_gl9190S?lrf3!KyfPT;1r{G#v|j2SCeJ^6 zzm3Mx)iB@ z7*bg_Ye^*8J4|`(2gwwls?{d)O!)CgAfwif79=atZH&}$ckU97ONvq(M@9H zJ~B8*4CgF&Qt9=}m_U~p;h0m&Q%eu#{O6rd`4hri#4_h6-T-=OPC>yZ*C3 zhj6A|NmTj^4SFB^7NXce^o5I>$#NK)k7;SD*i!ZM+o=f2D=yqFDzq)Ah=8LuF;V`( zH$lmBB5ai9kqxn(ucct`e2!E%#s#mZSyAqq@*@pJ)G9m@g%!_K9 zJ24cc%u}F>(cJmWq_Pkig0tA!LOlA`4g;|^zVr51dBz!FkJ1ak>noRI`)PpttNRYc zT7+H$@ebQRY%xcCCF-))W^fKNP9?;3XR+xGb~ra}pQ zHi1|7O%+O8TaOZtwMX8mE(V_2#*lvRe5p($4mAIxv}bs0JgLx|Q62C<>xtqvbE2gC z2x8r=>)%Z4Tl{+L{|i57O~LuoFUJVG4?E#>RaUHR3ax1u#tJQX1B^)!0YNp9aJ!Lq zv04nA3|NSE{Y(Ky zT*$K~!U4qhnD6lNO&!XRUhMx+q3@9LdF{<+!eN2PfIbZK{u^!lw0agPmhhyToA5DF zZTAMD&)_zkMwPc%t8Ehe%MH0MGUcDSimQdf8sxW!tppCrk%q$O4q2BFPr3-bPX-HS z$g^;<5+fjjiTJN0N@8O*$1~!z#5{HKsSHyH%B?6NV?t;WUB^qm87Qh1*E)0jFuuB0 zlEt96$z<*8h33g-fr!FBaFZ!B=A0kuKdpPYG&wrJ;}9921=kTBs^43I+*hfp8*_X} zNL6edKh4#F)^D&bcNOAkid7-bQcQ!KYaDmR{=vZtGN%E$tLUGn*jSx0^mnAfGNAyg z{1tqbH;ot(0I*izY57;oZ&0M7(2;@DZ}vHn+}MbUdM%J7@n%pi&AX!s5lQ4(KUtmO_j23U{B!;0N`QBn=Ml&6c-W~UH=WEqy=y^`2JZ1|!Ba2VA+?`;<;k26X<7r+ zTt&t`(82S#ZMi%XO06`_y+)E>@j$m#8$eofHifkUkVp23x{xaL6wY<{sHCuMDoUH` z<(IKf7g}bAX^(hHJVGUMLBmp$2}uWb))Hz+@05lJ2(`#qsqcq@lO;{iyr?w+R+$*oO9GVvNCAGsr|qbE{H=S|bn_Iu zkNEycGwKf;Ui*_m+4w=&a(U9mp<mn?ObmU zDY8`UQzP*h`QAhS?HzDWDT&| z4C0V}t1gt~=%>p5VWZvkW&oIf*qWCO_L&}WV%umD3vE{Q{vO$UU?XcKd5<+RBxhz6 zA4v-afizM$rsdUmbh&pJ-)>|hS4^4~wDG5W(&G=uC9gll@Gdu=>_@EC^aw(E&0Rgb zVw2BTRWh2DLe4Isljgj4!HWc(Q)=_S=y{&tv+NEeTPCcG3M+KRMHN;&ycEXL3vDqRvi*R%^f~ek##LP z;x-d;b^tn)Xc z{A-I%Yp9Sw!T>Hu7oO^6A3X1+RwSNq0NxX+u;}X3>%>(K{Z4pW@qw$@ z3f?^g-{ISgM2tR-&<`jFgTMbmmhH~Y;wkWRj2cc z1pGJ?v3}zllEJJ^nn;e}38uR_97uU==Pu^oq*x|4H=XkyU^s>9UNL25Lk-F=k2ksZ zbMU-gjB%4Qd3>C;)9qwAK}`IdS3c0#024mTE*S4;J?JT~1zx1+Ama$BSK_@t_^gCy z64ITRT6otUDaP)%nzYvvE4Q?<6%FpWioyrM1s^c{4*1@cTek+H&ly8bB;E(rd$J+Y z`>pUhsi=N4AWpt^G_;0M0kV?W>woh-&{RagPWfaLrkY=__0hPd>FNukZCSEeU)@a& z%1!(s0ZH@cnR}VB`>Dv<>}9DHUEu?;YXl3$y=q512OjV+L^o@5lz^6%Cp-Ht*f{-8 z6BM}aLoh3R@(4YC=vCe`70B)7f4zmBlqw^unO*;4`$qq`$4?u#`GEE#L~$dKNmFFF zsDuE-_M6PMxER$L*s-u%d%PnLRJu}k5#ah-ywtTcL+$^Cu=Uj`By9@Ry6KAV<*I^m zr37I;Em*t1kY0RFH0bUBCLi=MAF*xH&kzXIax;RecG4Wtg{0Dvfsr9G8UyjGEMxR=RAf5!J{_?C6+f?%AsY6=xU!uU>v>ARg46_joR+ z)ct42=|h;tTaHmm=1H1#kEyb<%%c}Ta(aPF@`u+{Ra15=n&3@W0Yflz{iP&${+F>Cor1Dx(an8!} z0-eIb!~vj&{IBe=wnX$PM>RQwd(TCurPJ=|6ch2qp7oiN64!yi_0!jfv<%j-O?LH= zhR)Zn8ak-y82?hWUny>6zuO){Q9JcH(H4S+`T^^{bZbS?KwSn6f4 ziULAVw_*9eb4u2u!9|e&M9#R>cHzng7Q72iK)GgxBFz zd9Pk$*hf>OkFTUZphFxI1B(ltEd*j|&_TKTI#REYe(u8vhuT{U+`QpZDp(3F@DJMw z-B}5ax-D@A>ZgMbXvwj8sD7U(*L-Ym1CIFC|8^`*+x4ulP5sPO(=fC~$IE9_^JnM6 z`V9-G`^gL;HTn`u^91m7CqPSYtU|)tP)*9OGvAe$upnbQo~$IkmV*pX36zyMi*ZQ1hFpw z)^zT7mt?CS$X@eUMp71Jdht=-N>Ij+yzULW6l<|76?SJ`D=_*gHZH+YR6hFUR02DV zVM2ETp9n@9F~7nxaU0I54D1b?`GJZ_j-T)QAxma{w9IsaET3`e@PhLWM4Qs~}5 zJ=idB+*5%Srmi(J9BSH5PSd^F5KjTIsYNwK)XT^vKgqs&Bz}K|F#qkW%auQbOy!Sg z9!Lnae`gunq_%>Gw)bQ8o73}UdMTn&@yA~1SwYzl1Cz5jZ=BA%7L;apP?0tq0sSxm z?=lKJ7;=LkQ?p$9Rz_Ga=1oR)V8?ynn(fBZROa}jh=wEygo24LY9B%G0Jq#M>l;@H zq+*T~|FF>uvo``KK#Qp-suY9NWtBHrM37$(ygUBXrRfe)=J<3$GZyg#GyIsQ0tJJU zGfzu%R+p2~{`HT%=OJ=hVlQ1jyQhr!nfGkH?q-aa0J-!kvsHH|!ArD`5tGGB9Y8Ux8njhh7r%hCTs8s_KK3;$lt(p=wLRysgbU ziz;!E+0mt&-*1{V*^g$Ve&W-kJm}j*ej3hHrz%^)MQ)3qozmP>G3GLne9J{|om{YosaZg*^FLf1(sb)u<_4&Z*~M ze!hI$2E30TaqDv*@=Odb^M55i6r?JSnxC9pQSe=@|PZpQeMN69Vc?`@O)`*Ak7Q)1v(v}_~;vAa{s+%Z7Y!JF>X zi)3ab6TDV%9=y^s#RwF1K2VE8ZU(ay%1G~TEXbq7?3sh$;Ql)`K<3|Twc*YnYY+3y z01sare`&@Q$d;BjX*Dc7;^Wx?rundF!1wUr`ftU>70cL6{VvSK-*^71?a<7`6-R~I z!#&6kNdZ5~%{OaJ>6eDy<)VUzrmNnopU6*BD7)sd9#by;svXe3+P8+i5i39&#wQ@@ zMXw#d=U4f89djp|d$VxF_)%_Xt{2hHLo@Gdq>1)^%*o6Fs9_;AIE`Uup2sA1N1dfC z2C%hXM;o-)X^6>@e?Y4s&>KN?mMn9NrEm`ZG<-|FESpMwuVq zQrOq9b{w1?&lgrv)8QcP|DHST_aI!w{^h-fhzZ%q3-kZaZZt#nUQsDm?T>D%9r9t9 z_1^Agi)88+eu*zZm-P?dp1gGk#;|U< zmH5Mib;&mEGd~kEYW;cSXxlU6;l#=FeulaAm-%9VZf7Qr%>{v40wx;kX1*jzrDnb6 zzv73+d)PvLgq^aQe17U6RZd?+&Drt!&LQ&rpKtr37tPvgH(bu$sOHVvF~67+ThQH^ zH4j~smU|$>V!u>XTBn}I1OKqS{&vifjs*5Quc%arA+kYAsthRo{MoV9)mWr zVk`DP6Y@UEC*hRrJsX4ec6ZVjn(?v*U4v*M88gjHn;2HxvNrkL%r zfhR!)eIEAgqP7Q8#PEeRoSKH}xjBU2M6g)HCTnQGA@||Js&;HgdnX`W^-mgMA@)e+ zl#bXpGx8&2_DM`4gavV0Cc)7R?RMmbYkMKXa$oi=D&pLfBlIcw%dtoqbSDVnBI)W) z5CynvyJQ#osgyNoZUhpI83?X|&FG{l198?ptGd{D_YI)#&90&W0_xZPEUAU%_!eeK zCre37YxKsNzdk2VPOE0*a9?eHs`A!FAaw@osbwDmP?rJMu*97Wi&i`EIMCld~?lUXxy*gO6BSCS8^YMa*n59$SCkb~;# zayoprRt1NqWnmc<-Az{2ssS$p)eadH`(aZ8INreh+PV0G_HTxrr`j@NPNf-nAlbL7 zOJ8MH@~)~wzZ4yz7X6?PiLEikkpaK>F>O&-sjyPiW20jy7ho+r&6!e6IONwj!;o(e zj^=6C^li8-5&6nWU`Yp-^LNyy}!veb9Ugg~h`l%G34Y~8EWY&(ev=M(?U{AaCaEp&Cg`!re7&sCUU zpG}c8d8DS}`y&!MKLVM>6|Ag54ZaRmHNkwjoS$=Gai<74?UVPuMG_n!ZvL%~?^r%> zyTtXyklnsuT$CSPm+ddoMs~OE3pBruqcv|nd7^-*8qPYP=iA?lVM`4Grw{KI8pcFl z+~d5-=p2$ko}WI9ul?8*<jg}@-p z!V%sTw#ikXqU}5){pWCnjsHlX^>vl@TjD8HeYzN@S8HNJ@IgJQFJLKev@Hi|Lwf^q zhNLBM(lItN0fiiO4Fa|=bE6`;OhI<|Gbdqd>gB65j&_nc(_GI9jYCKBldorqsgEp9TK_as3$o0I1@3tXI1K0BD!z{{S|Huq1vTxAzirKWpUQtn0Qp z{m9=LGTU=8#_H?soE-TsYyx@ym`myQ0LKZk`4+eSCEm77`pl;r)BCjt6Wbg<^#1<< zQ-klxNqfd{_Gl*J0S8{Qm&iWSnB%LdUDOk+F5v#^geHF!~7nC0mR>x}V2yVc0$q zvSLWSTPQ{{_g}mqXZ~~G_{M)y>5ZUdFz>dA`5^w&E|zEDK(<6kLNt4JbLE~G_ZS8~ zegyUMMUO0o!|#zeEQSgE-b1s*c?0#!$nr!4I|rrYk7H)f9N*jiFJsRl(U3!}i|^1=+45X?{;WmUmb$;oXg~V>L$lZN2Eu)S zH=dDzz?WN(f0i-vEztxRuI`cQw&M9`9FRX`SUr9I?>4Ava z>u>n7g_eiyFH1lc2%K)A#;l`$LVA!@orM4xf$R zJ0rvROm-=IIf2n(?14j~!UzQXfJ6DPN%q=)-}A{RW&XqdqzJIlcO_J;On-31>icq! zB|Rg(bM+78!t3OZ8{p)=CID>9iqg%}{(NK6r}sJ5K{!rp_7}3m2jCG5<={boSf;=U zlXZSKiy~+B6MowVd?3m81*>z}Ze7jG-CT#QM!1-z9{>#=f^!P@fWI6a_^VPhTC(E_^&1cR;!OfrOf)CX4 zBxEt{r2R47M+scGc6|Q;QeV-_2=xb%&a4p0-}No0@QK~z)8^TL_HU5G0Q{Gf z>w8Q761Dy;TiVg4Z|sI$lhoptz?6Otf9=}+j*pQJ7Q*znZ!FMrwYBA|%YU&;>+(G`yxnKQ|HVK^<~cJzv97g#aF*4J|_IqajfaDXU5J8-I#IG&Rw%* zuBSzuk_G-~Mvsxt?FHGjR^~q1-sk*G$#dQpUP(+_&QItq$!h;;~j5Nn*eE}Id?TFX`z z82U%xp+)v9&{3PA_UklG=ss3X72zz0!-8%h{ zoaf$K6g>T|UxSd|-|(|NFRlCR9cuz7zv(6H5q}r*gbaBA#wl{qKFNnr0PG5Vko}h= z{`qBlE8(?XNSw7tq@>vasBL=Pht|Q;>L55lyY^4q3avl@6?Kmg-u+4O`f#Vmi;T({U(UF7p zZ3Voi)-)fe)BMQjLApWG_8W$0gz>o%$F{C}AqU)n;J-|Mp@yPJPs;?BUoWCWV)DueX;BKf!X?Q663SE6Cb$j+n;0rKMjN1eE!=T>fc0I zxS8k6V(||T=$o4Ub%RX&uo}OO9!mJn$HLkVUnOg!^Zx*EN7lhTA7gyMpY`}TM@!|f z{sj*{z(~`q*-}!+Z(`a z1YVcYO02iv=ALek{{H|=iLg_oOAjK-!Tt3A01)R*W5q6KZ}Vl#lCY7$^@Z1?AwqA} z7Ou;0?8LvQ8_ayUV;OQ@KG~iO9vcDr5$W;%_DWybJh@(w#65+;h9B`EeE$IA@LBpx z5@qr~hwuEzwde5v0PVKV^tA%{XQ!w{Nez0xjiUSW{{SyECz4$Kw2#u>LyvCj(~{c5 z)d_nn<&}2>hS)PdB1M#I2*MWqz^m(E z*(7}ZTEoSy{`@Unb6QA`HfBygy>-HmO#xLCi zCS5W9AUB8m!0t)v$%D)N;(eFr+t)wXVC@n8^t{{u02ja;zu}AhMTmcH#QP`p_&lzi znBR;G={<*!;0cYU^?Zb`uSu8j8ofW6bNy5Q03|P^VU%_c*^(di-UKcstnnHSpYISI zmSI2K@d(%-0g&~yuG1?2ge ze=q)B^`*%W09tbG-waRzKNcT<2>66H2d-bNQ7@4_fFm-% zTCuW@U%$3Ed;a$i)bo~u_k{hwsGpQT9v2zNx^$g?gZiDX`67fy*M~qpj8Yu^m6{{_7C4%85+Ot*ZM=j>u$(z5IgnqbWNH| zxNIpeIr(*Ab;5EsT@cdl{{RayMjx`(*fa3FGBdO6K=;}1AM>wa{aab+ljQpdZaI+M z5#Mbe53-+WtB)hd}b-eI&u-e=l+W0NFm;M(Q|!m}>amXRHhr z;ERXJO*NPHE;|$Q7EHyV6j9iJ?%OR4pU>RkHbc@^sIv`)CB%9+2Y)J^>?7I#0Aj<8 zL)OQWA6pl8#LKcT?Awyb{)qto9#giYUH%X* z9kc%cFPEd$y%ZE3>2!aO>cQE)`z{wEhkw%kL%_1p+B*sxI~utZm~-#ph7g7QzsdWH zAAWxfs9h{T>Kwive|ItRZh!b$K8=q3jM6co7n6?}F?}qQ+;KmT+z;eQ;k{MzC&?4k z_9uLNxB5_6A4tB1w;=X-T@&I@{^>0>dQw`AKh||ofONI_0MRx!gZ_}5SN&nTXZ3v` zd9tl1qsu4>>=(w&<4Sl#%YP4(#zcIBrzM+q@21EI*aMPeJh2a@kly|_+9Lk|icJHI zHJpCZ8hK^?F)7W1$D<>^m)29y>e9&r)&5f`f4A%=&~Y*L%6o_LV*dba??2UJJ;Y$i zwU)L}_TLbP@Rm>_8Tc*CP34KgO!oQj?ve_SPxU(LFJ#jND+lp2tJ1}C==rkNvTj_5 z5@8?a(wsbp7Ax@2^GW?NnqDrk_Uhtb4cl|eINtjwim*U?_(Lax9?hBd>3+g!HwFFM zkJe6glKTv@{no}Gb%}vv{#1(f#nyo5zl7#l0RH1Y=HANI-afYo?BOSm_^}wzu6?!? z_xwnH%tTsZbM|e|*LH2;T?4)t_2DFZXNz0AS~r&*6X%U?ELo z6U?`lo)`4CT{1+&+l?-hU^h+w05+$dS(r#!d-69vJYi}3Kv#7C0Bg5l-}+DtO_$^{ ztb1@0K#c0k%4hoqegb~w7BHt9zurnSRl$+<`Cz7vFuRPi+(IZyyN$l2>9i+2~bhl%#F47h!@n$<$P@y zJ-xkRvfpLlhm)M2-1|PvYySZFZ4Ah7#ewtrf$b5C)qH4U z6Vdn~q_}*y-#<~m{Fb~Lf4$YjZjkIo^9zoWxij+r05_;dS@_Tj_(iI4e>1`pe_Nj{UA^s;{5 z%!kjDboK3^J+z-0O+1){#P758Id9ZrE`)!{9ta?V7SBizmPz_&gBV+rx$LaX>P?dOE-oHPHhvc}wy_{3;f2+IW@kS&r_&j7) z_>z9`$Z3-4y7^!PHj!D|V?KYhx7;2WSmFzsLUoEMwQ7#r&l9{zjjLnf^-&wina> zEe1V^4R1+vlkVW(fBwK;w=Y6N1O)7;On2kU_b={5{+_}grHD=!AJX*wFX0J@fIYsS zNDr|9o=x}C(eV-yuuq%6?L6xfTsn~W$>3#@B{3{Jk}R2f9FX$%e2)%Z#Rs1)*v60qNF7p>JW4)$ZS@MrUNsT4mlZgXN!v;aKq| z=APOl!VSNfPY54m__dr@hgjSE6Y92b_J{-IW&O|P{Y>~G983hm^tTPuxR(E%-IT$5mVZ}IKi2bzBY!ivja9~7ZMX2x z3pF|9n*LW&_h(1=Wc~odS3Ut~z6A6>*<<@<51)vn-Qq{9cV}OJztxHue`x;zyg$s&e$V3ti})>LlJ)TR3}2tW+z_`sKla?6v3!#N zOfQcZ7kA_W_{dOi;-n~kpZKtU``L6z8k_fSgm9f=K0m7fAXBeqeYF?60RI4)Quy5~ zry(cZgIDpE!=H%^+hr%I`mz==K1%emk{%x>aq`3;e|I_Q`}qfp{{WUA&vm!`d$RmL z&5Ydk8;hyG21gQDSnj`qdfCi(B2hmN@p*vyf82&j{wV#J;qS0*>GmV5NQSOWX#W7s z;D`K7kMfq$zwQmXjEx*^f;?W6rng_|AIM}I{{Z}uCr|g9>CyenfZ|xLpO((-zJ=La zf0r=!#dpRJOa0P%UcZ!y2#(?k5-ydWINPQe`O z2uhr9S&@>Pg~sjD3kuMcNnpp-9N)~D()WKuf6R&gID)O=^&~{HBfI7Nu>HTGCl5`W zi#dGQd39?859$woM34M_Avf6Z{a8Wkb1uDq>NVZRf6tdqWvBX0n<5_JmS-S?d%Cc{ z06*dV{^vK}_^Tn{{T#mx5ec7d=dU4$@^yL9W8p8$h?1tw-Gzza5!!-I2U<1C+Z!wmTZ0l zZ^iV_$LcWv?eEy%o#CcE`8ACbuXw0}+z38(mL2Yqb(J+oN@J5kvnZhWDG+4khDewv`rq7fEGLhX{{W+5CH_y6 z5pj$6KgY=Q(G&T8Cx6Os*FPAw@hgp^;%6S;B4xNa{?Tjx$y_#UFr3Rku|6O79!TVz z{4;+W`1)zY{gw)}W1d^H%&I@+k3Z1=0MuU2kU8ZUgZ}{M;zs02Sg-oRsJH&5bOV1R zIsX7N(8~TV-0Fv}Ehe&)p{XvEl36r9hKIPZ-fB(b)ClLVv0s;a7 z0|fyA0RaF2000315g{=_QDJd`kr1J=!O`&H@gV=&00;pB0RcY{f)F{GF%w1wnTh5o zWf;cJUO(g~czvcHZY4^Bgu7!JXH<0#V0}Xk93&M1G>vW+Rm@isn1s_^d4tpVGJ{hF zux0~#U@8Ovg?n&6Xb(mQqMcd%!{yx&sFYi))=2-XKlw^luhdcbZmC`i{)uB8pL8BJs$iWeA+JtG^;Jc(5tv(XKeDgt!( zE#C0P7U`8w6nzA;b%|=q{{Xzje2|?Y!WpwKW@U2P9|+0oFbR@urY&0-ghGqt9@e z`mo?%CIEr7$+uAq5jzpKU2;MfxnDq)an3P7ggJsJ2`{K;5erPbOBX7{RS3BBms2mM z1W-L;E^fCTokH1xwgKLPIM`V2m04YU^8~_n&({qt6Veklqym1VFHv51;Pw7RvdOiA zLq|Qp#UI;F31y>F+yqe;XB8V|@NR$JnP1bEb3|2H%tKw`b*dZ5)TFwYFpr|lWxyqK z3T`lsN;e9)inRukq`-KPD+I5s^%%(j#1+~%3liLY0H_kWm+D_}Con7NQi)C2ToWxG zA~F8}*+$BTg-kb*+%4J>g3#l**<^788`Ya6G2FX+KlTi21)R(2k&9b7oc$2430bfH za@-ZElvYSP zLzzrBm??uUZX4NSiB4+nr4KaA)FTTv$~P7{Cgv+Js4Q*^12D_va7^)|#450PgG#Gx zrc-=FL|$ep;OD%tO4Jq?9xy4WuEBPRnk928Yv{L$QwE~b z#PguP)M^?0BCjSeKJwy+kNY#pT=4~`u8GH2mSxd>ryI?}0&WwbnR@i|E=8=#Ho(nC zQm+*_4km!aSY9JUz63$+35k`K!MiH8~FgX(E6A3p_0@cnd1{su$p*cg@3YCNuBM7QWvB7Gka9$>?qAx|jzf{WT#BDF))=bRG^i_tj1kMx+N+-rGzFWlI^%A zokegdKbTs15SoMpV)_peae?52A27h5G9M5;L%!{~)k0b~1hau{dO&ozFrW9N3_F$| zgb31VUyKM-nbq+vGLg$|(A)n2Ax5;0-(KIu))Awk0lGAifNg~znRu1P$msC`-9a$^ z17E1Fkd}r3a|Uq)%60S(;urL0c*kYL4*`fJ-eOSM1k7v^%|$l>V{s%AY^l2@Y%swa zBLkFZLyYjklZlOqkg`z*%_E_)#mq$=P0GY}LSEbg-wfGBvL_aY+plQiCNzL&(T*zC zW=Q=|nn8;CZK>uN+)Tt9irBEsx`bp>KoumV5T@orW&rL0O07+mQk_axm?sGk%`pl_ z*oIiB;lwT6O3lF&Fb6Dah~3Q9#EvE0W~FyHfX;NAl*9-|$!ttYBEhmyW;by%K&(m@ z*zkvNXH#ItF{_-}37KtNa|4!H7}FC8JA&?647D6J2IWM78@a5^oJy_1gVacvm>HE* z7MvF=t)Q%P3LmMtm$mJOyaPcMJtu`p-nRfwk{t`VcDd#W7Flb=zm@AOa`xg{8CY37 zhIUTTdRpM${3YYA2L!2bu9<;sKqPq@fLuUnvoXuGjVo^Q!r?$aiFf-j4jz$kB)L}> z@QK5%JCwt9AITo&QsT;SEf){Gw@d~xSi}bQMVp9&WLv78h07X+H5klHhAg{AVwE)G z)>$5oBAtq2%aa>)?s!V+jgA{5uCN)o~2fNItX0ShT+cuElfVJ}owmFKOnwRUS zmsvFoBHsRiiPX&8rYjyRs0G{$hRc{GqZGQ131UFyfO8H^!Et8Kp)ZL=)T+d}YSRqM z?k7{U(gorOtXAlknMmSV4C0|fGY_Fy82WQKnwQ+AM7=6vjnPHJHaiExm#y22LBeG}xoUC*0FT@?WZ z0vr%mfR<_p1s20X60;@70PncpD<1;sAwN)*!ZgHsHqCN7Z{vvdRe55N7eu)V0fs@8 zL!?0VB~;Hg3}s!Z7Fr1J3{&`G=_q#yk|CY3TWSE>xD45PS@TO7l~JlGBfEhjU+BL5 zu<75JNexThEEig)03%WuQW_{?I-*$R5lSqX&BQfTQ9YT0^U4=h>oY~Y5{6rdqqy+_ zbHL7WyKw^3=?eiW?m3F7^(EcqvUW^5JixL2@WeNZ?wCe$OTVrqRKrna^qD*QEs&|* z7e+8?rYYCZ6_zbKiJSUdQaZ#AP_0CO%<@I9CQQM^MJ_EB5~3o~CC+7LSj-_WUqysd z=oR!iAc{{(XrfBJK&U@Jgy#`!a-5+LBpX8vvJ<3JQ7a5UOc;RfTq->gaI!hv#CfPO z!cst{3XWKp0+>X+LUP82poC`#7A3W8@=fABO*qt6F-_%(;Nt9{IOALXMvGI5)VCu+fzhkJ|FC&RC5b4d0(iFNy{vi%RKP|KY^O)^dYfJdl^+;+LRX~R~00J9+>D) z_Tibs9Nrl8A3#+*rLcr~nPHex$Sm?Q5%bhQO`r}iu=C7kXVcpZ029I?_~K;}X|Tgu z2#q5eu?A*5kl#$FQU?fyD*Xw$L8wa{vobNXY$V3>7uC-YD{d*dtv|A~zlbcPwMIre zmGUJ5s75{7+Z+h8(F!Pk22~604#|ZDrZrYyvq#|$P$PhOV#}f+!)`q zrs@SWK*na$ftY0#m{FjX8u|iEzdwfZu6& zD3+{n^mhLMnwNTl@jT0LzuF)WIw1_g+YA7u76UC-a~($?n&2dj9;O76m2Hg&F``j3 zFh|5zEg?XxObaOH+VK?CiG(LIujm1d8ihM0pFja&aS$g+ z?iPeOj_w|k%gkuzG{nObj6(SOQnNA?%{2xnM_8H431V9`mfaDrr?=N>S(j8x;vh=Q z#4R(x1wsNfE4Y*_ZJBm;d_cs0Q-%q0nV6kGu&*L2m}DZ*V9YgiN9`VDL;)HgjACxwd52@E zaFyA6G1?swOAG5QjZi8U`G9buFVDtaOJDXN7e;TGPDT0u0I)a};e%SAWd6e_T59St z$%ZFlDWx)^OF+{N1h+l65S z1PW-CEXA{eBb8|JDDT3m1G0dlC^*L}idp7h-k5)f1R)uwk7-I`CaH^<42>PiG<{fG zD={b0(0s~w4PsEJ!LXI?R{sFu8_FDQPUE370@0>uz$i-cvkqm9M}#J|xX(A~2B7=Q zZW8?W70TIZHI_8sfTcD5y25#A&s-i1!L)XgKSeQM7SA+KDm?;Rw3aH zu{oK88=R4ADAHe1Z&Ni3Ig6Q?K&ggh*#%-!BylenD!>|zp;f_2Y#@a*vLHt86%bhE z12-{O5R$3|RAn;4S&48}D^kmj*{v|k31;pWz8pdVvjQ=bfI`QQS&-k}HI!x7Bo+e` zKqdlQ(&eNMbN8Gp@`nAq!A=QE6j`I~6JzX@=(Z@!BhY;YR66DOif>TZBAiye!U=vQ zVySGqITB$+?P3!XmO(1f7D|?6m_?>mIATPsEVT@iw$GphTO5UEF~;{cYA=)3h!eF| zzLy%X=32ORO8~;6m3o2{@_L?3Wt1Zq0^T7hWa>Lvm+nqT(J7F9qh=$#+5+iPAx`I_AZ3opnw-ZwB5;-vE@O#k zw{Uue8z7v=5{a}{OoTJ2?xj4$g=G@qPtemC6pm%q7-dq`a-Gfypn*A-!X_p00A&qI zxMMKOs6aIY)J-68=Ws4DqY9IeQ3X>fw<%RqUnXG~rUV|Nr!zZr zaEh1hpbxn(hWLyNBw{|8%Gs6+sIVJlj*|wH9t};%bq+6i%;JR1*#zW{<5LK;gP`Jo z<~Vxo+_P!la)ua=b`MhRjLJ4@{{Sg)FNk>o0UqKPtggt+{T?PY@UVR{*c>yh`VU`nmP}&G9o~5ABbRw#Ls~Z0Xaf!gY5<3@dj9YM-T`AfH2A#mBim5 zjS#5t{va08XBw3P{{To_@XYf}WJ32-Vi6+YWy%c};YovEFe(GE%B+3VOH8B9%pm}9 zgy>klRW{-?`%HKc^dE%46n1JmWyD*7xZQ{aC!`ZZ&)yswIrW6IxCq^{b;Pt;=$XRI z2SIyFpnO5-Bk6`?nefF~*W8T*Uu!J>1gI43m4d^)lKl@6iRr&i;o0?q#sq;W^m!qT z#Ce3dN~R_=97RMw#e;K8RqKpJS}qwxW{qD-P) zcjjG{o*45G1_COjPgo|x8xpPp;FPL)kg#9~v2nU0bi+i!b}$zU)U@(FMp=u$sHrF> zz%`@yG2VEK)Dt6Z1rA@B1hGXXBF<%Jr^J1z^=4$*cOTXqM_FzSS|Mk_A1H`{7cnTK z<~CS%LD|4X#7O&>x6-+bQiQ|@m|3r&M%adM5d;Xv;50>|Qu7QJiKx|1QN0tK$5Hf04kd=A zj)vw;!qYh&Y?X`@#IY^R2qr4r+U*UZ1TO^;gkf9#wA!>Mo|g{%rRu8 zOc0}FtRe9mK+#Pu2o(PSL<$e$SS}fw^A#HH5Ih6N{Yy(Jl=QU~c0BValpX3V(b$)Y z)iJ}Ni#=xGZ4)J8k)6akd(h)Jnq~+X~YYbiyR` zl}aQM?3IY&u@a%_EL^L>7)ypW%B4whUZV)pA3%2&wJPQ+qf82vm(;S;D77nh1G!ux zRJw~cW-wI9#MR2OIg5-zgDi$EVuocB?p0uoQtwj4sDc%XP)N$=5Rgh`8g&da7TJ7D zyh0f>7KLLpC~;>I?#et1Vjs9_HT1g zRs~H#`T>;GvFX2wq9u7oZYL^2EcstSPIbG8WQuJ8v=4d1CnLf6mlO{&yBP5j-nUIIl&ha>1PgIb1W zu?*@rxN#ENjox6XP1L1~utZ6$!9>cc4U+qmcLr{26Au1_2w8?x!!89xxt1;qQC&>R z@f4UP5TysWz%J%3vel3q%zBNuiqzY612+UzSqG9dm(;W%+eC0cxmg?uaiNs$G4wKV z7-ME`3{>@=C01p;MPr#@W_ZA`C(s7s9Ym1SvAD@7Vh}Z`hM83}z=!DCQ7#LJF$^;v z5G{ye;?81Jt0oXQ2t*DenNB5r0TK;MdYv90#Gb!x#Kd_DSYFEh#N7p#K1iX9yaWz*0b8x<9bM1OBE5O+p~l!c471VAa|0Dh#0L zVGS6A+*O95EE-|~FmVuBM-xkwaK@6_s8a^9DqX4=XBn1r7d0yv0B$wJOu!?1B?X|V zYzHJx$hU~AnuZs+loRMt^i@Ux%rZ}TRKd7|DYTB_DUvqAK@4gGxHFk`f)6oqiL(<1 z;KfmUm@qJyAl&UTp}QhkC{TdDu%#FxD{!7=D%3X1E}}-+j1J{BsEIU=C4+G&Q7my6 zaki%B7Kw1c3MF+dW@?V0z&nE`QJT~={Q*K&+ zHB#Gv_Fu%s!F;M=i=Jg1BIhD<0y^Sr8JuT$#@SX3{bMU?7+{AC@j;P>BZ9~V>pG6r zMKHid=569u29kwdP^IbA;&HW=>C_s$Tm67zc^RNb?+0yvF%5C)WhAw-l;|?D>I`;< z&lq?^Ssa`vOd!qlymZrl=N^%NFwj@doDi0VP=i z*kvm7EWo|W8Hrg|cpBitx6Fqvj1;BzWDgL84T)K+N)L0RQ2AigDY0mA|k;h8ZUO-;ha zVU5Mi1Bp=~2nkF|Ze&_jVmV?6W?Hgb5g?4r6ErL(oJyBPK@nSsS%9=wWy~>(jW7#r zp}U=iBF5qiV+4D-pQ>3Cg~1k3Yw2|^8G|t?ucz?`s+^eQaS~e4e5-5DW*{v z$0`=8g*$*)o{_jnxS_dlnKKL^+{ZCo#j+I@tjePiEyUDRyyi2`B6BafyvittA|Mk8 zZHEXoDzg;0os$SO!YF&4KeE(q>0O)}Ewrv4(V zR7Y`CTubz1iE%2Rnt)<62vHVPJ7W+6TQyK7Ho8C@8wp?4?kY5@QYy8HN63addGhDeE!^frNfE3)~4&c9uKb#keO@B13=%<^=$VTryeMJ&{$! zJmxFWD35$Pcx58keaQy^m zsd>g`8-^TBnUzo~Rkswu1DN5qGJwa96^|5xrQJ_0z@&Ql9r~W_Dn0Up+f<5P3k97fy`+krHFhZ$r9L*XrCLn^df3eZU2(Vu=nd}5+C>+d# zr$h*S^8w+FW}T25beU$N=GdPRqxBO22*WzupUIDBiJZgKqDmtP3T$!Aa{mB$aogD` zhYa)z4rUT5cDD`Gbrt$RcrVP#0DH`Rn_rS|%QIXt6~qR&r?>J{&51NbjK;1+^=4XA zE{lx~%Uw!YVpOZl2nx*NVX2lTsv?#~!Ya9dM7%*31o{gvhEifTaBA)jD3}}5Z5~Y=KPD$-GJtfx2WrEBF!k7mV!YGuH7O9Gq zj4_lNf;B2I+`| zfR@ALhGNfIss7&Zw$b*M!&j^Qi%?%ktyiQf{{RJ=^p)?r341J#3(Nq@pmOm577K1I z5#cEsVNiOHnUb076mt@hH5WOQAZu3$WMVl%29!cJB}+2P>Mp8SO5n@Revoc$fNlU| znOV5Pj&mtkg?^ewDwP(AXPDwz%Hx??s5Plv!NfIkv6$7u=^O(E$r`tD1Lg%cQ8;0^ zWVu+x!)$(X0|kN-Ww>4Bg*cB1L5a;2aVff-)Dl!M#~CMPI4xirORIjD#9>U#w!{n~ z@eH_t-No6cU5RHB)eCJZ;9cCaiIE2jFIGW!5z!9BPZEPrv}|pQ0x5t9>WCK4(?3j; zDQ%HC`+g-{z;hBYT_~$9K_!0Bq4#FP0>}!@x+(KAfF7qSX#Lybx0`N(y3$TbnPLeM>Mwl8xyC?}=j)gax+)8IE@hkW|c0VoZ@7PL&Bo}c-9my0u`6)Nh{`a=689KwH5}p#RJ2UNacdJPM|CnsGqVs&H3V1s^^a2ZoSsCM z$pKd^f-^8;R|%Hk(q~L=LfDis1)(*CmP%t6sH-qhh&b7)q+rNLxElQ#;s>}IrU=fA zFT}Z2Ln;YJa+nN3h1O$+Vy_bh;&m4B8w)1$6(X=)p_df^qqpK2a7-~7p?p9%6S$@r z2-grD#O_x&8kWnvvyCdCuQ50S0R#mUO*dtZs)h8bCGVU+4$)J>B94~Om+AYBLZ)D0DU%r!p{U?O;%SU&TY zBPUSO3hy!YOJxGdkEEgGi5F!tPA4%owkd%*=B0?y5W{v#7c4Agnv^BkE*Q)?MH1+R zZ7^?j8?B0nMcYKJyNP(}6eg{SZ9`~-PCn5B!Q5!mG~8I!YM|vI zXxcK=0J-FsMN2B;E}%CkO+>7axLlznh9>@vH#2haa^urEi7sA}>vIlZ0TUL$2pd#f zPA?Iw`ZJ_gFe{mf39w6L=sAEAtU{_}ck3-6V5m+Y(JBf}K#YMFFhEE!nL>Ds!`3!3 zvgo!|=_*!*Ad^ld+(yjJXpZp)jKJ*yvS5TY8zZTT>Rpm6vA?1ba*LU+Vcjs6xPvv6 z1$clOh(YdT<`(8*%%il(D^k2b)e*x^AIH&fWh*NpmRLt(QcK)LIDoFq3(`Zu%K<-M zSh~!mQ;3+1MVLgXQ37azLD)elRG3dNGeB!e46PlV$~0$^8+*Z-LzzoZmqyPK0@kBZ zv49GVoJAX;>NRXXON(~}0gYkR=PqH%C^InXd9trjCg2ayGF_M`H3c&ZVo`F-Qvq_q*a4`wg5u&y z(;iXAklKthFlJG41!6st>1;@05ENvI3PDRHOr*oiD8x!w7rAeg52Ui$EM``viX6Wa zD2(N~US*M?DQ;$>GO25bbR3|rz3kXExNJFfGh4r;5k~4c`c$i62~lZKtb8v=@988( zFuPSs;e-;HW(^S{k&vcqQ?5Pt8WoW{l@*}!hzg<@(mj(kTx>8$2{9ImNQv4)isJ19Zb|$24bbOj|3T+PT96C*%T?5 z(G&|JvY=_kzJO63OX6Q-A{1?pNkYzLw~{Q9o+XIDZ4vM@%NW(bucxGNki)ke!`LAR zJ7Qg7LvdVHq*$mbvMi=knOwuTFhI<^!6-m34Q!l6R>@^%AaMbCW)GN$EH2cea*UB( z%V@^#vs#cv<3tf|$Pn_18ew;F4x;HL*oAQ#bdEPN%pjqH#jxf0h0Nwl>JKc-T-G8f zM{Gh>fde`-S%G^k^nkjF-BQZV%rHb8T*H`-(5om?3Z_U2Y;YNI?w!Y4p|Vw`W1|cn zkxh(oG%bE4d91}2BKad zi!=IGV3`|~Iap%^%L|gCh$)$462Wny6z&b;Q2=QQGP22Ysa2OU&0-*E9mn$i5!<8E z2E000qw*tLj^+24NA*0BLa-7DdE_G1=~>8E>k>y22VE71Z4bH z1T%hDtlhSi5K!uTnzd3|%RI+rHYLBZk#*l!ZcE~PQXiL_XkKZc+j%T27)?>Rf;%2h;_@FqS=|e2d-v5-uzMw zR%d}|!ENHt>G%!h5=B?)BM`ZdDtvVFmHe$ zY-#FABd_cinRKoL(12^VI7!f+#};5`wiDBIVT@s`4uqYXCIrSPFY?kY!%tZA{Th?X z&==THS=5kO0PTg`;XoFLwSLiHh8{23(j}L_khE^DJrIKLoQ-9y>3cnW#oe9V%KzAI z+3}@|XmJ#wQfG@pXoAwfp{w(Du>lpLO|hKSeQGdjB0<*2s|1j4mya!coR}ur)?HQOb$kcxMSUV5A{uld*^Fl#Vo6fL4B*vk zBPZBwUgt^B#2hmI%I)M|-}iQX;wHg+8#A{2H&3rR$sd~|?-Zmo%(nQg*ZXj?RZX)X zq@78*0ReR%knCQ>JaPZQtDeu)ogy04f*&xv2i!)CvAN8n=@h1@y0}afsABN2GVM6s z<;^?#N(ypU+EtfK~BKg*l_-=&};}1v1>rnbZMj_N#~ciZJ%uIS!L!3 zh`4{ec8e8DtxP`R8oyVgY;Z+r2Sf&t2yoQg1&BpN10rm+R7o-V)t%}9S9~zWI<1g7`5+;Up9~OHgyeRMb{~OZZLgZnZDjmajN*0it@u4`lr$4oG6pw z4YKW&YP6(<5Gy)Z{Hck6Mme;b7L|?3@dl=zyp4_3AP|Y$!b~8Ij(O zn)p$ae(D6s{szz!q(7l5?;G$!MwM&lizf?>*P#F`nhclrb_t2OJ-^tD(b;JKXL)MWxl3QCuwY@L)Z=WH_{`}<4Da3L zJI}+8JHCsDcl7${oES=;$dXQLw7Cy78KeUL$B@q7@t^eUu7lV(GhYz1i`&C_xe@CN z>bTCNbTvB?w9DkRZB@Hyd0!7{qhx?CRsU*pEJIp2?W<&}&@ZUf2&(+vItFkw6C%n7MCXeHP z5F=g&hWuwgsQ$V1A{MUzo$EZLvdxtC;0t5bSJON54l$Anb$N_qo64H@1>X%wA&6an zvg|V(6SQ|!3pQ(+&e#Pk(P`@WZqN#;)wmCkaZZn}F~c*8wpi(pS?u9N_(jAd9Au8? zY~bRZ?Z7AgzB1~I^(s}P-1+KJpbT$OvyWYDxQ|_2FHbn?9i6sllQ3!w&*8*!K`sB> z$fv6o3g0aZ8m(RLfS+J5a?9sS*#u~i&TLd3eURbYE|EV~?#&2NIF?uHP0%&Cix=@6 z;Y?F(^HN;Jac`5wN|GKaOx56JT*T#v64l47&*R^X$^^Re{XBfJ3tS1-&GnQJkg*jd zvOIvg%OL_dE>b%WzDJ`uL40SWoUbo~|BFh($Q7IuWWqzUDgXO=#eOYR}XR)mofP6y-)Dj+N6YW1@Vyj(%u@(Xq1prs@Q z2x!PW)CPU&V~klxK1KiZfZXr_pa^)*p8Pzw*)gSYhf0=MAj9xw!j+H@ArV5(IwCBC zcQZwU+2jc?wBTkD${B<-b+7F882eKUGvjpZKv-ljGG@Z5OIa~DfTXI%)(DTY%=EZY zd^@*Zf*r`KChH46o7ejqw_vscM*H`~Jqd7Qjn)7yZ+wBYa`&pN{b%8swWk%^4?^+3 zBmDu+6j-VWm<0FbetZ#8*FuQ3y**b_-k>=SMB>+5>IhfY{EYjjf9_vr=ABYQn|~?|V`iBROR9;u)yUZCR}sy3dv(usUxA zYX|JP2^FsdHL6(?5@Vt%a|EBqsNZ+(|6**4ie|blcjNqz_YCf7 z*+p^iFMrDqE|MuLBi&?C85hTILG^8Y!C)GqAHPEn8Nk7;f-&(DEENC3F-;{sXo@b> zbEvXxg025Bcttc(X`tLfg%lPY@Zc-XfF8!_J0zVXc7zGMU~iXP7Lcvzs_Zj?)w*bB z>+&hUl$``;*8%#cZ)QzeXuerufUMNFxDv6;iPPrNFqOckTs$uDp?QGCY*dGbyS9aR zo^Y6@W(eff?!P06_xOt^e;u~7V|k3FHVyFH&rz)M6nvVF80W(U9; zjP{vAB}U)gn~|u0?z|Ql&wg#IG;3&AD)8;WxqxfMZr@qjzPk_W!!LEc0;zrx`+f6? z&FbyKma8=z$ltAAj>-3JwaJ6$hh3#C0?ov}l*z!$@jf`@+^Qgh@17$^wPL!hT7#%= zG|vb`a5?6fEK%a3M;JE!h7*?g6s@XL{4#qZq+>X>lR1X1v7G-fn7_6@sFuQ-O-Oo@ zwK{Eox1MU8JLJDQaKbuA52D8sBW4L5_3P~>Cz`DRybXHtP%Xwe&e73z0Xd=jfPPkQ z`(`k|{Tewblc+{t&(%4E*p@7Hs%-Z2Y_r?f$AGp~(hL+b9saF1V;4-WYj$mk4cHM% z(+Q^bPu35m6vT2X^K-9jL1z7-R=hEjra*m+_9V9{R3ka$_DLU)zy#TaF7*-Qw0Qr_ zPFn+NGcy2b+QBxZaA%+ES{o2}$jVSVV(f*Wv5Y+B2$x@wz`zx{Q!35XM_4Ky+ zjkKqWK#gmUzCDM*J76|COuQmByh<4Uo5eH7FeCOXB8cmRq9znZtw()V&;D+3T=PGIU-BCPKpNNI z3*ucOpMGbbkmG)p8TfXeq*hHuQr{L68txt#GOb-;^8A$|pYNQ5Zv#;~oK_?Lg}jt{ zwEEhHz5kd;=`r=MMJs!|ZQdhuSj6Lz8sod6TaniP=;)H>Jr(aQSxcHh$rHTEf;~ZI zS-B;@?c&A4!(g@}3Bk5&)(sni4b|_Z&x=sa91>Y*Zd-bH$jKj#Z!LPHDQz=5oM&%V zmSFG7a;{}_al-O0=7|->3(mzkCO`F6${n2}bJuaDTIvCV0rO66NA}(DwjUH5sl$QW z=gH1>)QUfjr&(5Tyj44$V2#RP2`N`U%dj7lu!g_3!m)h*A46(!EjZ>MFIRf#myYh_SDvzph6jF>RCAHy*T2Hlz6_T^Dap}=7 z8b6Ze(>&npM^4aG($!_?wRE3Ywt3m1`S!8(e5cJVA(GeEHuVFs5+XHPMK%^uF#0p~ zW$k-q6(f^Bf52X$l$+mo$@1=X;t_|G#EdaN{NwAbSA&j#CmJ`hpwp-1?gB~SDN4rF zW&X5}ZIFuLTehL&Vv^EBKQ}CNTBbFArn5d?TVAHl?VcwDgK3vJTLRy)@0VW`r;IBd zX20-XLGGykS0Z~=?04#eeD#?FmRRK*z$dz#g{7?uJ=z)vM|p13s28U?e=}m5W{VvN zxDL}Y5lW2riuAWGi#=u|t(FoGg(~wpj?FAQCmo)cEp!Oo)(zGVNP-WN!8giW7T(RB z=N^7K{1dayeXFV^WZwrB;aUg0V_nbG%fjIcQTeX*btumqFL(Sao}qI@0I1~}1xT)S zhXt^@be}T|JP6aTq|HLJFQ}Z2^xLUKq^qa@lN++Q@{rSvCf@`hBuaZk3iL2=4Hhh? zkLK-{9U55>$!dc7ihbT2xBo zde09HR!)4PiVM|^@7ig$59WftKVi$oG&`Iu+?Zlcps4YIB7*6Hb^zdunI^)umR=|B z8xDzL@MOHuPeR3iM%A5-?(u=W2hf#3%&mb3rKO+!koWq!o5Ty$P))i1n}08*B?n3=Yw#Z{lakqD)F6%qm$RD$vZ zO;>`WQR?Efm}&hYz#@wvZI-B3A__~?pILgvswJeKTb>pEcGkPPSVX$yMT&U=VePte1>N>D&KHtpuQG*Z^f|woC z)XUukG6Rtk);FUry_%5K9c`Oeh-Ph!hiz%kQN+opt{f%ra8q|ZQf-~;6=4;gsdtz1 z<8_@Uc)bqh^M64k3&a}%V!kDAWQx#V`<>&=ca!?y-@T_+w}!VSf4DBjrAk=z)gQpY z!?d9X%Syj4qxHX*vCLm3Qr z$QtA`<)!7bJgqin7s+R*gt_1e>zAyRP_j@0zE6iqF)cNJ%zw3{PmWbn^r5^;jZ8z0 zhSO`U3oUj92NH8zQV)z5IkdMpU6o9o=+Jl=Ga^SwYB+%mA{wu+&GBItOm)ka4U}5l z0wW*WMA=g&C)@FP36jDT(PmmKtHbC3N)PH$rrD3G`X9s3r>1S@K(B&`&{3iw6cJnW z-+^Gi{M-do#J*`O=RvJX=M>5x$*MEHDY`3P4#k_VHDBWFQpC^SFO?~5BytLjs>$A& z+RC-#2CKX4o%g~yOeCs1jafXYTM2lhg?Udpv=hgdsd3yOe8ZKgGrx$C7d)0R{8kY3 z(3ETVS?&+zJPtm2t7ipoAK9?tnMTb{(E>axL&MV@6yuL#^7l{9n+AY6+#_e4aZMYy zG^iU~xaO;y%N@{X!F$qTm(V#`u_`1h`t*o2c{n~EJxR)O>bq=L=y!@SetS3y$1}H7 zX4e3Elqmp;5k|dUc}?u9d>TVa`isf+tY|j%cTLwZCQoJF6@L&kikr|KNKV52v+TJI zA5f%U4!#_=l{pYE**wd3i&;|a>`2+x5y(b0cU2OtKqFL?4q&s;Jn{Uh6mjYbonB+w zWORNz=Wyq&1(-y!xLj#zk-pxT_HT!1oJD)}0n+^!^z`73mOv)O_hP}$mpY&og#5VN zx_utEG=Z0Ia0a^UXSX)(Wk0e#xojP)ku~+;3ony5gKEfjPsM;)!3teVNW9E*>LVxw z9I|DRQ&jf~@v|4wxyunEDsK0F%f~~MF|(0b$VI)~sb};#|2jV#t3`Uh0r5zX?r(Y> zI5cXRn_6sRWShd~ku{e-Mrr7E!i4lAHimfcwu&iF=1GVjkQ}b6TbwfKCeV}jC?erKBu7-Gc zrVxM_ywXxC&2$p+)dMR8!O{cRwtQz>@1}#7^U!>h-kZDp3tOT*7u3?;;9Is;Q{CFei)5JP{)jBDtfGbKnvrZ@SC{Z_)h0!-qR>tTVM zi7@!k=&oz0k8N8Co1C|W+YQ@9=&o!>efw25NDrV5ofGM2XZWzWwp!F2F%z}mW4&OW zkKJJja#mVOBR4=SZ#i6R!7kFY+j*wAD9iRKSRx5;ej z(18Yw4P%TTbtL*@?vT_^GRKUjk&iKfP{F1>`8!z~-Y%>XG zqFm|+;NU*{b6YhC6L_v7ZW)Jt#v$Hgv?CB|8tShvvha3dFN@Dbd0{;SF(9iLqo^ zZoG^{dPgsvu31C)dF&89OmTb*RP=-sR$4>)416 za*g2^y0Bw?>62FzG6&=OHL{1$%vaf0jYM0dkAE9?l zjBGc1p7LdVG3Sg2saZ&jHIBt{glr!0N6259-(xtkVt!rHc6Z4$o?CFrctTrnj7j)Rl9%Pe8n%pxM4`OXPq6p)JS zK=BsOGIstcXJUQN?6_3$@mjD6A3=v-66TQE*bL0b>2?jiSBYDa0xORwRQT65apMwM z;Cd~gPWddyWp+6=<#L?qaHluQOwq_l6H^NcD^-~cw-bxu$H~M^C6s#b@4Hj|h&j+p z`3>kD*m=;`)|vltJi_M*mq^U>9i+ zY|mw}d?AlfQ_(_~)~(NNU2xTb>@B9LR@n-<)MC6sF(o83d@#dWEX$Q5&OfzTH>U9C=7p^R6e)8l&cfDi=^n8zhmnXAqfJ#OiKG=o}iEzMk5jKq%zaH zJ$t;@s_V*QquAscAwe78f;s1m{gdw67`Q@0ih+{Wd>3r7 zM!V|Sn|2Wgxr0MDO%jsIh%`Pej!=*?o|~}n70!j;nyIi=U z+lAjeb9=q%%#?O-lh6R$eu-L!$}};f7nsSJ06pFfRZf0#ogl2KEmrb-K82Nh(Z*kE zOu0>>>@a(-z(xKS)9U>ecOwsC`#g&4X+R!sjx%amn8Si3lOG`za6FqQH_pc)Vw_B9 zyVSW7O3_f4IUfD#EIx!npB;9bDyY9{(`Ns`2P5XqWqCt6KJlE`6YOopac>1h%VSj| z`)J|``Z$1TUXhwmL{s!v0zC2=VfoeXvRgmCGEY0LdZxuQXJK&b1|hDPJ~ni8E9>y6 z;}z}R6LN8zvs-(4lcCFct2VfSD)$CZ<;UQat;W!WV}c$OKyvR8z_;tH=nN0mR& zW$}$bwC2y?c<{yVq<+FQwk`XSwu3JLjowx@S+ammX}Av`!K(OE=4Qx4Y%s>4N^>LE zS%I;vESA@^7>%->#$>S6FW=)|1~82wRZ(&n*&KS%+ReB|miUNV3y_u4N5xlMt9B97 zPJPx8cU|xVh(R=Jd;7f9YR9RciC6LQHc)s9S+O*KdY!Kha-VZc(%m}OYW;M)%?#yV zDTO4B0>XHkQUh;nslvasB}|I+>PAW}{ig zWc`m}FRok(jxQ&2J8Kr$Ch+VGpx2+&sUv3NCCx#nP8DmrQNIMJnxZ*I#Qbfk4oX7h zwja+x`Mk(?mWUAu|Lb@`aX^C_(2CwjAoyty#-96CYkhUbuR1M~j zl?wO|Ze0>>TW`oZhuFvQ?Aoaz*O~bT1J!j!a|^n+zm~J} zr!>Zb$Xa-moQ-_Rfw{ZCm0~X^rX#31FIAcW$L}7o*}Ecppu}vlNz2VgH_r7#wH&WX z;4|Bjr<_?vlU!7V`x&Lxo0=3cb`?YEk^-Dfs<4@G8tqkpDF2bRQMBB8ZdBEB8jz3{|trytZp0&Rk@G4)R0$Kj*Q(D6M~HceB_6{mY!(@_@cIFV&#j zpg^bAaQr6Z)pJGgdRY&-qSD`9x9KBYQd6B7kr{C89h3XcGagkHp-*)N|%k&Sb)ck7|ifI5T!d2ai@CFe&VHE+tG7uLU z&ATugSjbhk^GK7xg@*peD;_;S9>q!lFf)0I_4W&ml=Z4=e}87WhtI1Ft=Hp;JjqPk zsM2RdhRb=Wv?EGz27d(y2q@q?=SiLEvf>bq99C@Z*z zn+8TZV;_q>X`BWUy8;=+QaJe7$JxvKA-%GU9|;4C`L}uW%&0 z2{v;1lLE;1RWJv;A7@dO$;9!Qc^e;oS3DB|0Y}KYjeVj=#=Bqqp}tK#^NX#NMc{X8voMrR}x|h8{?i9Br{ewrsNLNo*O<+301f zffMk`_=!=70mbPUOB8^lX?(SLyjlhFZmf`_^H>g$F3Gi}sFA10@_G^6;>7dsiX5^nr3DXPpTjTDhD3Pf2T%;)|j@me4#ik4)Cw=d6)}q#f@#bAS@f)+d^3} zWG{75wpvZ9Lp|SfN@KK6zSHOrirHGZNK=RqA$k?t*12XOt13nrz&b6wwmOK9DNID= zF$TJJyIzy|bZTET1!lXA7blA`IO|#JrU$_+;~tXa3?&03TvOtiX5(4ZcEA;B8~@DC zXT?me?1s<&E|~vZ+~N}Z`(i=G@y+>{sCx6}>Tsh)wMSR<@6<674WzkY)Mw&&a#*G^ z19whb2HfpR--9aCCSF;Gd7nuiyKQfwIl9k48HdSv1}nDCw?{xAZa%qA9_nsnaomTO z^|s6ZxKv92W*SQfWNwI+(|*j*^P0A#QLP>}ZH~}Vx5&^k8xHJ|zT#tRpsud;VOMyZ zO=h9Ao1}8OS)lr)K0P(8=oqL4JCp}Wlxvslobu{R9x$#j`GGv^xF{E%kxk1zvP_EH zq4;!z;-v2cS*u`nOfY|nxZxYh@sNq~BbF}~0X69X`#5;Z6*beV>B`+BG8bDyW`rQ9 zWn`zq`Q6QYLgR||EYDN_;w=~D=Wts-at#8N;Pz*F3xvB>Vq7O*=P_+>H711OBQy=9 zsxv12sxtrgovre7^0x`w>iuTf&wQAz2Zl7D-*LwAzZ%UBt6&+ilYGtfofbDm{)24Y z0-7n=8v7iDFZ~*I)v_o2c6fp0n6B(i2?yA$WNfbRU;LKHQK1CwV$aPSme@Vj_s3{X;}1-#@M~Qkk9`prQ5O`IHgSrsG)N9iknf@S?X$hy zjOkrI)P85y%yVp_tGbbi$=;EKf09vhu4Pp6W={Ql0@GS$b(gq2|7=F=fK$nvmn#~; z^2q1FWudQZ&nhmOMUYYr?1si)$ZeBULt!@kD}Y`;7R}%Vh5K8eCLv#ln^<8y@OIiZ zPeb;U1QX&pvTRLaTD|)Ivq>7`%|uOP`bT&&2UXclYPe2yp)mbaiT9Dw0)sTHP5`GB z-~TTY*rfUk{%z!P%%<^GjEG22Zj}!wLbmDvR$yD{zs0CiD*OrK7Q6xcS_s~=4Kl6J zVg`RFpilUFmcQf+{jgdAJbo*pz}J85(y?tP2ngE(u6aFVX&_G!+UJw8q7DwNCXYo_ z{ZpTa#_*|J$&a#BV3r%ZBjLQ*AMo zlvw><%?Ce}?WWu2-h5C{faHWRi|tRK{$Wa9N$)yHc+~QRl`hh}5Rx4N(3Ja}h9N*C zax`t7b%2`KrZU6nJ?TN|(=Y!z&7}r3W%$9u>_FDCFvg?Tx(>waIP7f)(=tT5$Z?ZK zWBlr_%vltVVjFE&4Sl)LdYn+e4;|KD1f8_kd>*DIiR_k+0%Gj=*K1n`lFG^dV|bwB zXgpGS`n5M&GfC0U*H|`~cqE~M$El;Yeak>C=CNSeM1Jr|6hUrGEEYrmik^ zLN53hm?n6?lg~;qtl)6R9GGxiS6fUMv~&6-*Fxwpc9>*yN4`6ZY2HS%wxQJabel-n zc1HCdAj!A*Qv9{;MswMI33C1z_}_#^)*V2j`+pji-=QINS3{YEORF*!TtPs8BBr0Y zY;*|ZDgRhUDTR00t}}hU2M%pYu!M_#P^@`=~3Qd~VfwN+j zV-=Djtbx+!&ZflYz)fk)3&-%=(BsTe%`H5|77yg~pV>8{`9f170u8;X@CN@kr>3s( zcNj1#zT=%}#&Gpc+& z5>6kpQSjS${kQ)%mX7yioXt(XpUu5~)mZl0*B`_x@726j1vlL*(QBjBhOv-sR{I!) z6ArS+DCRI)SDVQp1qqWIwoZgo+TRKiskL*yY6y!70$iJ&J8#}gqKJ51~rQBp~ zW178TbZdMQoDFWj@ARb;9PTc>hc7^B=E#!2-OngV)yRmKj6LYN?R9!>JYVug5@Qokl9%xDr$YS*hz|QNI|kHqpwq zcgUq9>XD6tNjS5g(K>2f)XpFMtb^nJslZ3l zByq`DR!(Oq5e+8=nszE`?m)e=IS{U~%sj(Wh_nfWR9$8b7;jsrPV@s1*ekcvz$UlT^@ejv z!WQ?O9ze-rdFRHF4*0K$ZiThKK%2%(a4F8?{2IlEQKo;hO&wvuZ+5gWiVe-@x1ija?7_ z$Wtn&2a}K6@N(s^k3^>d`y`%jt26|NUe@J_F%7Lt5UG03q95^g(&qZyEk}e+BeC)0 zG6(sb3pf`nhzVXu(M0M zofT|I*!B%d*ToZXKrV;_EwEg`j@WeN2VA#@G)=qoW#8wkCUNNFE^D6Aq{3@pEsopa z#^%vUf*WhjJ0{NicT6VUE{q@96Ynahe9UYvNy-|`*CI7QE$%XpQG#Eb5looYM*GJ2 zc%Jv#L6G(WHnO!EKbXPO}-l3{-k+xsuozoxW z9NWoSOND8lE;~!sORp%CJ~V$&q6Q9f5>pQqWxIxQRb(>w%Sexy)?H1%N|JsB@Gcl0 zmLvR4%Cr)d;c`F%T|b|YU9++cN;XCPotD~pY1HOjAIVQwv>2_sUN!fx`QN~;Qlb7t zec-=MqdGY^w4Ar*&@knL@V{q;UoL|T_$b%xkMR+B{v`_YI{{hgaK>H>e4<)|6so`% zSNFzwno%v&$u-@r<}SvIzXO^at2fRY5kx=7Sv(UlUj{ zh`eRBJCC=t9%xX^EsscJQF@@Wp;jy$ zVbg$4&Fzo9eLEFDN4Xi=QnF|vh`3dr2pZ~5&}UEE`Ts(tYT>m~ zR^fTRQf_~o6{yc_9TlrpB8_jHJ?-R&ajuXSVA#q(Qa~v*vs^&p3?I2Z!oOy^Qg1;_#TgMm!Zn{0xp!QFSgb z{+(?}A>M2&7M_{Buf;mQ$su2QR{4$G+qYgRno`STSv&L%!Nm!mR;;=Dy%;}S zOnzt6hc$itV2{(T@&W$IkCEbUme%}O*}*Tez6&Aoopxl2o+WepMrQ{6Mtl&^89*r0 zGm^Kt*d`4+@FFJ$ocqgEWO$TzlHJ7Ee8ZCCV`HNm*%Vms=J&J1|C^dpP!|yVnr%5S4ay78n~xPC zSYazwH`9dnpMQJCbCm9%0{L1kX^W6k9>U#ia_rsu9|NF2>rZyvfP5{RNBV{3D|v5L z>TZ$+o6oro3NO3#4{ES>vR$0Z7580hgTPbz?YGnwT|)5St~j2v}kl+vS@7kxZv*)_RyF~ z%?iIpXD=j!sUw=4vP#uzGbk(yP z)mzC4E85(=oEsPPz3{cu(6{|Bh2cIdijBEG$OAyCrPnw-+W98aeRvqtRS2BKslN2~K+rkKId+<)(Xm(@dL zWlXfq#L?Sex`y>EW%$}9wF!$pTwmPqEi1bs-}OS%l6%nB)92(x%{deIuvI-q0}@u5 zXHDe5DE)s7_k(gcFGtqat4t{0l}8Sq83zC{)IlD*fz*hvl>f@R8T~DLsvFEzPdHr# z^>NsKDSO7Z!j{s&lh8M@Z*>;6wCqF=Gz&G1DIwonrR>&fm(wB`7L3aH;lm zfKcIFghGqpQ8J>rzIg>IcEHuaKWv^sdtpK@{U^&N!G5gXIwGT=`QJAY{x=%E0ybHr z1}Xd`iF-fA%7c(|^>JhoINy>>Q!V&=nRV%b}e&R}F;%N``>Hnd3S)%kIKtG)&nA zR3jA|o{dw+lc7FML zARei*HX4iRA5V|5EB;RFQ1w-y8h4c>k%y93;Z`C)4AQuQ(=Z8PO9OON&b@&U_TpTt z0}YYqio2ZMzEK&dDlN9qL6(10!&af+rJAH|i#dpw8UYIoMP%etS;b6~pmJyjhu>Xs zd5#mU3&t~jUdt4P=V^ILfp`97nV&uSzjiafGpypHR-5p7U5iIEx4rfRLOGpV_+D%}W`;Wr{K6k!JmCITR*&SB70XVuUt_;> zzf2NvIj2kD!INYH>vkohF{CMn5W4#;AJ@r@`RkaPXrTi%ZPTJ}9{M(~K zeKCyQA|=mho`C?5I)3qCNy}NvyzU8Dh88c@pEms}iJ8(v6GmV^(b~(^xw=zomu~c+ z70@?a;kOs!C0^l}#F?SN!d)%B)#QGj-`T1ntZ1R@g$!!Ho8;C^1n71q4=$F~MQUg^ z8VlU94YXR_>@EK62(S6MaJxh!S>aGU88>Mznw@C9CCp>I>(hXR5xO`a!MxBxkO;>k4XMNlo z4z}ewnmsP}uNCdSWm`()PoIfE^cSR*vi`)sWzzUtG*%(Y@TuH7VcKz@=i1Yn&3M`X zhM9ER_BD5Niu8FT({OJG7-y_d7YR{(mhLk6i7GeCR%XVPk(4_$=R)*$!nhy0m_}al zvm=2mDGljLRI5m!=JIsIfaSQBE3io^>VRXZj!S4%11O@o%X(*8u)i8*czOViYx}Ty zTrM9@VMq?RGk@v6L~V5TY`ZJ#^-qr+t#JESwieHD!Bj{mRXi{@kEN$u3@+{}9+u;@9PpogN`?jwFw`qX2;}Yv z`;&fd_SJ!Y_Zz63Xuf+tr~Zj(Mi^6|5Lm4HR@H;7BI`Gpi$2>-U-r1eF3EZJsc`=w zONhK=7+82A+nHTT3f3f9lb1|-8%Hi>jDuuD=od@tib|_rrgDKsW)i$%hIIj$L$Vde zxbFCOPloZbV8*P;zIrv)Pm!sM3a7bb$fq&AjsdN`#z&+`H>B7-@(})bVedhzGqOHO zEepg*?tYsF$UXvNqOTwZ(cT(FH-Fl1Egb9BKZIuu7Xvn0va@fCNf}W#0|bQBN?xyi z*;7>}%p0?GJ)u!b7GVqt-O4u|UbuN>p6~1#PA~s7NB5;F<8r?qp9ytOkSt5StLS2mGi21AKMvWcedOU(z$nzc%k-^} z?ef>ZsaKhDDK%JEQwD6%a@YcDtNAABfPtM_KN{^0f$ z%3?b2|A#1BSum;+_%1NwR|Y-2+c$6h1ZMa$=quh6t+?wE*;}Lzo3GZJ77(*t`DuHf zlYMrAhg9q=6tr7m>vOTZi~(9Eyr7I+snZKo4j}ap*lGq!%foopXDhkr7X_35npK*A zkJr+8#s8Ck$l6=#`MxLfe`RqW9+8&yy5UOhgd$U_i#Zjy)p5YZAr|jbecrPYJHN1w z-uMu573}^UI}IbgPRo!$$En<53EpteW}J#Qt}wdK6`8TPZ)@fIaMHHqQ)R|#o`hMU z-7xu`cYx&73Uf%4Nn<)sW__$J$sMNiFURr`9-o|1j5jh@a!~;O+pszB^+4X~1j07y zZnP29+)RY@aM|D0V{C=>MLSAhPQpB0#xY3>Q`WEHibt_2-rK**eWVI`d!uCYFeB=p zEayp4FyVp5O>Q2pqM|5TeTpO*JIxjSO!4xy|Mtha>mL^+KkImYW+AH!Y5Zy8&T{j_ zX)(MA_r0Yx*UVLVh=WQ|`yT^mu26DHK*aG~xwN*+uqVQ-9UTI!fs>EpDD+m9yW{A( z7pTGT?}bnpBaP)Fa25?&svH)GAjv-cfI++ONSS~5e0Ipdr8hKr>x$2Vz(X=myrF%c z23Op8URfNdapooS-Q0}{Zea@!5F0Az0rJyW?5zLBkwi<3%zF+>EDkQ-z!PRi@b2P} zo!i?A<%tF2EECj>7~VZ_4B9f8-Qt`TRojEc7*F)R6D|92@0%?{1J2IJFxd6S9n*>4 zCPdPfq6OJrIoDi4-U;bkDzO%6TA5zj5B>rULD6I;r22$gZEqLHNr{(rITHbVkD+++ zq6|qpCrcDMq;!ul?$(-M4*hbf(H4?%h-&h3uB0(uw45_?=ks$9H2KfO*E5o&To9+3 zKl))j_tYv$T`M3qKO2;+1eO3;It|cebpzacUupKSy{%$}N+aG>3G?5%Iye~oolW+}NQT%Fhj{9bKU8Pen-mtOJS}cU<4yi|WAFb~1>I{u zVAE9vpUA~SVf-U5cJEu=YOrIG{c6Wb5v;IZF~LGGR6JwjIluJt>~XQx{}^sqY|H&H zIoEkC_j?iPW=x~2XI;UmV&+`W_~bq>7ix+ zmo!-Gexmxc)!(`H8@DHtc+X3Hex<%w4w3k!=4dL0kiJ*P!_wgBLQ03YRWl*QA?&+1j6X>_ zoPd-T^sW&!Q4QF?m8UabzQSlNo?6QjlmA^(cHH+HMqD=NJrsbppt3&BZ3bAb&9Ehx zcWeUItN8w&{t_Ep_shJlvPVvIyyCegObvDKWj{Y8!wWK>W4cCyoscersfm7Dnk zCa|@DYSqAPzX-n$mRJ#`jRe0(e{swPHNz+OR>6^@%GpjN4d2duJrPpVO$MUppV6BE zztgAd^*$B52A_g{3JaLL5g11;YF&H2*x;M5!7{SqBcKw!0c*jYmpp-7I)pm~hF2F% zQlHu$T4!GjyVYrwdtWW}Usp6sw6F#N2N$_7=Bg=(c01q^9L3g1nEf9BoORyy<#xoeEFF{nTNH^CV7oC@f0nE9<0imKXx%Wy+pEean%N(CRz)#U(8au z2}m(fk^$VkR&q5Bl{@MR20J^5he;`MDdZJozSWE)jfx+@VX7nWY_LbRBdY^39KAoN z10*)Wp;TdLaW7Rox_^@ZW7RFf4x|M@n%t&~6_m?EZkd@~D*@hIx`ew3+R}Q$C^aj`o#gx ziqS7q{>vk7dYACW1W=Zu&9PrLUkQXDQ&)&xufh|0^9(UvM6rh*#G(U&b+klLn2#gU zJoT*q0F6W=Hf!q}l3VtSQH16PapgHq1+&ET6d4T+Locfh0P*LG&0hKiHiT z2gV{4Z2U`Wp|3EY>SCcYaVhR04U)hW9k`sk@dGoTPJUy=wf2mKtW~5{PI$yJ%4RJI zt1Q5KO{dLMG_vv?63oh(Q4I@%_hYEG7ZPfHiJ1#(bN~sFCDEDclbY}ZKIJkgsSQUXj1r`Y{{U1AsyO}1DK2tEr;;eV%l`lbiBRpAZjiuj zoki4Md5lB-Qz^KLeG=+EBU#Ta!HVZ6=+t*ES~B}25cQ!Sc}gd&X?sVF(9C=Gs}xr7Qqai3`C0V&lF zyj;jklq#Mml`Blld1ee)s59I#aqNn?R5;>jV{h{j3jRKj!?U04F&_^F8oK`ga|F`E z+dlCD4fKh2Y@AguVpy)uqJvaVm_%QBa)e`*E^m|C3~Y~EOhj6XDMWqo6|pLe&uO+| zVW-eAEjfdAaIM%O!jT?O#0FZZdeF1HbtyEol!V`Ui~%d$UhFh~W(qGBLmDgA7A&-L z5WL@lUs|TM$CrenI3bpxSPADcSsP--acHU!@8|--3TX2$x^Nsy5uT2s3D{yTN>bT* zF@&fEW&qgyKd82&uH^>sN+2XLpQk7GR@V`OrX|^l=>aG{5MbpGku3CJ;d3Yk$*mg* z0Bqb|6b!-@&MkH3AY8$xNAE4rKLk$Jqe6&fba{_U2+uKEo(OZ|BbEevgUJ%e99D0| zX`e^}l))W^nj?tP?!OcCA|?=v>lD;usFwpA9-sCG3x*n!KI&A?O`$DUW~E}`rTjoe z#s}K{qa8(Bc)mVnG#!VM3}dn8X_87GD5wbMzxafU)v<$W<=6S#Tus^`4{)Nn4-)Pm zk;PBeI5|fzSk@nEQ=pT6L)Kd3Gk{}n8 znN9DGWjN+jY}R64U4ZYo#EXLlLNh6VZfP9N zsgwq2WeKJwhgT6WXSnB|AwUAmr3@D=4sF2eG~1EP zF^q8$3eoc&y<1Tb(n=-E#Q>@hH4uscw^67ZK)@w3!Ghi+O+vT(q*s4Nl7 zP?yNA5RFSN66&}kYuuxd`^^6U2ShF))AtH$#S>1Kacb~>2B;I*XG&9?O=7^~7y zfq!^pKVNdEfY}jH#8kA)NYr%ah{o>^iRqNfG>5of=Adh&t+c^@gXTPM>_8V3TW*ky z($)f0>B}j{=4CE+i^2jLKX{)4b>dbdV?6%=6V`__?+rrZ0=8Oa30UgY_c4f2 z1Bd_{a?+SJ5N;wwrxOjz3L$KecFtog0$#>H52@xTEo>k5rE0tCD^sVk1JjRiEUI0C zDVS?ILbj#>_9eBK7EaGRN7oPbHv2zj2D&r-63p^k^ zK-Y5)VZ^$=1oI|&pr{_Cqe=^xi=(0&I@~*$(txPm@hMT45p!zF`GRkFA+v}O5s63b z_L#9<=W}a+9}w*{`H6;Py2{EkyofwEF)4aAE1m`z*CY1X5XYF}A5~uXj^rV5({h1Z z$E-_Ve@_odZT|p-6-TH0a3W|-#IZjyY%AlblWxRYH~5J1k5U{!ThzriAcMa%(m8)b zq1Z&PRj)n?qp!!LU5q&Ve`64_uUKR#XnMdVY6fA&0hfmb%!sSv6iq9R9OY0TThlQ* zQ4M#*x)UFUB}6Vcm}!J*sw#Rghz|Bn!)L@U?D-6(6s)6?Uiv4`n6FGleTe7~1FN5y zg(*x#_qglOb1){A2BNFw{8V-bI)CVm%Vs@DG&L@vsBZgD+yxC^5U3}TQbotAh`rF1 zxzR+Y;26O9%&iSQJB8K6SxY))*92&@;roRIl8I4lgAj=jKp2T?K?v!CGY9)Ig4|mu zg4hwmxGQ&Zqzl7lBTa+{jZ1Mu{g`;U-x8$QX5>AQYS`HE2%(sXRHFT3cCVy!cdH$< zPfGj47Y~=g3rKTE_5iiVWW+~{t6&P3*5U=`41PkV!~|Af493!1H2X@=%(-rIZU!9X zHi9v3SPLxyzo&>{br@R)!1~RA9+7?Y z0T8DC60+cxMB&7<6?~JnFEZrnRr|72FPKyu{-rnoS*;Zuh=;KSO?~hAEpL5CC1`FX zxI&|3c4%6{tXo&N%%RcgE{NiQ{lV(AxClE#I@ydU@J7X5LL4=zZ<6_sL4D#U%&8jZ z#9f3wd`$ZT;30kIJ$Ya)cmDvZ0tyIPIUM>xx|wAv*;V4=7y=q*YuFI!NR& z(>8XCsli;BH@3@Fd4OL@bks_Y-fmsspNWX#p^q#_3u#?VMQ)>q6qn?21xXQ4F`^;s zdAKK!2{_q>{{T2nnSxNRf?r}nt;HBM0Y*u4jPVPxeTYC!_C+ej1|U~Df|?v8e9{vWTN1kGmbcA)a&_;);fa_jZ?w~+kUd6 zTjmP+>vs#Fd`+bn%v*={}xbKm}}A#j$esFS5nB}l~MJnv>GWW zjiSQR#6c6F{K^Ut9b&P>BRiwAXD&R%%}(mbmBMh5It8q~uooonRH%_>!domRI#3+S z^1%_SgHwZ%=vZj(G-x9VvDW3mIt=EM_ZmD5+a4%Ze2E+FD7?H5ZD@Ikt}*Q5yu zXT(*W~P^f^iqnGt5#PCL+Xu&8l!g`Jl{QVY7hP~eWN}t{1#ND@} z_Q4lDxb!6{+S~bhLuO8)S-2=-iNs(l$_e|HdlVM=E zFA<_Dy7cvcTOm}e>-8uyXCAym93#hmjMU3(TviN0m%*m{E@iC>wUvO&jo$MEFn&kng-M7ivc(qpbSQSl=$yRADvPLQ3b@jh!UqT)28hXe9lcDECLIwk zuy58D%&;_a&PbP7r5eEd!WCoviCP)vW&>>3Kk|Fy{{RTr<fh87J%q2#%6y-hPD_m=2ZmkOJ|Jqa z)sBDt`YQ>1oRr17SgxJZ^7+4=H zOHKX|n+L_h{{ZCy&`j>0CUfxKU>|lsVK6crOJ?STNG z97lH+f@>jUy`_T6rQ6XSCYc%BYUS46vkcw;W|+LfR2m(>K3xH{0 z$CRa2Ovo!_Ran{Iq$b2-`GM|SHHvpL^}{KS9RU?A&p5kTe;UM3ySjOnnk52l1Rhj8G73iQl( zcTdwvue`hZ$Ciqy<|3CRLKUS;7U&lK2>MCE#Og+%B9H@cYi?nyR8m3_wqK?0u0QPZ zFZ3dZL>2dpjXoj@ghO(?vbR_j`{5q037G!?K^nWBS}qT}0AMZu0LXLN53~b)8V>xz zN_Kelh_cBON%tgiUWORu`lmnj6m;?ZPbr^>0nC0Ilqr{=Nsuy=`2~FjH5aGzC_f*s zq_{_@=I{DM>xbrXkyHnm@duWZOUc9=Cs+G1DD_HZg~W`q=2lk9Uj)?%EVz&vqWOY~ zh{c7ZGCZ&bkEC`P!Xsp1j_)j+i>YWJ6huCRYui7Q261s$^Zn)Hq)NP^bL-*)sZBDx zpN?h&9+_&d*oaezw}>L_M}bZ^DwI4-Q#>-peh9l?q_DaPc>K%*Pg;S9zLu1{Ph>o> z?9|0z!U4$uYseHQG9uzMMZlPgP3m1556KpFp776@EI}zjnP3sru`C!)E(lA#DRGik zm@5+JFhFk>fr*o4Ij%qB1p&nOERDA>WyDR#yy4(SJIDP6vXgWQxXs zxSt1X+c*V=HDeY>=o_*^fFYYq;voqZ{{V3g7?)6EkU2bY3{w9981b=SBnF+!z^qOt zmEYp`iqZBXR8iX&veOO(K)J4a%Q@Q!QjY$MqaOwY?ig((3uGJW5{*Bo;rZ7kz*m%_JL-Md9!7M7FIWQNlI~PmRC79m zZTcqbHLJ@k&72+%Wve2?xf~78X~5Ad^c(BU0ao=1uDtn+J)oVa(9b_;ly%D2^()a5 zG{bLm9_7>n#d_MSyXV?(#pLw#m}EB}4b5whhq=v?pC=4(tWU(bj>Hx@8OlaaQdWps zk|ZQBna04}qA#f0K*g=K_LU({PuVCSo$&tvW3t}W%z0iG8cW7wp$HF8m}n!PBooCl z1R-BTI?UB9x|9h36y~7~z%pYt`V0Av+FVhrjvYc@r(p-NiFqNe%MsV0EBMj#2uSr$0qmeF$D3Xr8-`SOG8?7Xy zq9u1wmF@*VtSCQQe-NQe7FI(F5UXsnA~yJyYBc~Tmr;g^HnV`iC}vAy35ZXm%G?M_ zp7D__Wo+9DpbuG$cj=fH6igaBW>k|JL;|Kf%vC}xESR}4H@s3m^;4-+3p$jusj9e27}xd?&K1AW9wu6Y z5N9h9HUs|vAvyYBBFS+uxivm9>%>g5ZF)MIS`y$Dsn`9EYGYk_V|ZEcLM>Q7@3ef4 z!>OVa3kfKZTn>qXVdKOA+?V6bF9MADX3Z|R^@nkt!K9!l^nmK{f-AH+;NPTsaDTB9 zU0*ZQiIO0O&K{29v(w~aZrAOUhLiOgf6Q1nocz!7#ma92LA)*C+(Y9>;xaDR+8{=c zClPWO?fPQfriWf-?73yrBjQ(}@@^0uw+B98C8in+J;7+4Mj^sn7sN_nYD9MdW5RpF zptT(HrW=QHmg7gf&T?5B9pxw?_|5wgh4%YLdgp;uzZzMMV>Ws*!R(hJPZJ%V=ToU6 z{R|zvZQ;KY884C^L2<(d7YB#YneMt7oiAt`lRAVn9!YH;*&s>@?JAa^62%F_5s_rd z7u}CS?5ciZ$9OWEZ&na^OEZQcoXR+hhFvto5H+91Wt5&G*Em6K7ns$VO7m56$X-at z$_ly+;ker@BZvt%Qw4fFLV>6`Tt+lD9K_nZ7sOe>@52q0G>)aIFA?aK;Fheft1yN> zBdQ=;EWzZ=JhIqE6>(eUBLJ5wC@K#SX>eQ?Q!&t5UM3NNECoWkC8DldIrE0`qwEs% zoE?T3)KJR76e5KIOn*fjR5)_E`Yf`ta^n23LY0|;dVc_fI{{WJ;WtESz zj}h%WJVOOnvoJ7kZ*c=HA?wUL4MJiwS(yg^00=iHrHQ4S#c+Fx@p@f)zh1aotH zWX>6apiPznuMwPSX)EGojEufb;sW%_ZJ&gDd`#5{J)k?k-c`mdzVkUO4+Z=44em>w zM~_ot&WS>=TO%&i|krVU>1{5y<^edJ04@aN-l z`@$JM7$T$cSl6V>{p`W-xqv(ABTL>GEesOX_L@!Y5Xmd57ELqbDgMXq!{S?>CUuxw zIhCN??iF1?_0bV&k1Vjs00LLI2C3X4n|N*>^@E>;a_l;zZRP`z!GI1gOK2)6vhwwDQ| z$`gbC042w_2?MEwTtx$G_Jo0`U^7S%eI-cXzr=LkFU(hCqEZxGA&$6iuk{ry*Zsr` zF0m-f$b2@y&iqBp-`f+;2v4G!Q}vCpH_MJfVbApC87F$Ty%bsTwuDGY`DL@V_c-UN6#xHx@7PnLD?y7h~o zM0H`n?3XQeLigtW!rOHmU^CG%<_}aFU}K>CO=`Y>uyI+*nCXSG1+!-pc+3?|uuivA zjt27vuD=q4Ifgx;HOV_ooyd=o1(p?F#14IaAhkhDhJ1uTowWPTISV3*te+%XsKItb zh4zF{2LAx>P}kRuKC^X(lbCcV+~q4q&zPI|nfz@*1FO9K%*z*l4!&j-FF*8(U0p4o z*iHA8vYwaF{>)k5q3%Lp?kXFV?J3w8m&wxN19a9RA)&+K1u3-xrQYTn52UhujC5&f zkK)|4%Cf$am&6FuC;H>5sf65eT&rVI*J(nygl;UJnSt#rp_Uk|K2!HH-kL%EFt(8v zDJ4-Xzj?p}WpqDbt5M6-6hRJKjQ|P=-NH|(^^BL1FBi-*ag`(K2GM6z9XGAZuI6D*b4Eg;fDA_#J z+#Bk?V>6-mh>~s-_uQ+r^_cfKm_V8m)76eWV0-ycsRvhn(U%7_T)z|k>RAP*y$O_b>FEeOm>Xp_Ll) z8wi4RQ5>zqP;;SU+Ai2P^O!335t+{A5o3e*GS@8dmb>(+Z{3vn&@0FjjA!X4RQ3GI z(eW6`joAK11=eN4Q;B5oe&eJxGScAu&L3U;O3ArYnh(sNwR{}E@=yZRz-Wc?eh@Y~ z;t21QTMQrA`UYRmp$7WkQFjv$X>)4>>o2vJ-OL0lw=mM87MT|TH`7^QF!B4bTG%;q z%UR_Zk64U05^`Jq`%R4wAgguxI`=UcgG_N8;pmz}y7`yS`sM-Uj*Y3GeFZ4Z&lW>1 z1RFX%BDzp5h?Yyf;#Jk}$0=>zAMY`fm|-O&A8!15#0p|E&gG-p45xfV6JFe&y~-i| z{kJL#AM7%}-#6^U!TE6dJkI4{0-(5l3rG%kNUx^n^-qLo~sZ ziWaR;Dae0e}tBZ>t8II!aCQ z#;Gi2=h+iEiEDJ9!g5N7bMnF~ikN|~OK&-)30fvFHzX`Jv>0cS7cP`A0b4K~p`^=ctYijM}S z#lN?iTrG0CB~%x;n7}wU{2&&4dP`S$-d`Wso}C6U=B3>kTk#x%DLay~I*5See~>U& zHqr^YdAu$7ZwF=)Y_&@B`2QP>msb6AV`(MnWT&2|PROVLH{Yol?seZP}v*W~i51UAusfUsrYwt`$gu@m2 zl8|w@JrVexq^zJAnR#O^Z$8AS#PZ7{Z}TvN4t{0P$KpO+wtnF(cHB~V;pv2xgJ1ax zw$`82Ao0=9lh1PZf>xIZ{lQ+sLp&@W)W?#-Y&EhKFVQLr($G`21J>wFt;avaN+|10 zSTw!IS{)1Ar5y(b2ro*`7bSjM9 zAaQ72(i9|LF4U`Ua@S17FT~C&USPH6pb>>*x8hd0T@WML{-IVV{6I%;m-Q*}5CqVq z7hg&Uu1Gs00c@l98$}PX8jH9@s>pOWl<#zn=pq{$>hbFiRM1e=rqTbTT75{gQ(Hv?plrzh)@J( znnYmWJjNNE5FN~8=uvLa3|J)4GZg6jh=}H7?sxJ_oeAtbdrJ`HJouLG93&?S!u3zo zqdb1oDCOL`%TRxK8i(aBAs`l$1uY>8NR2Plz|HD*K>A$8o(^sZx?xWYL4{$NbPCJn zQLcKqjMkj~n8W+>SP~(k@!12IeaP$x90?Rq%%>it<*=0#c=(kjlzv#lGHM}?24z;+ zwDV`Q6I^2juzuu0vPuNdeaTa?^inM*8BOSE{V>x+a&_c}mj3`EH-QHpPZLbUZ=2haq^=8_~C;`=QE{05R`iHrUm5G z!!*o;>it6(nP*cz>RRi>#neA|9LzBRX92+vSf=C~VSLnXD8Gmp<~4?^?-(n%wiZ_9 z&k3b|d5nc*&`S4 z4=^uq{6%gkob%#nh{u87yu7U~9J7z1>_ZNIyI%ym3b~rlc>Zd zXm2si+^_171J~t@x39|&nxw2)TX0+I&CNI>K>b~;GF#bp!?b6J3aegWG!>UCkMSM} zBI0OuQxWCYh?W>^bK(=x2mA8W<&RYaU+uak7tF_rR&kGZ3m39#5%-8mkuaTE{uyH{ zrwnr`W}h8Gr$!rDEdEO7!N;VcM&p`b?5EgJcoN@M9S&T+timPo?Zgq8n1d#}jS+Tz z<>=7VVV&_E?HB01Kt#31tE9QNUy;;d(z*vlK1qvK*xUnw)O{6oj)<{i5A z%&1+%V-J{}Bf1nu4un54`Q=<<+njD@7z$uB21~h6FZD-SsF};KVNh$RtUvD&COrYQ zvI$F5T8re5Ro+3d?1x74Z_y{56aK!@(b91RSG2KXN7`C^5-)bsFkZ%=Mm{YM4&I$H z`{^@W`#^<8FOnWgZrFc}y3qds7F0(yX&dv_$9P)$50zsA*#vD z&t@~_;spswu8t#JkrYfV^#KuoZ`H)sr8_**AOr)hR=l)BUy$Dq} zl_OASaLsx}*oFtkf>O-C>saV4=Re5x!EK`kH~fjy7Z83KNMnIhJ&gg>3LI2Omlh{A zFs=}WCjt1FZpl_@q3wx%ihy}z)t1NB5v`@Zr{Dfc8^=gf_{$e;qR~*j;M8IFW#s(? z%bIRf41`uc^TZ2c=g^PesPD|Y9t{it9YWqI96d7(jYHnLgo-na(rr2EeL7OaaF7FDi_T3j@%Z`B~O&{cPJ#x z2U*b!w#HMiMIyzS=3jBVdo#*a?ikMI!{$x2?5kFR5>>W4E=Kj zJz^jXq4}KN6hfE;Ene=2$t}%w))3%S8k?ZjT~HIuqt^PgxM)*GfgmZ5tYH* zp@1AlK)VoMM_sIBir_&F^#KJl0E*enA?;ByLh~}JCJzVETa=~Tz zkS?m}u_r+Xs()oJr2LY@TdN=o)+N(?!&Wg0Sby4^J#&Egmgw>smfb{Dwc1(lBLN0U zauB0~)>(=8mamfc%z6q%VlOVGsFxEySC9KS_u4G1$NP#7&2bMfF?EJlP19H35!&Nq zzP7`z2ON8vl-At3zYh#DRM8xgi=zyu4WI(>oNBy#&QvG}pl0@XlqQ{_reAvm+F4g` zw5RIn{6%VV63%QeJUV^96BKfKGKZ?->CN^t}dIY?M~&WIxh zd2~_s+xmm&f{IgDaXSsb#2#7X9nTA7_Sq3zWTI}k*?h-m{!FAPYo;u~s(qzaWU?lp zIQ567ocIY@GzWDLPFY`gl-C9PTMchp^qXcF-sC~%>RzFI&M7*2h)X6tVs_JK@_CYA zuEs~NB%`{X`yln5!}Y_-5_nafj0)>X4OoBi?*fT}D+suO zjs0QT$+r&5Y^1#4a;*S3Ve`5!H8Jd(Z`c5xi)$6?I|$m=Q*2l z_VF%;D0AK-SmpMCMbr$P*izKU!e$B?%CGGl*_XMpk5*XZ7lLMtr%9V}E$yf_Sox2V zh^@PZf)FUT-YT|KTpIP6d0?<2T@bl?mxT3F2NvLz*5DpOc#B`0%!@81N)rOD)}|u@ z;wiv$+$bxjDC^%lE=HEwbHg-b{?Gpjd|k>H7})FHJ-F(_H> zekB*-fykwnbVU*Jz~t3Jr67k$DNr6|xLTRm*Xw^b1zO0 z=te2!)As-$H`u~J?q;!6J`e1FfG0u&-hT!qm6+2B0409|Bsvaet#g>5xk7l<0sSHo z8J9gF`-|kaxBg0`LAZ^{mOWbT9M`Akb9jOFfrk?7Qx=BK;cUg6Oqnx>h6j7t#HlO% z0(w+l#N_?V>JkFg>=?cXTZQ>Ngar+}4kwvJ-1&W`HlktE={oZD_n3DS z`DRkvxm3A!BxO`P*)!0W)Z;1t0DR6w6XAf)RMmNe-4{HiViow}BG)7W$mjbRicj$v zJu-B|yT&A~E&(*=4}AxuSi4*aPW*-*T09Vn)=Gaj3Smrt#y+Ju@f@PERlp-R`I?F4 z)VNaXiJf>C;$X^-dr!omqZ5^w9AcKtB-!AZ!B4!&T5%F*{pl+JAQ&84nz*JdTuNLX zQZaA@B77;bl~BHsFh(D|z5NkeGbm;U%%(}{F&!l9#G>M*W?)CUjhNm0PBcb- zQ<#;n!b4%Me?V5hFgKjdD;3)-m{KDL}GDK-sM!?7KDGGdn|@U(aB&7!$hh?P zg^6J)O|rjU&`WXsr3y(S=0nDs#rsUwEfZpgZz?R1j#hPe1ty9R+XG7KS_T z)-69rvSP7m@53`A$@AoUfUF#xkCIjvbu+dgG*$S_E1&NrIrO*j5)wT!&;C`lQkL9Q_rkZHQs4F#=c=P)%c7alVrHpg~G}Mt;0-o zJNJk57zvTe()_NGVtl5ezb?>WTNk z{{XWsOu+H`Fk)+%UH<^&wR#gJu&s1zBP}2FeY~5Ax3n8BU?Sy|wq|Z4 z{gziO4h}u#L;VOp@dy|2OwA%38F4ehez7Y$a29jE+W6 zX-mfAh<6Nanf^j4fYds$p}b3tOuKw+O0#^WxMaZWow*_kmtCJ-p*5q^&>NFp0f^Rh++bu zo@EBw$Mr3h21rht*)wT6UmPcl<_OrlJ|LI|V zWVpc8#B3FwA&FuNt8wuVTG_(DMj%8qAt6|+dH}E#Tbo;7LMSp+be}SmYxZK4E-I2;BE2!gARv1z}Zv$F=Bcg1g=>q z?x}zsIAK~Uv5qAM@5f%yiuiJPkCw)zNpNNDDf+-Ig9_iZd4bgJe~@VtShjHANmgQ8 zHo#=&E|rjd@c?m4g02Pf#S0YK7T*KyFOtas@CK4(HZvVH%)H~dQej41n@M9J@3a0u zu-?KrE&l+RV80LH>(UdmM*8w~_>N8(0R+H~5WP zM`Wy2`HekEQ%JBR%?tBX^^!d%rX70CPUL>#R+wRbnV1ge*_s%#yZ*<39&M<|2|U3p z*r96fQI31!0njDidWe0DNqoCJoRB_JSj@AC8g3-+Jzwi!OJ%30xqdHO(luR-)dMi1 z-lk32$B2LTBq+8`!9pnKQp(+<8;igcjNU>8<4^|Z4W8SN71@PNWGKNV4FO_Qb8^%PU7X1^AWUj8ww$yaxszeZJdm`EVw;6f(SIG5f-j% z?-gVqf0QMy;QGvWoaXZ{d_cg4NUB$?pwlO`#)H0KiFW0eFI0g=T*?505Nd4H4%QX3 z@G*^*=HnTMh)#?72>WBWrxACxFXA#hz=9TiPz3S&f~nshGVb*KO9*ce0X#+m*dgU^?UY~guTqM<%L+9 zlG{zicEyb57NYrC&O3Zh5G-=QKgmiR$7E#;5}N1xEAa>2i(dF0d14kTF5?Zy!&ML! zqs(v)QpDuMA#lf~kh+(TNB}Ef(cnHJF}Th7tM|;Pey=Pwi0EUqyKVc-HYTMTlTDtJ z(@S4t5U*~cPHgOEY2+DeEWan2T4lD(oGLTik_OTtF4XVc?cFL@zMJbp9cgb@$95FVoT|EleRU)q)WHmOWS#3JQnVN`37R zU_o@t9^8aOmsPnib^>hCK zNKZM_#rp90Wz?glKV}?r6Q1y4EV+ou0SR2hCdGj~H=mMUj;r$@uUH`1nR0Ms_x(aZ zUQ9&mTZLh0$@znq3|&h}r2TRh#j*PIg6ZPrejDefpR{b}3*?waba?R@52G?c?Wcnb zqiP;C5a`{1x`vk`ucZCLZ(8y|zx0&{t<4c~c0jekho#~e&N8?e+z%rctFV_T`WW|< zuks!zw=)pSEb3UYEzE`K{7?VH045Lt0RRF50s;a90RaI4000015da}EK~Z6GfsvuH z!O`&H@eu#o00;pA00BP`Y9(JXoDa|Whe{IDsBTMOj%lQs$PGu7NM%Y;!T$h3dlvry zp@6Dtp&aZEy7Pg^rnj)O@&fkY`*g7Icdv{)HVDkxCdAS8ZU)4ph@E`C$fuWR`lLX3k#EVyY63X}uP<5Fcu1$v&rS>LO5nMe zqB5ojaK0P*!gO+xcF5eOTnBL2_2&PFkl~WF)kDX66|nVdeSZ zTW}%K>CKI-WRAGF8W+U8U7@8MJRe~{KRDL>V;yIP4?c51r(NSqcmjssP9S^MLsJ@Q z2z;7yXhHBzYWNS{F?op*uzz^KMxgBpSqp(}NGZ;|20EsK)@}WG z#E?49Ky2Urywnu60(fbv*SR)Zs?0R}~3Lt^eT^w_--WhBM zAP`T;RTW`e|yO15YQ+4;~NwT4)eXZ1KVDfg!u0&B3R(KA-Wu2xJN+q zi3SSGlb$~_4>oKD#kbxrw&`KM_`+&vjCub6?|9vDpi$<-crdULE$W`l;a&}e=uMdx z<7kRUS#*qT5a;Cc>lZ?rP0xql6V_IsuIPso_4vtz>ntY*1Kv>K*wonR!gdACnr>YH zC~7<0;~m6Nc3J%5?imSG_jKVmrL#^K`N7JEQ-fdT5vJ%lCu54ICDYCPCh-j+E!^Ar z$J8vssU!EA_ppc?4okVt@e|mF=uDLEGxgWhdhzsQo1NViXlxnlAEte?S_T%M8Zn?*NC@A$? zR}HIjEI=*@@j#ru{p5E`{0IR0FpkfIq2X~#QWH?};GSV|@;x874RcgUc|H6%^^m3X z$IaHStSM>0C#c3vb&Unk3|?}coMweY!MKEOB67n@6J8Oi3*tqDt6#h(xo@i8o^2_M#0UP!RjdR z*7%b&s-e{Tb-(HJivn^Dl=t-JlrJc5fzzsg+~th2&5Q-Vc&pcK=Wp<21OgisPQULs z8f!{xX=9p_Y0Xpn-BEOr8*xK2MqK=C9d{m^wVJRbf=Rb zBc38-KUSA7;{_@c!vT-RUb9yPsJ}9Cg8PuwN_+8@Y?U2%r{^bUYi$PX^D}zsa}T_5 zg4^ESu@A%pPSf?8ka|d`oPZx_ZR5^wfK*C4)Aw6}N5EfZ_i-fXgh4$$Z#35@xoWl*fSzPbr9vvQLk9j6e!DoZ~5Vc-Q9^(AgeR983@-3RMZu^O7iPeLS#Y z`E=%T>j?9)sTA}0#;PeBw0P$zz*3x#N8Wc+r-vp9y^zMK`W!o%#zOjf|d#i8;r9#xrJO0@^4510*?+#%iHcg?w zJ>!#iOSh`!8m?;d==zvA?gZq2^ABlvHSXa^sZ(np^3U4=&Uruql+W!E-oXP=CcYR~NyVS1V9*ilVi1K@Wu|sSXO$y1*LW&$AcwgQ^E=r6I@Ba0I;S}t2 zKf#KXlmU+3vG(&8UD@8{z$^el)Mb6YtUUTjvH|&Ae?Kd72|WI=QJ|m+Kxkgw^O_xp z=8hop8Rp@s7zacblW*63;&#DTTH{dPj0Fx;%2s-}>j%+k?f6&QpLnelqEdVZjI0ic zStZ)C(<&9oEURXNbC>uoi7L}J-p!) zp$X8Dq4~qCj0tzngWHLr0KmF82iu!p^ramUI6OMIqHWxx$Q(m~>ce<^4tIk`(Ox14 zHys7+V(4vh19WU6ZaFR!iI-l%JU)D4zXd_09?&;F+-QkIS+05fW0i^N>jlPApvWEv zuk$F>9yNpW{%1VZBxjuAm3~a*5eoeu)$bZ8fOXhy_~#DJXaS>7{r8*1TLXi_e)CS1 zR&r?%+{*?;iEJP8;oc~T&E9ql4>NqYKcS=zCxA@=aG4BUBE*Ow07rrA3#dg$O}ZRJ z^)eB3Ll0REas%nZri^xlup-_$$k(<+)Th+MvgGEN-f$fURyE&v#3bui*05;hBo|#< z;c$ees5RM+kX_w}!s4wF&YR1*YfY~gyc2}P&{xl#36k;RhP`De>TE}!82ZE9Rl4Ke zS)j~HAoTZ^TgC;o2dn)0!#EKLsiv9+QUP7BeSMB71&5GtUT~7?xRKfDKO@@}A&n@G zk9SwzJPa(3j?Xhs%Z~d&;o|R_ei+Va1n|FXHgX(eMxHg+vK^7Vw+7$*ti281o57=5ZIzc}H<(!ht18hhBBm`^Y6 zkjp4urEs{R)d*g;)AFBrsohtE!G?wibN>Li6E=x?x+rGM5{PZQ-;H2~);D7?c{zK0 z;iy&dc*y03Q_e83L_2$l-YXH>NL+)zK&IyWVhGwc5JWMMLYHqpe1GOkqq>bfpMw_l zXa|S-VuWPZpeGmWC-4Yu`Wi@+nm&roW85;iQyncsY3z zK%61-QTzC0G1RMo9CV}I0Lq|?TJx1cc_1HzOb=tP_rO9Vz0&tD`JH2 zi?`lP8fl=scYM0}!2)zDx~G-$V4%UENvv99dmu=sBPZxc=*N*Y$+35Ul`)Gn;N-!*^r2WED94A zs%VR?Ki37^RmYXTtT-SVNPqi*AqbHE4_I+hfQpak;~*rJYV-d9xY53IA-#`3Sqjlz z&Y7nx*Ar*sXXg@I0luTlUX1aGr#ka{$_NmS6?qQ1aPm<;$gSsr&LbD(XYykb19n!w zPJZ&dfQurtZ;SVb)PutDS64(k!ZaZDawudShka(mT9&=ly6EH94G@(;8lh(XGmpY3 z4OQ#)kY%tZD$kO0{^w$buLS;4jGzclEpq*zj9`>}L>FH)ox0%I=#bDI0l}LvJTiG4 zP;D1S!a2^U3Dr)p0%Vmh{7iI7ux(y}yw(>Ti_kOkT#W;EY~63z{Vy3~Ifj8FwRPuM z-*W2F%itZXG4Q4qj_n6suBu!c2$M%pEpL^_7?27R!8*mEptFUa8o;0wo7I#2PeBB?M zD8?$$=m3G6Xhjbl!DxLoMe7=9^JA+~M2e9Rd+XywLhy}&QLjSb^) z=Zqyp(JfPuezE*b94iiT6mC^M_ZSjI8s0ADh{I1Q)oC8^>E z>m0CZ4TYEDzH?s#&op1w3e`U!3iT+8>14)R+_3a>ac_?f6K%9q^LZcfm2*k~o_csh z?xTi_PkO&g$E3gkS#VekY117q82R$Yq&cL)&RS6^rXbCXW21Z4*aEvkwLV{7JtiCz zji8CL=PSooS6a)6XfwVWf%%8PpZomf9T4n)Sac2q95F-LNn3#`u8*uETlEd>=X)R4ZGnQ&o&6cKFff--$HpLR!55=WxgYa2 z@Svaz%kP68NQ+Wm`>zHGo5@5=>yC4gpp6PU9(9T10#7wgzA}ObT&8H=o`c!@#cYex zs5W`Wz>*x!_ksB!hR%R;YZ@7gW%m8tD=f*E$??ux%;aCmFz9JSRDZAY5hS~C1EIN5 zgH-_NNzXj`!K$~p8l^raHc(8OX?VdFA_3anbE$zg9gnMxcjF9(>SBJiiW)Py_jvP! z_Ig2{Zu$QJFXICJqi z#+}Nwiv@T3_Tm8suRFN~VE`KJ*X#3)Cc;moaG4-mG(YZBFe15MpR@6sK@d~I{{Y^z zXon-~%*Bve8gh7f;L64@v<0JW;V*q0N{~Tb&)%>RTJ8ixXG0u`W?LbEo@4c$&=@#6 zW*Y8ZfGv5ehYOguw}U29yD;y`>kzD(2L3KfooyEaQ_(Gbj{O*B=D{A)@ra`NkbF6# ze7_hz9@0(Lq- zh9I~Ef;$`!j3EyIQln|V4j>k!v8|}d=%cc2A`JaQAfvo!7S2jKK;XN(Aq&H+c8 z_`v{l^3fuGE<~RSs7KDv&16Ua0JuEOtQOuw0$7Is0O5LH53J&nbhP^YIk9wcu+v91 zHjjeyzgRAOp()?aD|DwuxPSYKAf2x$NypAHYE8WQ{p3g*N!zq_!-YIxg$LiP+ZBRb zF|^hjLVw!CWGqujJ}~*n8s>67ac!Nsyfo{Vj~wL~z{|s?KREZc0i(vRuUJ!lgbj3H zxJIxD=f-j5)Ei@I9eK-t8(gmwwBK0Rn=&Z358fbpB-3qg-UXyDYqunnUX=$I-hAsA z8g~Bx(*US6uHOaxXEQ|87}wI#9v8lMj`#z1QvUOZ%06dh-|>tiuuevvJm8@f03Eo+ zFe4N-Z9V?52P>&gx4-Kic@c2i);SR1&`Z>CsbB%&`15hU5lcwoYpmswG&@~uHjyi? z8so-p3i5sN`!XHr?FjsRWdIQ%ZYI59MKRbN{m-0Mz7!kl96n_c(c=j;+8f%MU*{yPZ3sop zyx?#MbK3RRIIh6$9(m&%%gF5I*p9N)R_4^7AmG4^>=RMpF;p5_Tyj^`^VSX~P(iO5 zLWyN=F@k`HQFu4r4^=_rByr4cg!~g`@|szMKNewvqXa9|^{i1c%20dPynxgy7$&>( zk!nJ_U2!poVD{c0SX=>s>*a8TA!f}z>lH&*gQt()L>zXRdzS3q8D^-WRFY4;1O~+# zZ~kIOn$j)a`s4e}Ah@_qaQ<-l(vY7&KJw1J;eXrSu*xA4bK&Qa$-tEY1em?GVwpOSvAQ^75_z+s&uOqHwe-V@`>gx|}GX23znQFy=(DKw79*OzXy zL;zTCA^e?uAV~LyCoL7?_kc7*frlT!~8ao~ju#DBbXQ)@y6K<(?i ziaoQE=sLXQbYkW3M@BEEB%7_s4gyLU z2xV`RPW@i-Lr(;=nhgoptVI%Av?db}S8n}4oaK8krPkGWWR!}PEt~10KAh0q;mW~% zjd0K=rQ(MCEc?aX;Acm7t;{m5j)S=Qfyvpkhb#@5CtYIJ19s?ZsvfX|K-nlBgF^N* zP<;uoF2@eg4mcqv(FTznJx$s6IiHY5fXaeCcyA6azXf*35VDyxQ2~Qm36W}|8mW9?Ew*rKbHNjt_ z5;bVN5FfHXcoFiARkZBe^@8E3q!^WV5eIuFDkwxl)~^2m)1Nr$Ns>^LiEwGt=JHY5 zeJh5Y4!^@Fjoz$-yHpG4a?`T9CzrrKY1VFK>+FWM)*6+`g;u}^UAPDYqC8b9AS6mS zC>@rCb1LLCpBTus2S= zrYjRRdkKu1j#=Y$CPtF}^4GX_>AZKrq&9TcVPkcuc0W`cE^E0TO9X^h7(iYcV zKD=eHO^DKP8Fs<~DtL3>oDD&?r6a~g1UnOttVP5r(5u(|-T{ZW3P9`mxaAd_SVQ7r z@e_Pk8-i0wQ)#{{fx?!yph^5=5yIK|e7QJnBf+M1#yzO!rC?ow;A(ZOKwqTh;b9s| z2w`Yo2UY95S>%o5c@Cwja?9cb)~f#iSeSSqZ20-hP&R@@`@s-)9T;R~)k@!-V-VN~ zbLUuX3N(rsqpZ7{7m4O?c|nya>rwtPdKFdB@il+#;AcoeNe}YikVvQzJ~7$o5Fhi8 zOyTA5m#j7d!6V_%h5m4LXgh~`dctVy#{A(TbHi?0PpkqpqXKpFkYL{z{{H}+lqf0= zJ-E(LO(IC=@2n)z@=LCD_xxd65X%WDGE3O)C6k|Gd#_x5K$Un6z>OpM_HMK*lf zyZ~9x4!o-Hb(8=Bx(>sqtcn_7ye_``#!EFZT^|0imtk8e!;b!A>~{FhV~Yc)zx==m z;71j9Z&O(xH7LZK4Cm`LpMt88}FF+_mULSc9r2wmMU-z6uL;_R7 zzrPtggB;7lT|2}ufkzV3xU=}hay7sRc<+udD4VMVMt~9gV1S^a7}~2=4ke`ZQr2+| zu^wNZum&_bIm4g>P>nrIO+J8?;AC@Pg1c+}-UZE!O6WlOTy7Dl5K`~rW1b!Ny7Rfi z@p_LBK)e{NCeukO#c=7ILW#l1VYo&DzR+Mj>*qFyQihyYY|23u`-$85*ObD&QTm^Q z>A!e<>{8x|T|Bj4SU^(BL%!~>zPGbA^FtUNmB z{xFGX zDg=P*&igWLlT>|SxA%|AwFKjay*a+Hcs2n;S~^PPH3LPogI&7Y%-TWLx9nnYf;^W22pTmsswaxaotPq9LAHWkGCqWBq;$cROfC1y- z)XQAND!%x{kZw>{;kYPtd8qh$!~hdP)Wp(ASSjYofEG1Kp~V-ZO<`TQFF6zo%rG^B zVL}^j-2QPcD%@DeSEpp0z1=n*Q}+2E);nZByea0 zD%g!TzD!58nWL&4qe^lE(~kF!8hYs5PuZ+MA#xl6oX5Km_c1!KFaquPw-3Bz)PU(| zc&wGCR-;-G!W{$Yy0{RQB^D$dg*oZRN`xb@NJ)8rX{u zH=HyNY-_C@IHd?^97{=w5~i;P_4=3rf~=zsAJ#q))m9Uu$9`@}oa4_PIK(32+a|K3 zQi^OoFx-aQXdQEfN_N60om<8kn_5`*TtNZYT0dMkQgRbP)9(Z==0{_+yWqsP5<3d( z{P%)pGzB-lyj*Kc>JKX%(|EikV^(;x{?;O)PNvzR zrNJuxGS*b6K5sY9JnX9*FN2o>l~i2=*XJ~D7CU?Y0J_19A>(yeK!yxS6+NI^=rCfyt=l z`pYtU)8GC5Vkpz5o&Nw@ad+Y)XkaLYi@kh*8CTlyo;%1+i@=uVg2t4)9D2t*3u}?^ zelU;sir#!b^9LQ8yYJ%@LN~}qQy{D2C_FPOx>o~z9Jv5u6zflS?+7>%4iBZD^NCHt z0FAzm+zDPF=>Ulim!Ch*C8(Xk!xB&e zzT5{Ews(n$h(RNG_tsIFR6Cr0wVKb?nv3&`%@jPNKdd25Xyt2wx;Ht>3ULCyl;;II zJW{_{cSwlJn9ojMxHSSc?DN(JULgSycIrQDtCa=nEfcP-mt1EpgkC>ZurQn z#6eFh`Ix|fH-9Dn0C6{Vodj7w?>MV2N*g}+rI}8a0RzH)GfzoS(m%h>F#^ycwASYS z8MCcLBfkat#0alZ1E1pn0VU3Iv)@>ZPIA!rJY$`nn__EI>nS~;e5XI0&{3P0THR}U zz|Nm}P%gOdD4<13Y8cWL>jH2#EH?G71DdoxL!mOp;Zko(Y1`g0_+L07z#l+hprCvq zjOTha^M@E=gmD1x(TF1DkC^&#ru?P=pAGtk0vaw2BvO3=yn`_y_7St`&9WPRl4l1Y zHvKl_9*{~~ly^v4EY^g4-@o1@q=SM# zjA6ty3C6cJn9hSDI@R%s22?Builmq`PHI0b2NvRb(S%QN#iEc1m&_bHs^ix)6!K)1 zCdE|{4^PG_u)rkf?X$kK$Q+lVgY-4;5J@oB;5C2a&Qk|~XaWubN8SmXgg^rwJ|E5k zuw*PEy@YZ%tgd}SM3A1&evB4k8nh$%#WGMNEqaKalp4}e46kghH?8a8?>54;+6|@h zYXGD_F%EsP-+kqmEF5`lw4LMbd;Tt~{3y>1EZy;6&L*8GR(*4E{^R=2$>fFjW?8mO z9sRSM3;}ze47;Rl1@<7Hha?i@D?Y=HBF3ij6^>3O5$SMK_l4RYVjF;$eq7RX%Gx9? zb(Ct*U_1kZ#t*;1kC(5!y9%X=uuuGQyeTm;<)Gf&8F1{L>`~Fh<;dRE=!ku=pIl?I zMz+ocp?YBq-^f;20+WUsPcg;m(E_NRo9kZj{n8R5yA{(p9b9*p03AI(7f;S2hz<{I zI623@@KDtpuJq~a6n^*6N*b_-c)$gnnrS=9Kn9G(?8KW&E)e|LzGBBbhvi}^6w;i} z8MvSe(T)I)Bz!KmU{fam+QBC%K39;yL|3S~Ir_)s_egEiwb?REY^Vg`@L@y{X?4!u zdA^5oaQ?B77hx;-&JdEu{>B&3M{dWRVqxwO7oE=>VZTL+I6P0Y1R@D+QtZIuhiO;F zgA1Xd4_C?Kj8w$7H(v{j3;|qxUOySu`Kg#YL$SsSs3<}FJmNqMx&_J_=kw9+_{`p701@_E4N+ zHm1{FpPW@n3>Jf}VIb4;TqDHe>jG-0Kn?XRzZk%wbSId5?-Dw|#?!y&4J^aSe1ACt zCX(`B-N4LKzk0vCh?OJ3JQ>C{o!t*6u?F{=e%YzXV`TpT=4epLBl145;0vMTPM_8R z5r}HKhg{&R2bTnUCN|}Vga?nG#uBb6$>q9r>n#IZ0w34@Is+_veEId#sz71h@i%0B3H9GpwlQ0Ydm@#Wtqv z{Fk4M6Dlf|x)b%rSTmLhzV*%`RG=eG-j0n-5P;}vE_PU)8|&jFunL;^Y2zW8kSZQo z#n%Q8F75OK>Yt1^JC@SCZc859jKOVBot&Qz~Q9_`Y+_1Bz%U^!fC=8FZL zSNg$FaQ*X)rz!%2ddVGONb!zB2ZAgC}_#d2AoX zLWf6&RCXXDZ0*MX0Cb|9eBtm@0=G(`*5JJvUZES|2yIU~*9K|H&?@x}9z%Z_W_w1r zgS+|1=)+;}K!-i=7!VUc05{;16MHBI&X>E37{@3Ysq3|P*5)d}iUMs9Tu-+FfL0PR ztLy#81fdbKy7i}puQ^1a(G=nN9pjLqA|*RF@Zd^ls-BHA~MkEs(cU>dj`8B%uM<%U$Et7jD$w3gDDtfn!Oz zapdF(REUPpyeOdHDmeFYVYN|R?RVo?mAzQ7_6Xa2d&%jMa!b2wueNaAy(@jxKE%s? zD46M?Ao-ZKwF)nThv;&k<@G?dLzWo3z^p}5@^KCN$*8ad2}9+_pLwjY3IVdjt~7n& z(J2eiZd2pk&3U0IsM2Zf3Ewyr2}Csdex1%tD7+L7g?U4>A;}AtrF)3E6@MPMI;_W6Qn~{LIvoBjnfoE=T}fNzcQPd@#cOVCV1(UHLOm zkzip%NPYhB*{uNXYoA!*x)Ko9{{W1VVMrg(jBG}OAx^F};MF+2k8 zZh$_HUNQ(1d#^4W5Y~k}{Cw*pWyLW*aXnyJs7?NJWMJrz(U1kg9XoVnl1VjM{N}CN z06R|}aYVFF2OZ3mB1YBTo56%pIk6HL?Z810M<`8vBlm@1s6W%AoS266k$Km@^9hJj zB3Gq+V$hUOdu}`Q2i|ot@gLCOQ5*;e_8l1Gr(w7}vsiTq+q7Re3~)uJZ<(78AQTO? z{eSLnk%2jW4_sU^As|XQ>!Uc|4VZg%fh&1S(bhE@Aq+zXkcxa~QK6`G;}5Yf2%i1# zd}9R&1<>92#g&1m3%+#w94AodfQ;vO1hAn9Z<8nqA~c5IX`C7*2S=yBcsW;#RfLcC zg)V6u=-(vXXe^y;v1ii1oME6s{&MP}r7qkax#I;1v>(4bnPCN+>T8GJIWAcd zS7!M6&5I_V$~cznj|W^I{L1`rY9-Q%@stJ-#(?shKint-fVBmAd_P!nkaLLY!=D*x z9Y6x;=i83TJK3}0IP;sei3>$GZP!^;2Ivbw>#Rr^BpW=<#K?_NAd&B_{%{7#(W0}H z@aro)NZ1R<&iKTvAc?a~^Ye))fRUB4uAC#rijZkI)8`-3P%0mh)qFWyNdd2*nq}qV zp?E?B%v;E=yHgqyOQ&_%N8tB{GiO{JBfO3j3Imu99#;Su(g@{8Jt(iPGR0G!-(3u+ zdRn*TaG^q@U#o^7@c^GY$@Ec#k>YO%7R-ek?m29xHNPR=Q)vT}+j>0X)x%Td=693e zSm=GP#~4at1n5Q9S5afg?Q)kIa8|ADQvd+i+DE2(>lzs8P$+E@hwfyta01HEU^M!} zX0At@3(wv)2%R_&c!MRX-S?Gb(7%QM0Jy|>0wg{Ig@u9VAfy;+b_Yf4uPd9zbqdl) zO`Cg|`v4h}-=FBjEj%UeyPsb23Y6(%<=zVlCXI8hH8A7bQ%Lt5UGtv2ASmzeUwFJM zQ+B&OnqUGEv)MTb<{;||%{j;ebk_UH4H`<+?FJ`};DB8P4hh#Iokn0ufMh1L9T`+dN#s!+0uxLzS(2h^c$lef>e*`7 z2L3Q}%A@Eh+3y>G8&#GIL%wnnJvFVAI(riH<2L+!pm}{frRX=-Z3u)Q1Kz;(^PCzQ zj@|j~+k47-6^;TPbRKm%!&xLomak7M=Kz9%T4jEQ`7yPrMB z5G9*TO5UjIWea%Ol8Xzo2kE`zu$(4n+H`u&3IK}lD+<90t98fY00gfkC^BpQ-U2^( z7CLZ(ppjiTV@}ary3029{xX{E2Pxpot$?J{TEamQtp@gBfh@Elz_ zqcEB}wy2&@&Oh#ysqka9(C!Lr_?dPbZvX{Xf(M`sOGQT$4yTYMUn));$;rXAexze3&`B;mnbg z*gN=p!S82XcJk2u;uuYKx^Hda1p`sW7>Q%aiT z`&ziT!)=Q^=*rHMp!eA^ASQt&PG1-dwN~7^BdNTyBiaDGxH*3^r~I1DkRZ`PmtXb8 zV{}gM-{%4j6^l4~zmB|NYyDm{-o#7de;r+|MR@CHH?QNCY%xy}F^ zcJD^~Zy6R!6`vd5IZ8Jaz9R3Iu~UcL^?inb@?_bp5xQ@x_T6)cnFWrhHJ>~_aYFfZ z7}Tub(LJssk=HGHHjU+c;H(gO66hIW_3tVuL@S)~(YD0*^ zr~*{>f4JwJ9?jHx>o%a^S~VwJ_`<-LCeJCXJv(j3kuh{U;xkH-2zGGuhz-Z-74P2^ z-+>kpkXZP=dCr#Ll${UGKyY}XhMTeRmV!eJO^dgVYt|iU^ILHrgY}%AF=}6Pdwg|; zOo3MK3t_Lb4w$JQV&?Ia&P-#EoRn)K1vlp`cw|~{=O=@qD9dkn_?`1Q@ag+7O`~*M zzBT&Az&(KlA0ZE_@mCX`HDFi1H5ARlDCMl$^~NDabJ5YM%Cz`S@@Q_}}1OL;;Da8AiJRB6G5F`NTSJTfet1URZe>hSb5ty8*LimG%40 zLj+%ofIc%~WdPhitOOeBI>E>%M?8A21*Cb|lGrZbl02U{qbSoCVMsXD`omKIsD*f99(*1$ zR<#@OSr+8BdKd&iTlEBO9c)f9vd_>gQSh8FGf&-xW7Mu2g2?-Br=I@+I3)a&6?G?E zBd>kz(ijZm)%H<}`nhxh*gb(bMc#d(kKE9~#y`qmplh@lH~&p!S!5JGY; z-QWITvLmvtobs}b5bq%n`C1$PVh0OnZ}F_qcgaD=cpL**Klz7I5ZdMX1U!vwXB}a- zEJa7gADHQ(ANX^V8>w(N!R~kOIfwUyeGmQC9pw%?KJn&h3X!(s&Af?FjQGiPRt>2C z0CUr<#{$X|NF2J|z=BOe!{LFxxfJN@SSDz0fW4czz<2=5bGd=h zW^Ge;Pdb?Ff;M&&jb&whnk07qwT!$4Kgqx5H`tFJ%w(fopGP+r;k24^sRrU`4A79_ zTAs{O#SIjn{ct51(yb?EZhH{Siy+#LP5|fLGjRkfB8e>!kMAo1t6E>-)7f^@n^2tvR&$$m6uUVd#J$ zjrYbmKuwK?^}g|etc$c_t$TfyXYUow7r zP|zOw#?VTOW$W<6QX-INm-mxFrrz@pk61Nj(bnt#0C~>4#m#@SW)PTV7lLd~&t7p7 z>*Z_umw6PDPEf~gsn+K!)c*0&p7!WRgUiN2frc7W=q$~N6oZ4=@#7C%CEbp=z|)pp zpPwJzDg>k)!paAJaI7F8wI3YuthNF?*+f)wSryn>Hx^P8# z4)TzZueIsCqV_AKPD7lDVFjkQ&(VO&{R2r>zHs!26$=T>=M)32C|jMsKCt%SRBRvt zcH1k9s)T171xfpk@!^9o*BciJnup*nS`(~_;fxW}w`uP<`j9E@F7tJ!kqJ4sc~l{E zL-@vyZiDZx%rqVeC7tUHwe3*+t{e`L;{Nio83F^^$F`S2Yr5|w6F>mV)-ez03?}ub z^^ZzuDQ}s)u0(Bbj?Wkb$O(CE0N!nA;RJjLe;8{($!%D0bMLHy%3$0o0S?WtID6(R zY;BESt_)=|rMLH$ckTid1xA`?CgKGk3KjFymp*8%736&T)(gDaHo6zp1UbgAMNwH8 zJo9sKK(%g`j&V{zYy|y0=CiJVdGh6Vja_5wzpSa_CP3in@rI>=uwTwcCIXB(E+0f_ zMITNGC|rsyXO+I3gQqehZ=OH%IWU?px6>&5A0|Dt?Aai_{{YSf6%1X=-}CvxurY_r zw&ZiStlQcwsihzB#m!UI>8YjD=5wqiiBn>f;z#j`0QA5^&zxDK%7Z*tBE!3w6v7?s4+)QYG2U|zp1S(RZV0dHieoW_R-}y06jWnp3KN2MYOL?4c z-WG&TjdZeBrh&6=Gcmyl4rE0Hd4^V|+#g$f3Og}FV${DG+B?Zgq8nl%)3 zU>_ri_6RI{FQ*%^`iHkPd*q^Ve|Rbj~)Tk8-Ahp&EICvz&4ThD=uJ;6Hp z$iE`1zQ@M!b6eZVOfNA;iR4UL-CeZcPrO0WjR!BE`_3P7*A;i?H9#Ke&K{zY_{V^! z(Q|-oFBI& zrl!l$-gJhRA2SdQX9L9l0B~JSVK5&)`*Hv(b4O{2fy^0Rll^i-P6^whyXjUDs#yxb0iasL3gfP!t;!{aE9#sq=Qv{`zmI5a{Kqe&P?E#JUagKX$!WFqsPuLKr0G^{@zh! zlBVc?FF2&h9ih85&tCHEEH=#-Z>*`h2O{xb#tsTp&}VG_0Ipc7@@*7J)^JebUKpxe zq15$z_;6Uk7__D5eRqy-B7;}w>9|IY0XrXa1G=b4S65dKqp+rs?-Mmay|{oO1szNA z{{U|fZ?xQ$J#m0gjjjFxuhu~O zYP~2AX8FZCQAY*S2ZIFSNW=~7CphG7jhK88Yd_-rRC0}lP3z|mO5tyqVd`6P0b0_h zg396pN!s*KYW(Fben}s=KN#@|wxE&?4?c0R-DAE461e7qDNt_Bhriwq;FOuXyjS7L zi*&8pa0A{VW`YsvGzS{IT%g<|shqf|)s%Ly!0U|J^h#RWbT`_rZaAuK_`+KdED`?z zn7v^Rna?Zm{Nz%YLM0f5@M#&&Ll_Bre3Oi=GND$|C&Po4X9QmlIijaJ@g~-O@lZ9t z7orF0Oa?k8Ea>JW?-`I7F<;Yz0i;8)(g54P4$V=soJk%Qia6WOOfD-Z7EL}-deMv9*rFil6>IU4 zd2(C!}s1q$67^o9dk;2VIiW5Q|s(JnZ9--aTPxr^_v#_DyPN9 zQW{jyME?MC>8xXhX+4=Ka%_3|$0V;qh%76Yr^YKw_Tt9)o*#^B7UqCoU7s0A28=Br zPWOz~gGuKe=o4F5@$s9u=;%Jj$N86ZRt1-p92#%eT#@uPC zHgkI?8F_&Qpf%inb9nWIwzX%@StvJ1`ff4lJ0OT^-!0<+ZvY^ZjgBT78IMx7PtV>`Hl(KlPku21?FU+U?*#-{ zLwD~9Mmbzv=ee2*Zpm&5VN~fn`j`rwgy=K}&+7;#$n9C#*UZ36ExQq6IG0JQ!hL@j zpB{=A_?c`UGDqDL>%0aU3C(SFeh)dqA%S+~7%5=`V9KAJ7`6~P zhL{0uI3hX6u1bdbW>z35AXV_-&~4QN_{H4B8neo`kOl@D&p>1`;6SQ6era0G?eYkLGiznaz0CGZLJ_>Q6A7?WwL6lP9d3oNj z)1-7GxtlwRIBC7&TMy!atHLo8kF1B*u?o}}E$oVMkyvCJr7q49;kew6KZj(j!gWdDjqtcH-Ig{xAwA70NsR0G(qn>QSe}zaru>GmFin=0oQLH3|&7 zfB2sA`S2|co`(ElQP==)uoqf;9bqJ6$Y(0#_EQ_^H9TCCE+KY8ysvnZPQj$_>kmR9 z5diYM{NVr$R2(4f_SU}foqA_Is_li_@rEADxi|1q9+m0_TbOg{h<+eIcJ?1w*NGI? zAq-q{KOAD}ReFy-vM%QhsNo%8BQ4aB7k=Y=^vZ-fI>mL$Lkp|2iP)w%1UhY z>-)-=$^}IK0Pb`Z2)74#tkFgT!*lq^BA|Fa<{NpT993fV@rx%xhL354O|wLIzb6=2 zP$?!CNwuY?jl0Q;!3Aft>m{Lt5nqg1MDpyOZQ}qeT?M>z&zw5#irIYctU#$*z&H&Q zHiX&y@MA%_(#|j)!LC4FGLf=7Pfs|Yw^+J|dp+i_Bn?}xCG(UHveWn9$LAEv_23h= zXA=>mh-^ylU&b3~VuSVh$DPd=NB!P58FWqD3^1LfukSdp3VVCFu7DR?$vY71->ly; z>onY%0Z4^*{&I?p??7_qoATlI!P#gX8SzQ=e^ zqlbZc9-u#YTCy3?_aEyZp)?$mfQN8~F+$h$ThD-+5>IyPYQcIQ8Uh!=fM#-k9E5nIO~jEfe8r%FXIwXU}Ftn;|Wb2n9!ROTk+Nr zYXIDw>wx$>$@B0KAOiKrSmE<{Dp$T*Zemla&0M$S$zYS2cfz!s! zr%G0I@N4?ZeWJpiNHSmriluGy$2SQe6_oPvhK`}CPWOl4>kI%;TkcQOf!rF3z(Jex zPR{VGZbAcW2b-(jy<-q2YIF}g&$pa4)mAo(=j6x1i&@wu@)*Nyl&Q4auP{7yg$MSw zHijo~J>}RTL*Gfxvcjs4)D79^11sAdwL&Qqez74HQYR=N3)E+vO*GcM93xkx!Y(!e zKj$@w5#Xl|K|6P+j5`Hs6*Mm{p_OJd)LkLZ*9VUAbrH`z@7_PMe6cE=ihi=<=8H66 z-Y2xn+lWb}y?k?qlmLZYa2Z3ANNK8bnSRY0kQL*NbSCMi=9R5mrNuUP1N zIzS+ChKq+03MXuWt>ENgLS&eZ#Bb{QxS8H%Dhdx83+p!+Qhdby4mjy@k03T@89*XK zz{R@RQnYXT!4XGp=yUM%tXhh=Z{r!TZNHw}NS2~Q0Bpc5;k}p|PfS%FIh*^Luu%3r zIA>rG+~kaKF$5~w&s(|6(~0isTkv82fu~Z{b%5VJ2%n|5ssoqqE`wTq_ zFUzy2BeEHN17pxY9NfwiH;Bo8*HK4w|V(9w;HRSIKlmTcH(Sg@MHtg}Kj64*h z@v{OpbY;6}e_1Y;GAp2H=rIKV2AMaZ>AAda?h(K5I0pqfA`gBX1H@;Rjg_q5JHQva zMb`{6lu(wvVF)B}(wZ`|m}HyK^{iYS0s(&TKq|ceFN_sMC=Ih@Rd^)y{!D|z3UEAL zKl_XrKr6Do@#=_+cOO{!19m~`Wr~Qph4{EjiL+GqZn5;1=~v?bn2Iznnock&5!R7{ zL_R>@9xe)16x{xtfb&#My%+wtl10P?`7%`~R5%}8S)Je2P5uXv-KDH&dAuZZMRFdC?A8Ykl&Nfbi)FPSmzV3?!B^&U9R zdZnse_Gt5jiL$YdoR8}mTT8F07MpaR0 z3P-cO@Ezl}uj^a5{fqQv_6RJ+U0dtTG3jVdslXlp-wNIh#Ds+I)38C+aUe?IDY|!M z-W{vnDs3JUr<=Si#T7xy%_$FsJ51FG!c~N~+Yw0B)s9p8x$ojU#JEIW@2f+$)|VNA5=?*ie|)=O7@W(X%)&V|6`o0&o47|RC6 z(cUl*<_xl2ZxQ&VcaEq>owHbQC^ka&lbm`ZJHGOUN!7+mqKBVhkS791;GeIo6%ot= zIZ^ZTh%L#vzpI_tCp3A)L9(0@ML$^Jz0#_=9WhF98~eG$8RQa+@y-cg`mQ}pnyTw* zSF`n%VFvpf`^t?c25+bS>o!tVS`RzRt00XzQ_ml~NEu0ecY^>mXE*hOK$PjDJIw$E z*@8r%ph6GM3@K8Yo_E$u?L``oonV)Evc0xqz-Wern%8a#QBWdx;m0$cUP0w~F=e6f5P{e|Tn)e{iNCycwkZt=sWSR!HW=J2fCmrLMgpS6yxr`=(nlyPbt4qt{kov=Cw*p_t-XhFdB9!N>a8Qz2Uwm&b zTF^B&%KhAz`Wu((JHmgfGr;x#0Nhayk?DExznm0Ev=#fh9dGrDccK;GJB+q78iRC^ zHBTJ9;^;n)K@gtsHOC4Ny>As#2T|LT?-wB|b53{P7}Ove7mJwj=LTuR&^r;xm4Rs^ z+mKGss~N0#2!a~U-TweslOU9#-{%NZ+v*)0$&x`L1cCSO8IoXZPkt~Q6Qy_Ij~l>y zni132>uMKV_8DhRja#B!t0lZ~kUE(RJ#s@Be(NxTAL z;4ufY8+*Z^M;_YMn&TDW2!y&qXyd4CQet#TDjs$tuC5SHaR%<7{u~D933EAHJTvbP zM%y7rbArp>2GC2U+}*Qj|p7k#)x+2n!_NHqTd%Hh&f z5w?J9aNgW_o4QXzYKeF#w+N>E&hYbm6CakrYYieSmsQ4Z42993P;|mrUZ*ylA6Pb_ zsoH3LYb3#nEu=?DIUIR5(kCu%T^sknJv0M!{3Cua$y5ulL*vqE`OT3yOZ=emGCx+z z%JvaH@mE0fCE3EN&1;5# z<|E3(n8X8#5spr9uoH{f;rwGXl?eq%d*d{q71q;cJ6sPVoS@Rok3YwJb$@u@VE`2! zzKlkKmq~wMNmpSc}(4) z<)EitIK`OKRB=l78wQqx{08W8~o7wO{>4L6g2FV-F} zSw+jpNEt7@a$Nuo{a{KaJQ`7OP)^IU>jg3aR70`#tAsV^kSXx;OhgH(X|Eymi6{l; zAY%3$Ms)uG%lgBzpvKt$0L~avrs^L)@fnJEC;tEpy9C;BYXeb&ZC8sr{{ZGj01-7j zCT}Ngu5nN~n`1A*seCu_&O;L*SdA^4-Q(VEUdj_;#D08u$*5Y%eR2I|5KOcFF7T{T zfX8nW?=7|+p&5e3po*(rF(Miv^L!k(KvIo$cIC)nZO{Js!45_bWxVPBaTo|KZQmdJ zkVXesH_3$%JWAQ?{KXhZO_AnehMQJ{*1oe!g2?=tKS@w8{qvA+hT`A#f>pYXu`pGf zw()`9QpF&ej{gAn74$qkJz@9MJJNY@OF#t#z2Q&Y0D#x<{%{JKH%*tuhyMUD3qiKc zz4wg9#^L-8|PV47hAMYUG>BRacFE-?{fIBB(aCf{m#G|IJ`L_#g zO#(f-IMG@?ZC-j~3Ictw>lkgdy<7^Ck%pgU4MQ7iUj1gp768$)Y=5|-BFq)x6W4CGEyULLf5eJ|R{hqDE8XjFKT;%OQ{UidSObL$vK$<_PwgzP0B z2cw5FGIraiLc2Gr(`AbKD979NC#Ju13nyM{{XjF zgiV-v0Ew_G=M8_SoSH4$6Y+|kYqL(Prv2io5)>R5T5F4ds+wX^-U7#=NKwMlQ-cwR zg&7}rtks0&f_ehJt~+bB>3#rC!xR#!yV=@boB#uYin_cyFvi@$r#ADajG;BbBk^+l zO-Z5eP9h;8&BSRJ@7=f;RZeHB;D0B^2t6tZG_D%)`H6V|QEnR{L@JTEw`YLp8syl(z;PM$mq?-|50s+)Dq+6wGB zz)zx;PZp0kPVt}eMeK)ezY{(1Ltw7TfxkG4kg0TtZFR%h=LzmKL?}l}3k6ax`KJfL zPplSD6`&L1mw)pO6m2BifOyv!#8g90!M(Kc;}7uM(v$xHm{}Os%u@;`4mCaI5Xe#xyM_@&?dp2K)FSNmVv_^|txuecCNx%|wth00U8HEZ5nC5+ zz&ka*JYtltDtltb<@#_)uRw#XYrrl!D)Q+buYKTn3VST$*BB1$e;yn|id1GVd}ErB zl>WawWSn*Zy`gpSlw=AC#|wm9wN*|(H=GPqIuu_t^MJ4c+P}QrwyCGs!(oM3xA*(U zO~yvYT=U*6HUv0((;fi3uuN%FkV5nRRoA|g?jElIub21dOv(4%M z0J_O7^HljBvt~rcgQI!>0Njj02=n4y{bT}a)5+z;F-e{Y&m7~iz8l@dRdPnd5u;I- z(+J9_VqYF{jFA*=*E{~b;1Xj@?<-^;o?kb|c*j+-<#{j(3q;_=AG}FW-Ie%${{Vw0 z8mfF3oqyfp8iZ*#eoV4alsDSt6eY8=8sxxNW}pV|^QRpM#7cq7T3#{C*p%0N!G5^H zc!@x~UhzO*dD8YLCpWAia^M_?Y{ihGJeqsNSCZ@{?AEbCNlG^!x-(7!G1UjYF-#%V z4-3{UGrS&0I{L>_#sw2^`GABG^84muED`}Rf8z=*4wzh{LkZekpXNQ=E{i;I z>nhjG6Cb|y`@!5ax{=lHV&YVU5~qjPzj>ni1YTWw{o$@)-nf-VsFM_*0tl7w;}~U% z&5s-Z0B}JBk`+xg4)Ce5BDU}w67`!(8^Ldh^^X|y+C|Xj_>kZkkz9L9zZhh!X}!PZ z5Ld0^TJgjF;!TR0)P3>J8ayu@8xNv&kiHr!j=4N|$jd4e{{Y@`8xrv&vF8(3P=Zgx z=ORIH7tS#Yp$h!lkjhD^`85w%u<0H@^dF3CNJCX0Z}*EFtu`}^S5O-SFTs#94XQr7 z%D_}c)7_>eRFRVW>xBEr81J~YTuHmMF8ii%Qi4AS-myyGjuhCAdLLQZlxRQ! zQ_lF!+GixvoCO|^G`mfxE!CUCj_^l%B5R-CBo9j35rR$*aQ#7vwWjMd^XI(V$h1*- zQP$(`;NT89I^iduImL&84<@sba?XdZHL?5t^78&@=HD+97fuFhLqL}*V6&?C^)R&B zb`z6j_{vQv8*`$W44}I>JYi&{$ROX9Yx9nm`T>3e(7`(hivIex-^B;?H3UKgD54+L&k=Vf@Mj4 zmbwu=^OeaV782~_G15@pDp&2pbX$m^_He_T(OBrKsJ|X?nis&uIQN&zG$)LR1*2ob zy7F)s;-?Z#;g0SfBEFU^q*MDd`ODm;j?>WoSkVf_32(3>XYXWDg5%k&CIx_jsulFBM&E1!#b22K2G)9_t z#w9q%$9JUV=L$klgF*fO0GVu7c;1kgO-9-9D;`5aZj4N*RxrWOdgT5ku5X$`NAyTLtoQQ7@`;=?*v zQS89sVlR-qVkw0{`5Q-k?>CB?Dkp_D9p^kpT^CgU05YSbVJ)2Np0OPaknXzPAu{-h zg!82>J*D0VA2hG&;{&93HU9wSI_ef`d)KTqX=!;A5-i~HO))is3UC?>_UjAQl7U|j zd0oV*+wgqjac*|fV{B4t!Fv4Vpz%f5GU8+>6o_@l4oIkwLPz;=6%r>Z^uHKkX$^uu zQ~uUP+jXG}{$Lmaw8GMYo2%>e!V9g&6ECTg%F8yiJM$ zFW`rdIB}3Dxn~S({{V4yn=CiO{{TODrqWZ(I6u}mBBg71P5?ywa2*EE;N?o5f=Vlg7pe8ueIg|H@Y6vEn&e5-p25~{Q z3h3_;bG2tPeR#zpRHzNSKUk#%v?!hU)&SK&BD2Hr^5U5BqhRseo9E615$a^+=JT70 z(0imkn91gZguVQ09RP&$b{+lYxj+MxUi$uV^mvpIejPpWjE#c43iSD4UIm5EW9+!0 zYXBlTZI3xAz}v3APIrbdPcW!)#GhFRp%@5W2K=|< zB-{kDscU)C-xB2K4GW-}_h&db4koU?^r!1ALYEC^XL^s$06Y;9dQ`*`Y=|#{*Ce6` z0sjC!V@J4h9z%a4A3{vLi7k4iI}UO3OHu=_{PCOS!U+EW4gUa+0cUn~fER(v97E1* zC!5VJlAcxX^M?Xj_FQo1;ll=D4zxLV_Vvy>hIWJ~JY$Pu*bC!~8N|Aw zH;EO7LT%Z`;m_Y8w>F+KM{ae zQ)}VFP>}=#eU9>FgX2VJ``=>aFJq{v(}78=xu~D$hC13{lhg0zE_t1 ze;K-BK$RrsuD-D=j>eqX_`@v}fob{oOieIYcgfGzY;INU19#6F%bG}roAb^m<_JC4 zgX0vQ_N>$EzpPtulo;QMkWkljhi>(PKvXvMJma}lxD(>L{A1{Xni}vtzdYu%FgIv+ z`kLzyt4qS3D%>Uv-6DN)g=WeyzRx(Qs=B<*W3+A#)ut^y0dFUz!$P%C>_?k~N@#{p zulu~9Ury0I{{ZH4Z3va0?k-kAXO4@8)fGT`>w3UOwK+|jj}JM>cMU+j`S*+Irb+wD~9N^N6K^6UfL|8~A>(9F-7|4_x4efPt^K z?+qHOPw-$Dc#^w6c$Q!_Mmc#>g!VX2k}QM+!^QwC;syBwuJG^&ye08`d%;Z{AQ0`R zFRUqvKyU4FGNPQ5SJtQ1!d=h`Xyv0O?9NdNmGnq&#sUZdy}za@ux$#VKDoFo#!DN@ zr1Qg!YD=lMZV!Bh5{wiTX#ML3!|@TOe+)@mWnF=1!7{+X1zFv@ez5F->5CwB+?ZKvf;l9sh9`MdcQv3~Y@aJ6evyFBDi61i#bxd_9{CiP38n<6`SaCu__e;)iX^@h1x zEgn()Wcy1FL(IH-HG=epoiT64iTlK6=t-%Xu<7*94YgD%P7eGQVOK^o%XnT>cK~N) zfaSdJjS=S+;V}SKL+8lhvP2?_AtCPw#8?rbfSr$KZw(q0>3Mte(a^^G4yeB)$EPPt zl$P2i=%sKZ@qw|N0+`{VrS{{l_o#VaEAJId0$t7g@L!i7SZj95ZXQN=lo0~i3AcSX zFr^U~j^|DY9wGJDy<`aqN<{uT$rLandHyn;4On9xT62|M0GsOwwqG|9OUTpKMOE*e z;SON)h#)+lj1j?+MX$~_13k45?*PJ2V0fQd5@nINczoe4+8YPXA8uhGOc#(|Zy1TC z4%4hB0KN3!u_McS#sHlA#r6yLxc)+Y=L_1EjBHPxelXz$coF2BI>Um=Q~}NT#Rf!! zgX4eB3alRi2MPHwzK7#cLVTGnXbPY5VM2{To-easjEd{0n{+stMOh?j-GBEAydKYM z$Ie?`tg3H~V?)NT3Gw5+4hb7b^=?oUMR<&S+~82{)IBT~)miF}-J>lS?J@&v1fGtL3a->LJSjN?Ibulu}1A2b4p@r0b= zZ0&w=Cfxfknd=i(4lM6?76W8VZqdn>;*Gc8$(lt)U7zO=!d@9(Ul_qjO1slk!`}f( zr6$~E1ul^#FY6SINr@h5{{We#;FYPeOx-9Do=hi93h6p{m<44($ojm0_W=mX=~v+V zU^t{%6+6B9F;~}Sox)px zEyX{VSRXxrayN-05@mv~05{&SU)x4L#(18vY6mbjPLZBgtxJ%z7Y2| zdCfpHDN!SF=e*<)15p03r$U-{@ZOAQM(9{Pr7%^G3q#`ul>m=p@vM1`NIDMPJN1Q> zJ>^?<)yA5IFj1;I&eqYhZ?k`{uu*~thcEYyL}^=J^ChFBPm`+u0PYxx+|ge@{l)~B z5P#`_yHO)%3kc3wzUlqsHb9LZfr9n|SRO+2_l|{|rkQ&2>zs(y0@3mlH_9|on&SW@ zI#`ct6~?JVaZOS)>KtO*ic?7@tk(8#oZ9YgmTbQH#6p0rC`x;;=Qx&!>@~BWzl4{{Vk@i4bm2Pk)TAl=ADL{{Xwg_E(br zu?SKcGp*c=+M^{1VX7(whq~*$vFj1D=4ts%9RRfXzzF{Ux0DXGVEsEj428%p1e;5} z0OgZ8Vx?6Lv|ZKLUNA2vAs{Zz#$}8>nl}6KADn)$TDYt2u?KHiCt(N^!O%C`tYlkM zI`ZE~50Sy`G%qMSDbhvh%7v1MUbNTbXS_wUQVzZW%6`nL`*xOq7<(S0Ct{{Y1m*Qn z^@rfj4)owY$os=5F}~0&g4wdmj24wCcrV+RgBI?oDF?p9z;~7OymMcE$KjoQUTIJ& znt1ERHyTF7Y3Q?@r#_T|m2qEsCWLL~er_`(C~}(@XB^~w6(uIPCaxi#AqfYNb=GXI z*pph=xM@HxwZ{2qyXyi9G<07YwsxNw{A&t&U)jT&dzONc4+HOm+XEg30l^bm8cv+A ztdP(E2uaBD%Xv!7-wb(y`QZC#t|p8*gNA4GgciQvLu9l^Or!aU>U*5^MPy+gI}Dm9rk+4g{#Fj z;#F41u{i2@&8#RBX&xUGOqdv(O(PQgnRXW-oJhOY<({y|s>CAoXJ77buX|}U`omLF zN!inX?co8#veosiVtRprHvY3gZIDn7xyvcE*ck8_Wz7LP0{C6x!VUyyRQi}OAx*+ zK5^TH6==S2_rGB6_cf47s5;lj>nTOMm$z0s#t0f%a?`%>4a;MG&nW8_P^jdilKtUx z1i~VIoDvv3?HuYxuP2`J-8c^Xs6!!ij90+(K}h$aD> z?|l2lK!<3E^R95>q$$XapFLnDHiF8%VAF$u6x$ItfeR@151shLMFT3szt>Y2dNDCb z{jpl!qzXMRKX?cQ6?q=de_VOV5XC!b+4F?~QDA6&`sW4)j8>18@9O|yvxN`i7xW{` zu4VwmdK`&_Lq3oCcZ&2kQOAOK!9X3R&%I%G>*q(9b>|ADRD}CZrXn3Mi}?7?1#68y zIhesn;uPm;&YYXfNwpHA(=R45@JdEYa3pjr1S?%GECm-w5#&BP$*7Uo3xDll%HyWo zoF=&HZyflj3df&!Uw0{H+L*$yr~G49Ij39i04h14#n$oSr1+z)b?Xb`6J3v>hNr%< zh-Ae=Z+`y(_b3D=#T&6V`Nb;WZIeva-exh3Mvy3j#J+Je0F=XyIOonC4&=f0gr(BL zbQ7O7!xU{2{sywqGR*>bZvM=$J~LyLv=nH?Jg51HK{ZDQd~=tJP2mTeG%_0m3i`w; zA}Fo*ko6RVFRgQ%1GLj#UbBZ`vEF*i*n)!N=Oi(eTB**Zr`>&x|vYmp6}QBrLD z=7_OdAkb(#C79ftIz`%0K0M3DK&9+~u^NO?7fTkfw&pFygeM1{o_Pq2#*sERh?kcb zX}~wRHiL`uYRSzYsPGT}07I&40)q)*`3b@gksdRj5;f-yG2*r8Xo`X~btRW>nj0dm zau~lNamMmT{B6Dos_@s4<PLjY1HM9R(XP+I zhcb{TDu>-n*5Ne;N*;Kz!5Ps-Xo84wjQ~<(6S9{cf?Qnl)VcodyQV*RVL&PlQ&xrN zrelJh%np5r8{QRikUP#iOZ{XT4Fm(-=D6p=X9VBS_QBPz_VcGUg($&6Sb0saKJo`1 z7U(|;_0r9x+UliSt!Bf(L>_^g;rwGt0stFcyW8t1Sgo8JCmW8vVYb~QS8(zx-Z4Tp z@IF1{Cnc`Za=l=PcJtODTSBSzjRTGYLkrq-iwZi7Mxv1_Jk5S^K=dJbPBEjCQqlzvK|ahoQ+aqlx%vymcdA;-al9s4(L=#ue=bd)B{10G@qPX!t=IciV!?p zLt9q1Cx4tq#4nTm%F9|WiPC$<>tHk&Yp!%mxrrL4`S-?FQR#!<;lZQ@F=4|L#U-ob zonSC2k1ox3fQC#$zA>c&GzAX##<=sBZa@%!zEqe|K$`i#oEOf52?uXJ-mxtJ^QR2W zA1bK3bMcO@r3*A``Tb$6qi*u|e(^P05FU^3-a52d#f#tPJAj-Dc^*EosYnj(=ud|@ zAt4ri#@=&~>~Cc4&9b zudE_0GL83l^^gU?;z!wm9ubj1czoq3Xr%A_$~00OC%+iMlc^wq&aDP1^M5?JL~<#b zLZ0%}X{%Pb&OKyMcmSZB!QLtml94J?;|?TvuHIVfkFQw?2q9cUu&59|>le+$O#anm z&eHn+vE=mE-Bp@T1AXB1ls-k=@$rZNF;MaSVQ9}s68Ad##)?#6!0KYbNh20>HODz< zn}8+y{9)>*h<+P7`Na!S0avf*J9gRO{{S$gi4DnLk9gm1qP6bjA}Xce-bu&aZ_uFU zVE+K~41BFOV(K0~Fl8$;LA#GroYj!bv_eR@M`4L2Iwibki4q%wkV1!p{A8Uqzf+xI zV{>&XU-z7J5o*=hsjYR2(%VGvJfC>o2c9xbYp!+P38jn=$gG#+9n=_w{6FnxwHJpB z{{YkLBOsWyJRpCcddl4(ZwI^P}eDM}M9 z2YuVO@rXVk*++(MWHTu!Se^`i`8YjOm9QiQuVvBx7bYFR~ zW~|A?MEkqRsY9p;^3-M`%{r2Hl{b|^nqjlg6wL{K_m{1HOar!uyJQu8+l1_3*Lw-Z zJD0)29wzW}S7NC+GU_%J;ng@S-@GmIqCnx_3RihI2|GLlvK@YL5eOkqfbss|6;{AC z+O>C=AZThnVa_6gHbLbClg^nL?3)+GcuIV<;z7IeF~LVCpa zM5Vva>j(k^rCb26u>r}0T5FA`yl9-Q5SrX>9B~%j9X=xaE3ERDS)ge=oxfQXv>|+l z#J2*D@WM1#{{XyT7cd5gtxtRV#}=RqYLWGvC`|!%4j%)1%Gw~cYFg$~4VYLK$l*tDb)t&OZk>f@PR;}c4Ddc^{oRAUeg;Bx4sNtGrlmN7W z##z*--e+oMy8=D$dSve}b28 zVJ4l_F;$X)2c3#@@?@wqOX;)3-_}GB}1*`9A5U>)&@nTtudagg-W~!GGPi` zwdciOMkxy;r55+b1`w)`Mc4U&Ec637c)^o>czFB6jIUigS>8>H%qm|7Yr#S~0qE8| zcvgv{ozED^b#&Y79S=5hU-^TsiKzX9@rfwLo3HzUn@6=nz2rH?4#ol;2p4yWi{yZY zsCR!oVkd&u?Or_LX_X?WTq5x@u;t{{dDmGsC{BGo+Mlda9+5`9H~#A?1C-$D&GnQ+ zkW>f94Ee|>G%q|DeFb}FgEisTc}T9yB^n~}ryVJ~6PJEo{O=18m>D^G#u=!BYMtyO zTRzocR6bEGQHH`70?d;qNwj1d28nS5zA%NoCGb>la*orYQQf*80* zFW>JjA|W`f0`uM)Q;{MlsJyrLo2^n+FCKN*W&w!6ff8R3Zd?Y2kxY?Ps&}oAc|a$E zts--`oo0lIsz3JfD2^y%b=DjpBBiIe{{Y-z!mT|Tx)0sSwT3As_~7~RgPMFRKH0b9 z6CTiK4u({R7h_I zRl5hlA#OmD17`J%A+th=;AOWar`}l%g+~58;O?0QsmbBiQ1ffn8>gR%f!sASPXzDN z;}#%U%il5w{{S;)iU8f34_Y^lfZzbXz3&qMg%)ml<@v#yv|=B1`RfZ1L(yU2lgsM` z6460lnXC@{Q*u86&L`?bgrW&FC)b=};|x*IA;Y70tWQBi9>iXggBKyChvA0=hOMc0 zR=-#zIs|MuaC5^*56^!?hgmhk3&L)FmmKGP;Un{pos{W`;BF=kYh+E5aXHGmLX8VY z{?-*!@zr?ofs(htY4M^)%M_{M+(D0dxr4~zs7Q@??|yuR=kvxS?7x1WsB z+l(K``ZX~D;ZGj}cp=i-qaLq(xn9!IhPB`w=Ka!fUFnSZHxolCgml0jyi z%>tS<7#+??i$dK^KY1KfS()V7J{+11lVCg`qI@_3&6L7M?_88M$%k|R43m?_Y)i6m z#REh^lEQ3>e(^}O4H1BKcHSK3;P~Jh!Hd_7OhW)vbw-27XULP$%_Owh*olVdc%m-7L}Td&y3v&m@kvKN#=bogW^ufV$zy>W814Bgk#JsU$}7dD!Opm%Wed z_k%m+5##3}9&l~{0C1>M2vP={=TqZ0Cb}=XufNuAffKw2kCw|cYtBM3&1Y}^>klxK zjh_ADAiP=w-^NL?nz)`lV`2acvb>)CWJYBmCPCx%yqFE#s+>=;^N~WdJS)4XiGW3r zai~F4Of~h401+}Bepvv>>S!-MGitEco>9K86@^rIPPpKB&9V~z0D<+Cprqffb%L5h z3iIn4rA!dtj0}jnP)G5+oPZx;>pci)XnB1YGy_OcNzU44_9C15JbdRt3uvEsA`@Y0 z>(4oGfMT0j^M<6OkqhYaoCcjdzkac4X$rvi&pBqR(f|)W-Da{pyOlcY{xAqhK?;5xTo_=L6rSOMY6#E~dMC~Ti0qOB;vL~- zfWDa$M415gwQ-($(|7Xm#-9d3t(nB>%~csP6?hV+_# z@x%(gVe`OaA%-G$1k>@6x&aep-y3GKNoyc>>AW4`A-;iYuLev6Ee-zw+JQM;|=+$AWS?4ms!d-aOli;uY%d&U%}{x>$A6qk zIwF=nOVq$cR#vEf`16fR1_>vq{{R>TNZK?w4L`hE!qYVS{{V4FA_A>me(}UaP~)*3 z;KaHbK7e1NBiB(NK3g8w7``@;I2Hy`)Pq5Mytlw%Lcs+0KJvm`h!o=N!IX$Tz}I`q zBn!kS_xkGrq`@FID!b_Mk!HydcuTES+l!gN9D)Yv&lvP2SljC0U_eY4e_J*AX3m7D zPr68n^q2_8foLoD`RmFnn_Sccy#g!VZl<|DwZ1!`irh} zMqY}uuf8<$d*cY4Q@7p|ZYPs`CSLNR<|Gsl#83x@85dO}Xio_*-asTq-UEHX`^(4N zg}SFl`HP-)Q^4h4`gpi`ao_>SJf1}BAl20&%bQ;`c`%+4V54T(4;AtkHPa)*A_yHL z5=S|Ag0zV#uFwcrHYK5B}i-@dNV*n~gCcUD4I|*VadHMOG^Hxz-Ag77`wzkctEy9x>M2 z)@rpX?W~fX^E3r#yaB9@VE7ux({Tbmcg6&b@8z7si!WCbXaix0?aKsSW@^lSV_{VI z)%5N?2@^udItuA&AT#W(P4& zK1|+(v`zfr2+tj+Ez6HBKL$lf4{p9pHCiRv8-cnz)l+8g#u``w5|fP^{{WoXLgN7B z=**ql<(>P-TD%6kS>48^b%s7%a680&Z7XQ&i9O@wo_gOreB;azQXT`#{oz)hil+rC zYp}*g=$xe)SS2zoT|_oeTg4N=mn}o0v|lF=`sK2bvWJUi`F9+MgMbeS!03<&gIdl! z!Apj*z>@r&-Dd_mv440BzbFyhh9ItNtw1fL0g1ZAZYfR;ymQAH8*P9PVHxw1so8YN zfQ^(ZL!x3&Aa6*|Cb|5X2B!dLTk>PB5JB0y&va8+yfQ=nlud zcrp;jv!h?QhvyXl+`UBgiciJSG}(0i@rp^PHykav7*{0TH#m^AJ6`f%mYOYaf8H^; zV@=uf#~G&_3I|1H_%e_vs-uJBoKhhzJ7m5ToOt9t;)J5Dr%U6k;9i%bmf7)-E(?Q( zx^Rq{cqkW`;N>Ay7ocdy0Mhatt#lE)#_y~LRdOg|Qv}BNyAxl6Zy7^CHhUSYD5(vF z9qqr)CY4&Q@4^uKa8|OSw{zjvF3wR>qt)|{RJn+Mmb)$}-_8JXgQq6XS!6wMzsrh@ zAvCO$gPX<~uqMl;gafhqxQBzVLCW>|%Y`n53O;;2;k&8OP4&jHO18R{I14`>FxB^P zBn2Y**Cr5D4g;WP-OJC#i3z62Fi#*mr=xhH&5M6IvKONDDT|Y9AOn{2VigWl)9j)? z%pGB?j3Wx)vxGdSnDbt6Nf(LH#?s&&l+vw-KMnvF!AbPAt~?APC_PqSQ2??hB-;M~ zrV}tgq)SlQ_q}AeZDdyxq8!zH*M=b(+y$>d@#(&?Q3YKA`!jG_MV@KIPtOKxx z`c5ydj5rpe0TSPTc-c!!qOu`+dCleRw|q4J0CIv+Su!17X@YRbBzp$$`N&A{8y?O2 zv+D(`@&E&BOdBEG<=S2H(*0n`YOI4!e$I1^VHBmsgyRT@vTHU|UiXaa>SC_844``7 zKCtasBEb5=1vZmX_S}Hc%$#DCpPUj29`KuMcDVM67`*S}m+J;<1PQLs{BvheXx|_H znC3{*SGmIBj@cafcr}B*u!y9&O4#T-T_WuC65gif=Nj3822u`+WdwRqz5Li|oPd+f@*MKX{@;qY!);iGycT8?* z!J8EDO@9FKh-@B#0K8bCla&VmM~hG7{{S&UPa%&sjXzllaYqfP+8s>ew#sJlf+#0| zV8%)UhoD>hT%Qn13#C!;K5--qTvUoRfK==-4xo)lR0O2Z8=A!B^vb>ZXSVa2I%_<} zPOGeKgR3~=)1=95CkL6f`EaQsy$v0B09=zasUI9BjX6WM!Y5xFzvE+oO zN4uJ$AP9v$X4Tdeku}DqE$s;Y$jA!a17B|s*Lg^T0k^rWxIqY@MH%7Ez)l9Y%J{{7 zfnu$7zA%(YG-K)KHpFarHHQwN6I_r?yF={bUEofG&-lB`)ZG*C_-EcDDs;aNY14*z z2Pet(j~I5}1G+Jb*9FXRI`cL|U9Dj3&;75KnAptAf`x(RWgdi5kJ1>DZck=N%TPa`JKT_r^CU zMgAI5XWm4DZxhD!zAW{f@mhs&%`xORaWyppqux{-Qq*scctw^et4Zv8#_j>KBeJs_ zgz*5#KsUc7r!@QgW{bOl0or+feBwE1HPF+maOw9H#_5ZH?l7j1opkK?!{-N2GE#AT z6UU4|-QXe}P=;Nmpszkn+l8Y;f&T!T&Q2VhV{9xOIpw_PNt=M_hnt}7&BR$Zxofn7 ztLFN_AP2smLbhab+C?_usSHEAfg1Eag|MNn*!N>@s$qj zSowysC-VlFb>j*;c!XP8=K9_u>&Z_k_AtU>S=v=}z#k?En658_z;2#?u%{_@O*dx? zq3jW-SY*6-NX2lmJfp*$a0eUvI(=gjZ3efnyZr0p0S#tgYg-@W9FQV$4z64j-&is1 z)E8qrcjQR5VSFv27)yhmb#E?>fv8 zWuS=7Y0aXV2k?Zx!;QH!W{nhq0GJ_-&CNoJNpy#ksIBj3;66bI<#(5zX$HP+OhzHb z>Am_+b7jy_w*LSa)CSl@K78vW#Z&`Ean2BY3r{=J{b<8d1RAuv7t1W9TPdS(^NT_i z6z}T^3U@&y>)vlC{{W50#!DcGPC=LQIJP0!Tmj4Q8ZZ_iZ-u}pv&OL$0{Xy3Er?h5 ziGiBUY1J#j^^VDOsjl^`U_nYD=gw3#hi%6b)D=&7#vnND{{Yh-H4p}mFzf8dkSZ>y zvEi8IEh$0C>%E)D5&r;j4zlL7KX>ZSv_2oR&sj<<;l zWU78JAO|65jIg5bU><3#C`1p*w^$-tGhi(9F@E5jLwuJWqT0?+NId$EiaEyPCiff(|ruyN0E(2nw)6 zjNThix6wNJ?-6uR{z{|gd2MQJ)97bjFsay@GrNDhTc<_kjfJy-T_dM_{{WcSZyFh@ z<+zd~bb4^6d9E z2i9GXLd%sFQz)?Q+};Z`T^n#3ThneJK|F^2<54K?=qOS!Ei#=63v{DF;1$bN&yMvoBQJ#lQgIw{$(@z zfN%2W8b)vhKRp;Wcj>!)V6iHlADPa~s%#-CJIByEkv*P%@fO3qug@m(CZQX1Vu`f-~ki!TtZFJW@<@hjNIW4z6=_2~cp~=;Q0H%34%&xpbE=^gEIj zP~z-zWMm8p=}lu=rU+g!g>aAu;9`TQHs)>m#hxJK4+oRZ7PyuNpgn%^(1zp|1K@Aa z#WvR z7ks}R;I$KW>3w;{Xo7*ggG-FZ3Z)<5^MvW>2VXLtwUR6rAznueeK{U@JPr8u+2=Q3 z2q6Z#&y#^i2{Nx5X*#=2J&3Ry8}@DV4$dB?JZV6$0T4X_7>u?Mb{pRhGda+SBOsS- zH6C2}TO^$&ug)#47Aw#)249V8z7A90n}P&jCus2eV?|@Me1Gl*YYC-2de#7ab_PeM&lzTHP`Z ztg<$(ZR}0s0o_NFfpxE(NG8hxUq5)v-qF#FiHeu-dBF%k9^R|Ves?jrbSZ~=0pA^B z2|yctd}0>!rPUrWNyRE~pNS% z90AfG)8k(l^JzF;xSyp5zMe;g7I8t#F*{(OnYKLnq zy}`%cUJ{L3T_be!H-|DQCfknpY!k>ly(IUKP%>yoWI5g0l(hvN{AgWIS0H?f z8g@-;dA_p98EZP6?|*yi0SW-~&|W9|jSohG$y)DNp(?x0dKax->UhG`poxjQp=Pj0 z_B25#q?&Jd_*A?d_1wUM6O|@HhB*Q8-Z@s#km*%j{xF0$Q3Lbh9x#E493!D8BFXmV zs;uoT0_*@5294ulK*$ZP{42|lrqikUoy&*_tnX~u;vRn)I4Ki%;N|vZ&uvhrhH!U} z0O)ldb)xSMwE$9AoxF!84o1o}Z^hPD1X@OnsP93mJ^I1|g!0T>WbSmvq%LR_!|c~al4YbJ*>2RPTPhd96@4Y8+X*2TD87g~@@8tQqtzR3;MI)8XR zBzGaasRd0RAmp=Hkq|)RS9qC7XdPjN(Fg|j4lj+z9)qz)?%j=cto4tagT00#U5_b; zNk5=Vc&%(oPu^g5stZ?Pqp61qu7U>w`5a0+5I7CB_kdzVXXaiw%A~ztkVjdlyCGVh z_w|j&3O#MK>j*O%R;L=C@$1@<~=?orUrG=-|zDo~T%uDDoye&4*Uk}!B zDxz;)@s+XVFs%(w=K^G8m*F6o?7_t7`E&$zDbYZ z2tl>{{&086bcY^{Q2hXE`t#l(K-3QX>nge}2)^$d#?Ay&m(QGZ9BIk;0N>{h1g$n> z+oM=g@sYQ^>z|Akh-4UrPB;#E!T?Z1w_UhoQA1%h8vExTJld_E)&4N(a$C(iP5Q|{ z=z%shi}=MG^7_^1&2Qp9*!CHk^_^8M6Gjv6N^E{{Yc5oeRRD z$tJES1l4Ny!hBpNYMrxk5!g;k>zj$;=!J3K zfq)&>=D(Yj1c}X)u;UX*noIE`nqnGB7hM@_&V+lOeE2?b(4-J?sJy&>eB|{(nwOt} zbzcr_-^64#BgsI6|0?5P1PyS(q zGfBDi{=9wU**C+NN_cqUZyJ#U0Ii)PuK`D#S87vGJe^)D*t6xUvC@A>=`ecUvcds? zh5)dxxyV&88^MlB`RfH*trIwJwCUbcLn(Ck*d0lZhzY7U87&SyWq4MXq=e-BIizOy z^j&Rl@ro;8jWnS1f{h&P6W1@s6f)ZgUa-;v1x49Lartv^Lg+5EJeQn`kd4)RGmPWi zu@xL~{o!RgjV(s}ilMWWH)7-R!TRej@|+q!q~BZ|T*|(2yW3yh?mzuvO)}Yk3Nll1&b8kBkcAQXm6e2KgSc1Q^<5i@W~- zx!uT@BDPL<&p2RNwyqjr&H3XPJVa9ZVDtPyEBzn5B$9%#esI$SSb&~RvR2wc2YZY8 zTxqrF5_Hi6?*kQJ=!8^5oY~Pq=Yp%D>Dj)q=+Ih);cUXo-@=tok;FB69^;W-I4r|^ z@`fPVl}-75;1|fk4Y$;I)&*)Tw!1Ps%};pel{TdE;{5f5Qx{0~0iodDUWDa&6bKIc zxg?QB8&%QQ-XJgqvFZ6R?R=tee!^@r6JOhgAOn zy=RL07&5MaykJ)}c0Sh}DE4=Eye~i)4D4NUH_gXanc>u(k&^6sOkxBm`Ng7Zu6N@Y zn53DZb|`1Ll%N4vz+PnWits_8<+$|aO}PM{NI&nafLPEE__;m|f{b0aY zK&#&a?UX6Kz}6g2Ah z5%ieAv%0tDJmFxrsSp0*5*#c35eLgDTsKhoZh6y`K+rPjqEqnSStr(?MTlrP;Ps63 zSV&(;jk}g>j`125gtls9EKw;P?@u^5s{619hn($n{2~tzQz%+GSYNi_&cW!s1~~MW zBh;=wGqeG{$r-OW3XKF6H_lQQX_4YL!OQWB-N36Ervc0AXDj~z=-pL&nDN@gQDI+_ z`Nu(G4E9Z@E(wx5biRN4kcp*Mojy9wNYD*BaGDhy)5e@gdeVP~p{H31dxdr?0Wa49eo&CxcZlgw_Ws z_IEn3e9lK1mD&QvB-cALM<3%abDqBN6cnp=G~*sWVHvNK%hU`;=)7+j-wCRDQwyTR z)2DOKd2J^~udVzz-(XF+yi*IgXckhJPL2I$goNcA)#W|!6mB`8Cp`C!smxT?_#f*w zF)HI{;}VvMo*h%hG=cdP!)F^X?GRl8<9vw=M;&|9^Molkd*$mDlUfxv(EHXp)E9D3 zKb$wHF>X9P{AIPJ$7_}(Ft#U~qVGi-%q?E zA#KvYZUksWHX<09Q1N0bnxl8Jn8<@jF<2~m7;2eTSQq73723WeH`;Pmb1 zBCFTPFRgM zag{(N!F6Bm5?DeshjW~rsv%+if9^R#c395m7v67KPMYtK)@-mJn)&==rzM;_d0zX( zaJYLnuYKGinJva|usCmn=QMz)o_IAgM;(D0opp>GY6DI*zd2nujIHVP&3Dr;fIy%h zAQaZu12skC76purVefdrrhya!!x5vilbmYk;!1UA)-|9^ZL%^Tp>Kf-v%u@IT7f|H%EmN;{~u%`CoD@M2(ILIK4qH{7Xl^ZoRjSwVbDS$;-J-N+zY3uC<2*LRq6WE~tW)E?r>^MMNG}oZthm1A*`FAEov~ zj7_y{?FcN`8_~62>c(M8`^~$F%tx;9d7DPbBdjf zQdVn@cH*QFDSm&PfNLRXL-gff`Yk6uub-?UE!9TZCpfk`i49IY<%fp`5%)f_$C0VZ zb$sT&Lb@#f0E~zQzzwhc`N67W7k)SQipjD9KWsb)4uf8QyrQ^!aDG3WNz`Gt-Jh(Z zylj?p+nEJ z2;tHK+V6wg5Q-;UX}$}Z+x*O@6g}B~?HW5y6SH7ae7z3=0|=0~iQV@4-#C>nbJLCJ zJ{-?@=5Ce)@Oi?#HVuALXUO1&h-|)2-I|$h1%vaiFUg0FY%{MYe_13P^k@_bOsKcQ z#!ggz%%nFaw)zjsIG9nxC$*9LVwb2#UZn&)+};gEN8$9B37b2&n4P|hiX;2k_9WIe z1%i@&sT7`WSB#st(ci`o<||nCiN}`AAF7CB$SMw0d#p-L08G3%tDSZVl5J!T3*pGk2e39O~fkt-%ZSe>p}d0wMgde!mj% zPZPkdf6#-Vnl8Psyztri1q# zI2EcWxFTmU^r)uB0dUh!>;50|rNj1_Fi zwK!2Um-)^XOQA2EGy>|T{+u6rq3h?4^G&2lLOZAL26ifx9!#H%=hPC?gnk|4T43Q9 zaX31dhxVe;FC*(C0T9js{NWM`JJ_s&AuI-cU|@0&4f`MN43xTxH^AY0oRdaf51nE* zU^si?TRq{6#deOr`eu?MRv1T|>8`Q*>06<9Mm|TgngF$>Q6Q8M5D+AGVw|I~vMA{b zhb6%_ge2;86%fO0v7vhdCX9TZarmsXa`4i%cHT#qW!VGrK)NmPdtiN3VW+0y5fi5< zFyRLiuvNB3tywr}sCQvqn(kmr1Z8vDUaOSZ$4<`uVd1bMO}uAX?FWJNjGatT96iyb zl@sHP0g9mj3I>fQ&A^lm*j`QB%^z5x2-1ghSK_#Yxe3%#SP#t0LFz$wzkI7aWa}{7 za@@04R!6NCJf@UrKviz3cycLX=_dg|foW0J4QNGOOJ<3&u_JNet(tg{rSYM5EXr;M z{{U@(8Ya-z{c7fHY@@NKO=CK^3>Qer1V(5f6i1tg<38Ov!XifKoIgJB8?i^C5`)O^ zM$D4*@@U*0l-Q=HLy0E1kJK{b6hN0v~v1mAl^F zOaZhkXu&9}M0hcA-7AL94>?nfs*I(|5Ge`TsQlq$V!#-R{h1X&PTIl} zsZliX+<5(ADFM=tCX{|M&_WwE&I~1_RP55ubAU96TfRJGSqe0}c=-PSGsjaxjgI@b z>jQS2J;#h4A(VNq=)_eqi?9u!MaNS>Afj)toGE*i@UV{i`o#lL3RG=(`@?e_5U-D% zD|iG?FIv_Y6mzXFde^Lw#Fcg)91%2z4qkEB5ZPPiJ@4xXs}n#Ty{plPg>)E!-umkW z-K~z3WMhALpg06_FE_sNWVS#lf7}uwCN>8F&hv2~*mNDPZWaTlpGM!#86-IrK7D?0 zkc^~x4=z5Bh!t1Xu4^>Z^*n{;h8grbTXF}AXue!}xoQqV8&~Lecq8Tpw*VnBiwfwE zL!2FgBJuaFW9o60zb{YJaTWw%iRA~`$i5~m0%)U7bnDg|hGL{oX}_%F*@<9(IUD^z zLFeaQFufi}*L@lBfWg&hc9uu!GeBb;AF%|>_b}F92#@5%iYa9W-Jf?KO7ct9c~5-c zV;a#{1{^wez+vJkC&n5aSnJqX)(%Plth_p3##E6eA>dxxGexov9pIun;}9tnBrk^J zTJiu+`O$)aq53`HtwF%w2n+mYN7i#kT!Hw+)rM&WuATLY7#XB7HZArNd%)_~0RI4k zelpYwf=Jymv1P~^`>$Z%+p~?BXQB(Uvzq)D#}5w{JbY+*?~DY%Q@nUwZIe`A9Wn6p zjvpqm_~ib2&MqnBi{C;$V%(=$rv|S-?m5lNVe4u! zt^CBoIRw#OCdZH7aRUv|bx@0*XbL;xN8W5>hhxEJ{N})diWi+RB@;nXZ-#!Dupt3c zUM0^=xLP`){NgR+lREK7`oJM5?0JYDbdRibUB{8;CtaKCfU@Ayx{#h1-x|fFP=qta z`#kt}=Lq1i{{T4nZ^ig0F5ncFh%khrkYTHN8>B6`XY>zVchxrzhVW*v< z;7~V05d!}Jc+Cc=6K99pfQpI;KORYqR|6}m_rCHVh-~(6gE$=ySkt_1O>DMnhZ$0Q zZ4>3o04Y9x58f|ujA=a{FcVu%9iYbOpxeA3tfSc@sYG=Xj2Ba{IW%#q**^D zBW-M)V6>!74wXVqr8?;1>&Ul5y@2aJ8p3QRUV*5CsubR_`=ZHa>_JU`IRNBcgs>VK zixr8R`q)F}@}1je+JkCIP}~CzTV%TlOx0-#TPt~!vfzZ!Q?nyz6I~o%bg87_H@6gU zh^xHQeZ-ngbAS?PfbS@vWAKl7M6n{+bw*`{4|PU$ZYABk#Vbx;^f2i_92RJad~BHG z&QHl~LqZ1TJ^>-?pD3^n+!RqMe1Zwc4JlGM^m(aWFPOyO5vP@sqNF zE5Bp?Ii))C8~iyx8LxrCQ9fE>Y^V{f->1edD9iPOH*yv9`Ng!1xkr3`X?!9b&NqNq z?I4b!oeyH2G(7k>2AcMe)Go4OAOg#(31AAMBJL^xgxn7uj0t;um!(? z);wTEsJkhj`W64JDI|j2{s?CE_;mgL;Vw02I^DCbM}HXqbhaH&NI? z{x;4Pf`n44_*BIgqk8Wd26{_B)**qy3n*a_7FWB?c>e$}k`rK39m#dh3wZWUj)I3c zqlBWihaG5eJtA;fv+t~Eir|oM^Uvc8c#FvflU4N7_F%2EIs!D)x6N{#dg zh3mZ3M#Gzaa_>&z$A`QLA~aPVkGJmtg}~Z_$FH9lf{GeN)&bHrqk{>Nl>xp}_{6Co zgCO_bO@l!IP;Q*hS+Xr2FDM87ynHpgk0xfEEp}vRWdWV&r!FPg3{sYpkQ{}b@3&Ca z60)?7J#Y1o(`kzNe0*g{4WpfP>x|WS+B@WRyd>x#XzGWaelTg^MDRllcML zPax|1zO&o~!`{94$xsCw6N=)P@D_Ph7~>D58_FpE06C!@q!~Xi);9$O>UP!K^!1Eo zZXSL=ryc<|f6$+dPSFJyh!%T>IVPr6<v<4%GG_J$7n zr|+Cx$|2GXaOk6Hzd1Zhzy{~fr+7D(kd@xM_`+?c0-6@fnt^}~(_g~|CZ{9=?=_`0 zj1~x(I$YBfirRiLfHk}`%pc1iR0}2W7@S}tXW3t70iFbG`rmubb8#8MJv%O4ED+ez zAaUmjwu)Q?442ioHzf_Qn)5m^7VU`sEN_3jSU;-qcVDmGKG!QPls^D_a``h`zYeM% z1oeuDI8=1r&!ZIR&}T-{g6f^b2XTTZ2p(E^Ipds~h#1HlA3Edv%P_N8>@RLH6qy5_ z3V1!>7=RWMbzCpwCQ;=<{{TPSQV=ePoJKN0ZjlT_&HXtw;xz}Khx))^yebi(cXuWQ zx%CI%7%`rVzLP2>1*HS=&L|Wu5mmn58p{?9&|cwrav{c10XdI&iFPXLIRNJzJW&f4 z?7$34a!ZV17>t`YJm*0aHa5$7NPS}nEn~locO9aY;_;9-N)7BpIzRIjWlkIziVLtwj235Sfes0WmDU;>UEt9Feq z)-u)WSif?(Jei46M#6z0t21&X`3xc3aZ4NSL?dHFTN?^w&ws>@ks7F`&W7efM9&1# z!FQbjBH#`+^?-^%+k~tuJ)1qetltTj_SBJ3yhelR9eSU}hL|BoHu!?%48M2Lz2Ki8z8e;oc8< zNFG=bSAo`~$R!Mv!o>!K*l5-}wO%j&;xMmjV#p}&yZOPx-=-jW^D!VWB7%N#M08I` z{Nvmu-VLO9(@%%SQmr_=Jh<#O9z0kKooFOrl5hIe8I%rT@|D{Wd{lA zD;N%iI8^y@UKP?Yi{QJztm|a6qxNN0qHrBQK5|iBc$lKL-3AJTK5(}N612fGYKsx~ zm!sKeCycD4bq^BiVgpNbM8)GUoC)g^aL~FFbo&1QFbRZ%$^*Qc6uOl_t^h~mbCZP8 zO1~Aadhv{Dj>uA{Gg=+sN1!O-=SpJV7Ydi8ykx>aV>`g;I%Fq5y=LT4ji7S-@%Nk< z_3+bx*~T|+QAW{g^^b5;Y8P$i#tNLcSEyS(Vj&kVlQQxP;px_H=yV zibQmF*qt9(7Q@2b8sX5)7}?#%W3nB(b+W+z2?QyiKDHUGfY{0i^KBHYZ3%CLrJ>NJ#Z?WhF5mv z*3MTnj%?pS`72o$xMT$Oe|Qi^Q*e555GT-=qzL`vGQ=bncxm{Ui2=EXPO&2a1zu7= z#x}&)?E``Hihgh%oua9NOVV_CB>cGb`KWv+hX$3+uQBB=O-D{hbI(|dX(U&+*>{Vr z5Mgb*S6?4kyziA)9PjgS73vbvn^OnzfdtW0W_{tJ4;-9p-UB7}{Hv6y>2*(+*I2DJ zDIL0<%nR+&ci|b(!3e!3BTu9fEIV~!;4?SXR-)S~nG9!q?ZoS}VL1|8uO%>8y3ucIB5^G${eNL*O zJJB&=14bxmfbY|q&eNgV#)Y`Gw z0XoKgN)!i`t3G#^rQQJh(r*MDF%2EA`OfkE(Wj5R8&a108}pBLiYSqu`Mb-jdso3x z@%YQ4Enx#YXkqkJ?8jR*fQEtsyusG5+TsqIX^VM2Q_c?Q1bocpyzApHk_U4(sMj;& zcvtak351?QD@On-slTk(5HKlU&lqgjFJQzN6S0_{Lb_ool@O#SSXY5;<)+{vj7E#g zz-SC{RjQ3W;Tpb$8lm!C;n^bI5|hd@Kxdp`&@Ioz(C$ZDIdu|N2ZutwHn@b^j2{li zr`sx`Yh>X=)1x8g)3YIpHcD-nc}#(FlQhRihc#4->@-WWkalUNarw^rIbX&;vxfvl89p#nNKPBGd6+%;CO zm0!062bF23g&T2Z1r!5**CD#P23O+p0Pt18zBQK)^N7g#=NzGsL=AmzK#&YXenxoQ`oZ!Ot~c?<01v1{KAd@@RH{4ZgfIcg<1J7}*El0+&lsy9 z_`{XYCPV~#W`!*8SW@a~$QIgb8Q9xB;kMui20IYLYZNr47Q%s`I>p{BQ$@%=8Aro` z7z0#1jtGNyWE+d@-`)}=GmA9V52PA#iWYWa#(BW4PKye^)+rg)JSVI+9U37qvk3_i z9!H&jII&uq9c8FkKs-HsdBS?hFC0S=N0muI^K*Hj&e-Se>j4QGl;%9~h#I!~b&+GZ zD0CYFtQ#=S$-cdN!pXV{mf!wj=?&54j8baW;ii!sBsuxkcEqa$BR+Y{7y_Z@&Ed<4 z$PG2GS2pING7J9z%t{L&B6IJYhEqp^fzJJNhKEy;p}GG6ae$=Y3YE{ zEiIgH^M4picJ{!0eB@P#f>7`Vu1o5i!{PDc0|W(=Nk_+dK%xay4XgKxf+Qu=Zu;Ia z?&LDA)~@iP5h$Yj?ZwKR=d20{Rchg2rsxatV0*<52uN$p<9_fKT@?qOVELI~$2pgz zU~6jLjB0qk`A&};lf}R*M1W&#?Om?$U&jD`++>bm8NavoIS_Y8Y_8k{>V%Il?TC{{SGb)?Zi}!YzH459G~q8cFVK*7^M7(Lf}Vlh(O_ z@7@hY;ZlQP+WeW+{2#vGKN(VEH7flkNR?3vKLo&ILv$6q6Im#T6sYnr{pIcmk>0Om z!xso)r1G6$^kA!J#Nj?n2ro>L;&48(@Qx1rE(zWGMEch8nLx)^*|t9oq<_M8zZuHH zm4N>E!Wc;aJa=#iB5F?OLHRJ!Z1Wf1G&BN%9;T*LaR-umy_ph-QgLVsLY82Yxvopl=*u5JFY})#Q-yMd6-wrlvn0mVSk$jxL2Ap&S|w~wrmBGZ7r)%BEVWQt+S z!FC3YuzdwcMLA0}Fz&z|WmMN&>PS+rrfaQ2n?mcyoI|knfpkaL{MIx(q|*2}`N;2d z6ejgEh>NgkH~#?4eC;wPjy8SgIE_O6M^7{61#sMIBMCu^}*Y}gZhXQnj zy7kTQxed21Vm7ZSVA$|54MY$@?v$b<>CRd1C8=uds1G#j1IXeiJk6Y|ONUi&c{cw5 z@iJ76MJ^C5PNrx8GwaywMHH*ZXl=%0H%n(^0v5t z-n0m(r)N2|9V&lAjDUjdH;|>BJ?4QojTe?lEd_3!Yp>32?#A0afpH*{M+u5g|3o{{WbjTM*In z#yAfk7WK|T7ou=KUE^Vh3{@NWr`|9DX(>IuZvrMBBmUPDAao6h(c$~}!4Mr#leP}i zQzn^L)2jnCZ$qyctUeAtXaaro?*o9u0GD(>W-=*?HN!EC=3(0j<;n;_r%8eaJ^IC( z3nWeC!o$Pk6sh>aq69lGSx|=%@$rF`AP0LH=o8*^dao9k`P4pjh9(;9!=yDD-^O&6 z7QI97_IbjhA!=^jxBlY>fJ0o0`1{5KA!=KQK_(bFbwv)Tk_iWJTfJ`<)QBpVo;-&KyFn2&#peRkLF%DIy369y;MutO<0l!M1(qAoLzV`yP(n=jJ~MMx3E|5z z=R_FysstL=Z;Q0!YM7`q3hJbii9sfAJ=%>MJXz+e~;UW zk}U$eJKqK4DInSnUz|lDGQ2O0uKr5c<*AJz!{fo`m{^}H%B z&>`KJa@|YHNquQHJmT+a`dy2WczU=?L}*x~#|V_g*eN=dh#?1)Oh7v8K%OpeMH_*4 zfvHEnu&!(*6kV&O++PEYf2dVZ^fjjFDxoypVC=0n)41&A%8&*k##z5O!n1()W68}P zoK#TkPiFpY89{w<$V#E{-R-0DncQx)A9mD}>o?H$b8$^0M z{t6DhG9O`fH1CH@Sud(%-v6P#tuU@9T;6$$}zk`XfA+z5&A?gBeMIC7Ofv~?P zGoG@C37k|V+wJW^HVK@3eYOq_&n#2Nq~1O_=#8WxWY!<&u4Bv}G$cX6yjt*4qFi|E4&##ld9{fw?=2Ue^%_;ET)R z<&p3t;%11plh=n9L4HU70C>b2iwMDgj1wwV4wI(|`tUiK#DIY`zgQepCZ*G!@Q7h{ zqpSxUiF&!jbNI!%&Ymp$$UuoBcb>fF*d!~m>j7=LY(8hL{{ZGt)Nu_DuQ$)EDZvAg zcdy12FakiY8@*tQeqiq+n3;BS74^lVVrbed5N+0biOA;}*%DHJzF;HbTuz<#hd+j7+yzTtB=K zqZ%uHqYKB08hoOCQwS3Up}lNeJL{^i;r!u53ewSFEe;qa@wP(*0f^8`sARAKa}v>@ z>J{c3V6K7&nL-TKjURbEq(NUFj_X_iYf(=Xa(Qs<{1@|L;&Ujkj0ARVq}~vpob{VZ z5E<$m@%1Q-}pTjrWwr;dJo?FST*xvf2a2qI~6%Lu)wI_TDgW zHAy0@yyOnh9G*8WxG`cF4Gjd}=4P1G7RRuE>5Nb^)Jk51tCpWoB{m`tX*}T$V`Kv% zL&awD`o*Pa&^!LMg&Cz?ybkrRcq3^`Bx^ZbC*PcOCoeC6j7At5RzPA;`TNV~4lx@X z=>a>1#c%@f_T%j1qT=bSA!&ER}u1Oa#wKRFd+ z^Kh^z2ydL!0Yly{HQy%1hc?j?J>ZvGylVm+y4nj3Z{VNXK5P@S!{_H&B*cP%_?g@a zG6Ga~$M+FK%@M9ku;{%{#!9tT+PrV6#m`6y`IX*BGkq4CXE+G{4FdiI` zzz9lg^ZURl39u`%_6>Yuzr0nwJ+3bHRGZqH$p#1AYbXLbPQJ0^sQ{;h@rl92S6Cq$ za$;%TDuWLd0`rR4R#@<{fGul+ z&ilQg;yit0?8m0PzH%fa;Ar~I9Z;$q{0LE+eT=Na4O9_3IK$i%d#V~VRcbUt4dHaKA8%r+1En}`{0IN+WgzC&RD9^@8YOT7%juZxs_u0Me-9iRs!4q!1vN8VhuH!0Z)#MNS9C-dt;%ESqRYuNB1% z>SrSe+G+B0jU3xoYWu)aAe8w)@7hb=DarZG+pp&fcLU(fgV*C2xo)s6ueS8#BVA11 z2qrrTfZ3gAT5+k9G;Ta!jH%#Lhcts^0)b_F)}I>3&u}pS?`cYjEE!!s9#<2iurAI# zV-zJ~i{SB(SE?G9u&?u!=2W2l%v?Z3bQS({fs#}!&m6P)#Boj2uk!9-vrd>4;muTd z^MJgT0TXyehkKk6FPJo#@EGlewbqQEMTS@u-F7(j0X8#?I;Ag)ymiMI?L$x+-&pa) zURZv3a)N{?3@iTt%v{CE$Z(PQ%a$+Vn1NRfyFl?bgbP7O1!L)Pj){aqYp7xz@Y3x3 zVo2|X1o+qUn?Tj!`FZC6A-98fALk;AMaVRW`1T$%hAjZC@s24CTpo-!dyh<@$A>mkekvV=M`PI$&vHEvyxc>kJ0D*#hs#0FVX_C+|YBxbU z`^{?X9wfd?=d3Xt+{NCHl5kme0>JV$?~#LxUm%mWQay!t_R+z{2|a*o-~h+Ljm{@@ z=ZFrnuP>oBe8&zeP4LK_Zm$&MF-~-FFHp4SErl_FO?sbxE6j8f#&gvnnuQ- zI3&SS09WogMfWgktzx6Ukg2aXW-mLegL`eQNyfZoD+$(*;`i}}nPyS8fJS}m8UinL zIu7_A_m9woju0jn*U5=QGgM&i&e}t)c&|ukkTfRNBy?i{k;V(RYdm$5iz~L5ZXUJC zoKToF(jDu^B+FK12u-?5yOw8qzQg47Z`X{}G3mqX?{gDZXOr#pM0y8T8ExMP!t*)( zOkN}=fYf!<-zEhpPSTH^C)O{SyT=r$aqkv8H#aLAgJHAT!9{F{M`P}}!Ra9KJLjbQ zIdo^#oe?=Tc=Hls0z$b9v+_7=5rRE`n1af93%__!s-gh?Fi7MmTCT~{5bC@yB&>Z< z?Jn02A;ii%63>3IuYke1!E~1wxw{J3?0Eii(j*Xc7vndwD1wh;&Nw1w_-l_28dIMd z#Hoz|JN1Oq9oE0&6||h-oJ_9XPgsaA5qN(Xr{^>hDZ{;#!%5p4Cn-Gh^5HjdCRxaN z;mE03s#%IGM_%uQ{NO2erR3}9UNIs|sX$VKe_pZRv}EDA$JQwA1^|-yb|y-}Vmdjy z#_)9m>8<{9bafTJ05{GKx-T=1u@(n^C;hxMF^;OPb<~)tP;6F>ynSbS8Dh>Sl(Sec z0YDN2f#d)r6e^)Gl2v+yiF0}um@fGRaB=0ngqJ87+g!JY$zm4{YkKWmD)lzcwjUGo zl~auGp+EN^NR9#d z&RRhs1E@4PSDHqu-Ql!(NwvJNWz`*F^fM$=#^?2nB>5dbQxs4QYc+TA?;jip9-ILl zz|#zvc3&7h56TxKuIJ+*^BOJv_{EYUfDi~F)8t`HC4oX7paX@kG9Av9Ge$WxD)gMV zqTL1nnLuX03C6YF3&nTN^I)`w{{T#!0vC9sP3<)tBC3dwJ~42@rx>CexjVyUXPlsE z&7M!gju{U7#z@n#GQZ%Av9>-;OGh5EAxLsAR)>)FhdW=E1gR=SesJl&mcA6u7H(o4 z+#W?jh5+lnbpQdti8M@Cw1uWMN*^K~vX+B!lB`$`;k|prMGXc~2_Y!%+0DiR0THrV z8t=Ep9<~9-)C4l`?3f&7)w~1@NP!@+p*^F`P%RDY5Pg(vhK9j(G%`Pob7KfS3{sq` zhY49mN^Lt*uWj>_n{*0Zzc_^gqnnmh5y5d-bpU}JUq2Ib7~miii2nfJ8DIuG7hU_s zzySi7PIvKeEb|7KAMuvdi`&Wb-Y|jltm0OB$3-H}AHC}ufE7Azhlf~1hyW0mHA(}9 z&m8sd16D*-ss8|kC#1r2#b_-~wgV3l@XoD?Vo#21ek!NK4+`RA_~G?@e# z{yXOm;YUuhLVyG4@VRy2z|=(kz_-J>N#W{=K6$L#7Er&Q=n zpxM;Q!L=f+=u49T5vhGpPmEp>2gl<`UlB zHesY*Fck1gJuVh+9H?(bMJAh#;ZCb)4`v^?6q}%r=zrDmoAriV-srtz-ksT3e(rhe z1Jnc?65q-^bH+}4iryi|tzzWW5j)^=f5gke#Q-~WHkJ6sN1!+Y2;+@%;-D`IUUurG zNiyIR=em93f?YCce_-b-KSr$gk=cls#`W5As}gy}QHt^g_0u+aW9X-#7HBk&gu^uz zxi3B+semYzcD#`CcpSQYR~fi+H|I`_^>H6wo|AFTi7|-h(v&2dzYqo(@zh>F6C zUCr^n=g2KL;izwzrBkd0%4Ka5205S@j6Aos!E@+GU=NJRuR>qm^JwvZJ1 zf!|o(h`8Qv?g8vw>kaAtim}+bN-r!@C(81r=de=4xUB0NOlY=?d!R;VSWjm6Ji|=N3)7;ll`L z9X@xbE{Pmb!Xi0w(0oizcSKYZV4V2hdd&@0+91KAmrF@(xE|;!<>7dH%>W6x6W6>? zO2Fu?WYt?Plx@@w?B-!mdFd5?4r@4aLl64~X2S6&6H-C>Uy=C!Ai31~E$jj2&Gj3}jv)kE9SXtu=&Xm=*weIoFJ? zN@+8c7?PbBaeboNXQ+kX}&N}($Gl!_lGTlc^OnlaPQ>uednt2*?*jB9?=u` zH;+LE_I^0T61fBisN&I5m1ulZPCLP=w3>Yf#s?yB2y`A{^^OHif_ATi-W`)_K-Rt$ z56(2?o?MVT{{R@U%{0(^z;%1VZsBq}+-%;$bDC(0Y;v7lZ{j%1;uJVN#7{PtyhDqu z!}37-AjZ`LhV7Dfd0xyCYgV~Rif8~V3?(IEIl-goUa{*ErTwe6Fr6_<4$xiCSZQv= ze!|uYL^dd$)>rCg-pm5Odt5Yd(xQ84Yg~UgllEYVaz{{{c`=}XX`nQDb9b)wfB~?l zR>WQoaQ^@sp0K+Xy>aIs;{cZ4mXE9j7XSeIF1~ORuP9T)mMB$$6GqGX#=CQbgmw)w z?+bV(Lnv)iU%V6(S2PJ(Q291}Wae0G4X>W?*my@^ z(o>kJIl^=jp}MQv%Ytl00C)qeeGCj|ZE9q0!B|$pN7PntnT%yvbR4(FH42E8uv2T+ z@K&fUeWRV{kCq}KFWo$0!nwWTow# zp|j3$!|Nm2KoR_13?l_S%S*@gjDx7cdDon<8VpZK`N01Gp+~~s#x~5?v30lBUpEE= zf;Zf5O4ANd4L7&i3*-XSo=@(%v< zgv1klcYj#uBoebndBEh}uzc?w{bkRHsj78|1Ip~*=K@BSFek z+}}Q2qq1H*(cP04;5;~8-<;vBlnm+y2Ej{@%pDfpIFxLyC9B3MrAXlR7D|tg<3ocN zm*?M*FRt-g#3+maU!C5C=L-M;Il)(Elyw()r6XzTutj<`TyaKpyP+2vH+#dTFbj?w zla|HWOm2pcT^>2-6i_2bcF$d6)j_+~1iVGy88}gOPX4ePxO8a#@h+Wppbx2=cSRy= z9tts=my7t#xqzdWcLhT6jtP0zNKkhL^@!*h%3uT-L~URdXaitCw!)wti<w9vGI(cc*(ou zCyX;;*7?RrzvdY12O#|58h}@6@4fnA>VvH?>Y=l2QC|$&BfXCZ(nP|TuL$`d0815J zaf)GZsE(SFlXCnT$ft`3iaJ0*b$0cP@vMq4=?0OTTn;dLLwx+^q@ z>Ao{zASY%DI2g|W(CgMgWQ?1~Fpx5YUbW5xb|s)|quze(IBdn}%h3S#WPoK+x3~Mi zlnpd~*fC<9tqb2;n#M@f78mc={$R(MCuH&Ug=$g*EsMO$0hG&+8hYvVq8Y z{b49!L&M&;b7)r17?>$cl7J5nBRY%Yvj|6dYj@=FjZ6;C{{VesbO6qm@V`;W8d1>` zbttTrw8W+J2_d_gEZy+IyOKkNTvgg9ym1gB((`Zs05OB5UD&-i!jRK8VkkKBVdr_E zu-*vHnM=t1y2hbZwHsaGa%s0NQ54a}A+|M2?+!9}vJrzr?yTVX!$rOW6)$OsM`00k z*q2SWqYXnrf3iGSI;+oPIQWzhf?vJLxe@Aj~I{v+nMVM zmLM4LK}pHqSZ@!)0oM;4Lp9}RivnfOti0%e^CKI4TdW6)s^_xs>YlO61Uq0cv4C%9 zIka))5TM6b{{W0=-6^*2q0KqPLDyqI9yFWmxj#w~Ck=Gd5*(HM#Tht3d5LryUU0*} zFahPNb>la(L1gu!!}a0IJs@0lh&dZ9P|jA9x`f+C!B~4=j%crlazHhvycWD5-q=hVx#|9iBYz zX1+1O836?CJI$_wFpaITd4mT306DM7Z?=!K4_Sg+8q#<4OczH2zD^)`gO;0!qF{!e zM32?O;UQMPdhfK5hagFp@;@WdmZQnu4;=w;;0&B6s9~513QS$o{^Tr{FHmRr`uM^%~EJU+#N91leeFdi4 zVyn-#;ABmh2c%+_bBwJ)2_wxRPlMJ1vV$8nW4qTGxCT}{_zr9EPmTjscc?jr0? zq{K&{#6YgMfEDLpgX0%eVG8hK%rq1R9!V3i!K>9|J)TnqAaGk$52Sj36 z5TFejuT;c0z#ve6E+8c;(4{<2`_#>l>Lnc_;gTMS?+M+n0EV z)0J@hz)g-&g19eftXYmcVu3q2b3-?AKDf9IfRPa0IkQ8Z;AX9SJHZ;z>@hWowvYM5 zWYyAOZmn6+bushV>DDBu@A74=SK|?jYs-Q%cR$t@La5+%WXQN&G<;JR z`1Ndf({r~0-M1Rl8;+BSiaNb>pa`@LwBepL#Og5B3_QHxE>}Qws_6>G46y=&5&r9nvS0J8A+@9(7(xziv4mCF zh4^r>XcM$};~KTZ5#hji3m^Ba9IdY)p4SeeaX>D$-Nlfa(;9MjsQTG~$__Ms%tDSYD*~ayeSR}&tfqQI{MmCu1TcJ{egt0fNN}wY<~<$A#yA?}#h%{}hXu&=F(14P1S^jH zVuy`wICVj^vIi_X!&@p_CidP$;=_XtjdS99N}r$S8PNqJ zPR9NNmLu*0ysNeLt|~vzG_eWwoaCsVkg^|5#jhu)@b&tcQjms9_sY3{GQlKk z`rt$q5W?!Qm=Hs)mH5ZmQbX_<yO#%_9HOz4+pJ6Dh?Djoy#7uNx zG*Q8Ne$Qq?PWRiJbe(_m9;q+dUSz@zpq2CtJ5Yz<$sGfj+o%HS$?fVbh@_!i0m>YG zonoY@KrqxfTi+fGK<+gzoC%aGDjFh7qrJcG1Qw_`MkG}ap0E%%6L^44+2<3Ft>4*G zz%il{=s&~Lqt5ZkBvd!H@cs6Ilyw?2^ysDait3bt_$Oa;;WGWu`_}6DM=OOxf{KCI z8!!Hc3^KmAUMaEB2iKIP(FpKe$;WvdFa~LSo_OACjHD0>H@DOI#f(GDrI31_a#u9_tFhICe;9-%yOKHHRXXLlm&B4KeJMX{&CWJ8iMj1UHHS+6Wgu3d>Kpu08PCooScn> zHaqC$)Qmbcp?$+CMpPQFUhx*=r9Z|s2Gt$c3$B~3C(T@CvfC8zTG^5cK#s*W2H-U& zpqP31ehei<6{n9ll*KoM3BLiH+?}e{7omNZ< z0S|5f6*a3fX+GmlFV?zmB?0b+GR&p>f$Mds_m;^|2_aLi!(khaqf^0SaQfE4>AY!E zK2Cr&l0XUtNw&)fRVs)?M;ncOLH=8kLOr#I<`_J5v`zUxrW94xhv47bGS-p%!Y3e0awLL_;;bj83w8mv4+-d`MRl zAdiC_(HOLZT3LzoZ3Fc00W?jzihjfIFQ7O`uop`8)x>E+!p{fJ3{BR8TJI$R5tF_# z_-J_zVVtS6vnCFGm_j!bM;vJj9@1~Z_y|BfOaD}Deo0LCF_g=ka&k!3e-6l?bbQ$ ztp>NuoBpz)YIEi1AEYNV2mJS%fqH3<$q9(vgz{o3cQE*(C^-jA{td+SM;subkBzZE ze_3Ghi5zfFwY=dN7VDsw==nLpWLkrk#+`SNUc^J_GZJ;7$uU6vx%gQppnNh}}F~14mD9NR<3THMUAk z{G}fruuf%34_U{bry~kABd}eZ^*YX1D7Xb)bQ|Ul$}JN_q5A!a&XL7UwWhKQuBXoT zhpyyl9IrkfhakiUzz>&&>emj)iz_<0TXw^j!srz1H#W6$`%Dz+E$}+m-W?HFLlUSW z-$VS#AEaRi!7seuPl?5AkCzn*Y;t)!KJFU5?<_V0rR!&tCs`^>autVmHCc@~0W7>G z;Le0g9C;8R0&U%PZ;_tH0=xMJwVQ-?YB_<^K(Nwg<7|TbRlKd#^utQ4EsK1_U011& zj?}OoO^_azVFZz4qI~y1Qu5G(h*L?rcnJ!KFhla~IrVhDCX^!#Kg61P|L zm)3@a3UG{=O~Mypu23RJG$Q?i>kThrPkb!%^Ot8dnrq)&U}o01K@t;Z&skixp{yNm ziZ8=)9%>nLvJn$wmq9)k2rt%bhk36G4jsK*b=i6$#3OtMRLyEVQ)(m*qpnrji58An zXp}dn_{?RM+;I>(IW6%q!e-o+O9vYxe>eiQIU%&TC?8>RgbF>lv@ns=axlW#wL)r9 zR7bNN-V0Pl_O9{Miz;Y;xn7V#TdZ}1epdw3HR%5UF_EF<0DcVA3che4zFt|%n59&W z4l>Jz#vW4qxa!})a2`Ejv%~R$>wX;IZNIFDD?;^>0C7OL@JC8`&Qpj)-Vqqd3Sd=- zIUb5(LmiBNID|4}+^nTD0(KR8ta{&MZVoKcs_&h6O#Qy7?Xd)e5Jk&9RJ z%Kab7a-SjMLBcNIfWY7KOUD7PSU_K~3iHoziG&a23_p1~Zb1U)EcE9=?*1{6ihm&B zBao7p_k>ei3A4zW%2B}$K29bAMhz_oPqwqfqHo4j$N`bIxM^l?_Xz8z&YgMBAe1?C zYW0LkIXMf)&eqI_J?!KpvWkIsVB;M-Kpy+vGilN_@G;*Q1zWv*8`NP_Mzl11bBkj_ zy@CESVg(~twr|;%1rk8K!|PmA>m9T<%H?*?U1eRC{x}MEz`v|YGEgU5l>B0dx=g=% z{9w~@x(?%)$e8zAIsxV(^N7}kmU~)zj9aMVKzm$vZ#E)1Jj$GGuXDy|z86R@N3#c! zz+dF|fC-{DqQ`@ip0Hk1TblwMySvVO)gnAVpVlxr4;A(%ZQ3cb zlkd(sLIFV?6hAnCo6zsl^M^Z!RI%4T)+b!VMJ~QF*htZTkj#%r)}ODO9e1sCr^i@> zy#?b2$1u^35eg9pSC{jEpC}G+4%UgO)**x%TW60M!4Ub#h#aS85e)-p9rJ|N9jQFy zr{rhz{bf8COX)+#MVM+%6yz-3|TDKyUqfj zkrZ#1$L|K70Wh8@aL1m&02@6iHc)~07GQ;ycqob}%^)#wp5{CC z4XwaVrZ;?vFHSq&^@B&I+BHEH+Gy@!z;HqUgh3lh@k~UKTLqu&p9_-mz&IXjg=rcl%sn2r zh5}6!N!}}IUQC5R>~1xSg8u*IC1)kLB2m+I1u6<&$+e+RaK5(dLqNgwa0CBU84rXdAX3%BC5XEGl#%V%$$78jk2Bq^^4I{Ikbhwq;wQlio{aI*c8+}YY1Kk zA%E>*U8EHcjC~}iZ$5Xst}y`)t;1TCaMB$m0p_?@Q3{{I<;#r@V<_vH`7FwCv9sY? zd#*4Z1i_7h5APm()qz5?cx>d(dV(ZP!P0!;vyl_Rz0=6?id+Yc-c+9^1SirMhs*Z3 zT+*UFb+jK26}AektI6w&Il&B+PZOpOTf@_lB}*y`%es7J`7u*DJ2b!UGJUWFkW#2v zbMR*V{(>^FtJ_SFXr+%apwT>zKiwT0^SA36uhcXTF~812&@|DfO2_HNdX+<;r;LzV zaQ^^kWv6Poho18cRFPAn>udF#&kDf#Y<*+KN#G8^Df7IraBbN4qxFIV5!g)toqaPW zQ$^0*kRK@G0gqyNKxjPgTIZand+#Tehvy~Br#}V&&8CQ_c_Vy)b!jk)_!V{tC(_8r z(u5JQJKf_Hl3@Q6{07}Io-g7j38+H=Kz&JRz$$EsQ?jvE+mpd zdlV&pU2%Xd^a2&&+`Ro^dNGuz$!1F^V(uUJyg)*y!5NJ(gb~`jBw$Lht}zsS3#@=- zmh+o&tFN^vIP>EGe1;Y@Rf0jPJBKn_2^EAfRW!T=!f$tI!b^qIjSa4aFU*nn)&AGlfbaL|~?zes;ze4-BRkB`&7^ zVQ=bzfSOm)i^ngVQ$=~}5(GHjLBLgRYf7e**LkWy zYITc0!!{eQgEj=X2k#)3pTS>HHra+ccO0qlgo%;&jW{?04cY4kd^A>0NGam0PZ>r zoCUKj(7v$l+s`D$OGS!N{_-*p5XwJM79)JxRgbn4GKyIm9e$`Yya{{m0|OH1^!wC7R2~1IS8@a$=||PtHgXnCgcjPk)Rk zHU;+HY1S+&u^I8i#}7mXzZoT2*kCe}uC%6H0SW{}`7j}&jvohj-c&hE-A4p0o=rcD z*D)3e^h_gqno56tVUS94L~Tr=s<&P-2PW+b{pQ;S)YI(AQCPeAhOsq34FmVaI05JZ z`|A|OXg8fobBIj<19SRuI}F=T$a9>h16y4!>*EN`Nvdy$(B}RlTOjtPdqj+L-!~-~ z(%+b3I!oPr1<~_0iimxf7Wd)8+SQ=c^TrPfMkrmI{&3Plwf+W_PpVUY2h{H z^LobzLuouAt9P1D06fA?;xrnKjvy14QsfaV&>D(?d85W$BVr)yaoTZ*4kTQm;D_7u za09NTZw~mY-;5?PL=bR`YA3Oa$AIoT7I)r`@eA6C(s`+k{5MdFY#X{+UNFd>V@C$V z9^*`!T%N~g#y7`SrvP)LBd2{gMW&mQRd{u}YXTmE6lX#2{KDw%25sJeiHd7IK-7b_Z)lP3I)g#uGIZH`N3MTWd~-W@;`Y~3r^`AHC!Gb;oIpD{{Y;% zS4B2%OT=^F$S&S_#X|x;jPZ#J5Ch=jHb;|ywYhBO8!ps_lkDNUK zb-M=t0F35PXeri21u4q?WG%tiDR4j*IY(`IbYQ;qy%&Bpk^s!jdV$_?WAw1$*(^DYvq5lA`N)Z{odx=~GgjyYMPk(Ms zZ54{S!HH=DqL93L-X5npJ;wuKW#IRSWN4N;;llwyHwvc7&aXakIsyW#qLU3k`5W9i z(M6jAy9S&B*MMEOLa#<>tqd-(SGIsW<22B=?bERVNy~3#rH@Zf_!|^+hIY1XmcbkEi2?|J~Si#`hxcbU)T7!+e z2+sa-8Xy#X;d5bH*iJD_Rk-M5taj07x!zWS#np*CZcn^VYDW=OhSdK63?feJ;JB9!?LgmTm`cSX|$U$oRdIP7}7v5h2sc~3Zfl~Yt4_m zI}>#gQ3l!PjdPEh*0JbPX|YZ6!logls}S<=H#pnH$60ojH%9b?2WN5*Y#gK=m7h1> z5j9{Bm@28hIK*}voF9PY6dX}8kY~pQ`fvpOmk0KX{+t+`)@S<11|q;8jxvaFN(lU5 zOqWZ)yyppn>tY4`Yc~^@o$k&KJXUII^Af;;_Chz9r zsz_+(#~ft16+-0h9Tx&H(bD>C`*VatNDj5>b%B9Qt+T(=HvE7&J$zt-0wOB$vcJYK zCV^{Sajdzd`SN0->&)fF>#~kQ`tgsRR8+_l%OrQp;~NV{L<6bk6O%&HcH}(mDm!_b5MI1Fe)%Rw(4P5mFmRk39VKPr;FT@V{rCD@LRBkOIUf(aoP1@U zF693J9hkLP^3smd@%4vtI$-$=Oenn~I{^)eTvfqAWYR2^oao~x2u#uCQWxg~7-4SX zc|DT~EL>YVzRMq!tQMLJZ;#NBKAH@Mzas^^@2*%5?D1JwWc` z`S5f|Eo;G+3TwO~xqzZ@Y23IveD!|~EuNh5maR*yilT-==X|1!tjT`c<;T2CFl1{ma_JH$RuSXJl{CNfqG7uu0B-W zDW(!v)AfX>5jZ)hos9L5l=@bLy}q#&^^ynV%0$Mj6V-9@N?$i2()zeM1K>+Oa-Jex zLBGxhFt=PC$4~wB3xCT}? zXd}RWhYQ{=;no!kXhIrbEiNZSf}d8AQnGX)W#WYzG&^#TfldtbTUhu3_s_gYNZnTk zN@=UHeHgB*VLwMR#$LK%Rz`zJBqBZF0RVXi#-l#)$%;;cn|^UrDStQ+IsJnEFxoT5 z+>ACH+#78UcZ*TYcg_^74DIdjAOR8t$Jjf~i4|jmI+NwcciF7a2Uni5m>AW@JK$ZV zaP>%AedOq-YkJ30-eUpMtXvqOM%B&)N|_;?=Q&W6b%2NwY4OpPmB+zB`xw+DMJWS1 zTF4Q1+kq*CRM7dy5M8c5c=22luM|`p=M{|r*%}_qm}Ez7=MEtQ3L6@NW2{7~tSXmC z5XLME8ZDL;6Ika)s~>brZul&LvWf6fn9w~(~L2YT)zV>E6O!EG!4h0Pn=fg?jkKu$jmeOBC z_l$X1yfri8p0P^N+-d;-V<&iO*j+$%A~AE7rkL&^R#Q?qlPUJ)I5W}G)#+W zAA_YI<9NiV1Rn!q;{#|x3Uen$@P%s854Plm*wTgTi0{1F#PGZj+u_7FS`UF-rk&n$ z$+lb5WF^(oTx2S`hK!<=CmZvXG)Y*m>-c7>g(znajt=<4Qg$;&DJ*r~B7Q;liY~92 zm;S)*O&Uk$UwLNhS+xRRKdv&CPDBy9hTojCXadx-mF=sIY#_0wAYIwcDPc6NSKMEm zSy+W%vDkV40PZt|aYfa;B-qKuJ0820PWDmTAto1B0&d`0wN2Zl#jd89siW7=PcM09)v%^dVVupi{Wlr)p$n!LDnqqiB59`nKo4|R2 z7fH0|Ac)*ZKa;S(hZq&A7LG2ztQFI)%DUOXed4(%kh=MpZghdK;l?Eu@%5o&x3vsK zZldF$-0pVgylEX0;g3V(2@eabni6zqb=k%fDts($V=^K?amazAcuzk05c$s3U)yL@sF5=A?HodhEtA(AAdQ}MZ>(q?r=vCnoL~?u zz2IA4sg~=ftWX0G!uuZaoIJWeIbafX=N3?|=l$MEtO?Is%Wf~S8}Pb_av{p_yIofM_@cl3^(bT7qxNn&$u@0Y^gdJI*GjWv8ZaZ=4YQPi;wHa21zIx`cCwDTe^`*F#iNoVIT6-W3?vQDt{5;88+@NcbGN1oFKbnl`7o zi~*PcX#W7Z$Zum!Q;7KE7Y9%v_MtxG?d%(;80HwyCV->P*M?|7E1F^zU z;}oz_t(-kSO)*~zK|@3x3)u2jxm=whAdn5ne0|~;(jccoUW=a0B8okvYf-+>!vid! zi}Bh{xh5z|YV+T(7`dW|6X?D*lsXau z`nVy^it#+;sZ(^g+{95@qW=I+AX(tNygo1mYRCdTe;9=be#?$;zVw$MT(S>8jL_qG zm=S^<XwHj-$LS+B#ELgv*dkNFKItkJc�UA@muKuwu;Tb4)t8ce?_UA=E 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 literal 186824 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 z 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 z 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 Date: Sun, 4 Jan 2026 16:38:06 -0500 Subject: [PATCH 062/137] Updates Vico --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f2c9693c5..315e587d3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -51,7 +51,7 @@ torAndroid = "0.4.8.21.1" translate = "17.0.3" unifiedpush = "3.0.10" urlDetector = "0.1.23" -vico-charts = "2.3.6" +vico-charts = "2.4.0" zelory = "3.0.1" zoomable = "2.9.0" zxing = "3.5.4" From fe1e0e46c3c04f702092245fb93f26d8c1994dd0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 4 Jan 2026 16:38:21 -0500 Subject: [PATCH 063/137] Fixes warnings on test case --- .../quartz/nip19Bech32/NIP19ParserTest.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip19Bech32/NIP19ParserTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip19Bech32/NIP19ParserTest.kt index 58eb50846..072c6e5bc 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip19Bech32/NIP19ParserTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip19Bech32/NIP19ParserTest.kt @@ -106,9 +106,9 @@ class NIP19ParserTest { val actual = Nip19Parser.uriToRoute("nostr:nprofile1qqsyvrp9u6p0mfur9dfdru3d853tx9mdjuhkphxuxgfwmryja7zsvhqpzamhxue69uhhv6t5daezumn0wd68yvfwvdhk6tcpz9mhxue69uhkummnw3ezuamfdejj7qgwwaehxw309ahx7uewd3hkctcscpyug") assertNotNull(actual) - assertTrue(actual?.entity is NProfile) + assertTrue(actual.entity is NProfile) assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", actual.entity.hex) - assertEquals(NormalizedRelayUrl("wss://vitor.nostr1.com/"), actual.entity.relay?.first()) + assertEquals(NormalizedRelayUrl("wss://vitor.nostr1.com/"), actual.entity.relay.first()) } @Test() @@ -267,7 +267,7 @@ class NIP19ParserTest { "31337:27241bb702d145a975260cfedee6265936dcd939eaecb88ea0e4071752c30402:xx1xulrf7wdbdlbc31far", result.aTag(), ) - assertEquals(true, result.relay?.isEmpty()) + assertEquals(true, result.relay.isEmpty()) assertEquals("27241bb702d145a975260cfedee6265936dcd939eaecb88ea0e4071752c30402", result.author) assertEquals(31337, result.kind) } @@ -285,7 +285,7 @@ class NIP19ParserTest { "30023:46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d:613f014d2911fb9df52e048aae70268c0d216790287b5814910e1e781e8e0509", result.aTag(), ) - assertEquals(true, result.relay?.isEmpty()) + assertEquals(true, result.relay.isEmpty()) assertEquals("46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d", result.author) assertEquals(30023, result.kind) } @@ -303,7 +303,7 @@ class NIP19ParserTest { "30023:46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d:1679509418", result.aTag(), ) - assertEquals(true, result.relay?.isEmpty()) + assertEquals(true, result.relay.isEmpty()) assertEquals("46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d", result.author) assertEquals(30023, result.kind) } @@ -356,7 +356,7 @@ class NIP19ParserTest { assertEquals("b60ffa7256d3dd7543d830eb717ae50d05a6c32c5f791ed15b867c2bb0b954ac", result.hex) assertEquals( NormalizedRelayUrl("wss://nostr.mom/"), - result.relay?.get(0), + result.relay[0], ) assertEquals("f8ff11c7a7d3478355d3b4d174e5a473797a906ea4aa61aa9b6bc0652c1ea17a", result.author) assertEquals(1, result.kind) @@ -403,7 +403,7 @@ class NIP19ParserTest { assertNotNull(result) assertEquals("4300ec7fa2f98a276f033908349651620aa8e282b76030ab22abca63e85e07e6", result.hex) - assertEquals("wss://relay.damus.io/", result.relay.get(0)?.url) + assertEquals("wss://relay.damus.io/", result.relay[0].url) assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result.author) assertEquals(1, result.kind) } From a151bba3d0f3a9bddcb3feb80063ae196e9c9633 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 4 Jan 2026 16:41:58 -0500 Subject: [PATCH 064/137] Improves stability for video ui --- .../vitorpamplona/quartz/nip94FileMetadata/tags/DimensionTag.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/tags/DimensionTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/tags/DimensionTag.kt index c5fc2131e..e03ac1d72 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/tags/DimensionTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/tags/DimensionTag.kt @@ -20,11 +20,13 @@ */ package com.vitorpamplona.quartz.nip94FileMetadata.tags +import androidx.compose.runtime.Stable import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.utils.ensure import kotlinx.serialization.Serializable @Serializable +@Stable class DimensionTag( val width: Int, val height: Int, From 115c622dcaa0f904535521631532b0e23c85d325 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Mon, 5 Jan 2026 06:46:19 +0200 Subject: [PATCH 065/137] fix profile not loading --- .../commons/network/RelayConnectionManager.kt | 10 --------- .../commons/subscriptions/FeedSubscription.kt | 2 +- .../subscriptions/ProfileSubscription.kt | 6 ++--- .../amethyst/desktop/ui/FeedScreen.kt | 22 ++++++++++++++----- .../amethyst/desktop/ui/UserProfileScreen.kt | 3 +++ 5 files changed, 23 insertions(+), 20 deletions(-) diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayConnectionManager.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayConnectionManager.kt index b09c662ae..dc026c4d7 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayConnectionManager.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayConnectionManager.kt @@ -104,18 +104,8 @@ open class RelayConnectionManager( * Events will be sent to relays as they become connected. */ fun broadcastToAll(event: Event) { - // Use all configured relays, not just currently connected ones - // The NostrClient will queue/send events as relays connect val configuredRelays = relayStatuses.value.keys - val connectedCount = connectedRelays.value.size - println("[RelayManager] broadcastToAll: event kind=${event.kind}, ID=${event.id.take(8)}...") - println("[RelayManager] Configured relays: ${configuredRelays.size}, Connected: $connectedCount") - configuredRelays.forEach { relay -> - val isConnected = connectedRelays.value.contains(relay) - println("[RelayManager] - $relay: ${if (isConnected) "CONNECTED" else "pending"}") - } send(event, configuredRelays) - println("[RelayManager] broadcastToAll complete") } private fun updateRelayStatus( diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FeedSubscription.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FeedSubscription.kt index 66454159c..9f65c2379 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FeedSubscription.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FeedSubscription.kt @@ -81,7 +81,7 @@ fun createContactListSubscription( onEose: (NormalizedRelayUrl, List?) -> Unit = { _, _ -> }, ): SubscriptionConfig = SubscriptionConfig( - subId = generateSubId("contact-list-$pubKeyHex"), + subId = generateSubId("contacts-${pubKeyHex.take(8)}"), filters = listOf(FilterBuilders.contactList(pubKeyHex)), relays = relays, onEvent = onEvent, diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/ProfileSubscription.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/ProfileSubscription.kt index b1cbdaea4..446f7e1a0 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/ProfileSubscription.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/ProfileSubscription.kt @@ -35,7 +35,7 @@ fun createMetadataSubscription( onEose: (NormalizedRelayUrl, List?) -> Unit = { _, _ -> }, ): SubscriptionConfig = SubscriptionConfig( - subId = generateSubId("profile-metadata-$pubKeyHex"), + subId = generateSubId("meta-${pubKeyHex.take(8)}"), filters = listOf(FilterBuilders.userMetadata(pubKeyHex)), relays = relays, onEvent = onEvent, @@ -53,7 +53,7 @@ fun createUserPostsSubscription( onEose: (NormalizedRelayUrl, List?) -> Unit = { _, _ -> }, ): SubscriptionConfig = SubscriptionConfig( - subId = generateSubId("profile-posts-$pubKeyHex"), + subId = generateSubId("posts-${pubKeyHex.take(8)}"), filters = listOf(FilterBuilders.textNotesFromAuthors(listOf(pubKeyHex), limit = limit)), relays = relays, onEvent = onEvent, @@ -71,7 +71,7 @@ fun createNotificationsSubscription( onEose: (NormalizedRelayUrl, List?) -> Unit = { _, _ -> }, ): SubscriptionConfig = SubscriptionConfig( - subId = generateSubId("notifications-$pubKeyHex"), + subId = generateSubId("notif-${pubKeyHex.take(8)}"), filters = listOf(FilterBuilders.notificationsForUser(pubKeyHex, limit = limit)), relays = relays, onEvent = onEvent, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt index b162357e6..0636f2041 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt @@ -138,29 +138,39 @@ fun FeedScreen( } // Clear events when feed mode changes - remember(feedMode) { eventState.clear() } + remember(feedMode) { + eventState.clear() + } // Subscribe to feed based on mode rememberSubscription(relayStatuses, feedMode, followedUsers, relayManager = relayManager) { val configuredRelays = relayStatuses.keys - if (configuredRelays.isEmpty()) return@rememberSubscription null + if (configuredRelays.isEmpty()) { + return@rememberSubscription null + } when (feedMode) { - FeedMode.GLOBAL -> + FeedMode.GLOBAL -> { createGlobalFeedSubscription( relays = configuredRelays, - onEvent = { event, _, _, _ -> eventState.addItem(event) }, + onEvent = { event, _, _, _ -> + eventState.addItem(event) + }, ) - FeedMode.FOLLOWING -> + } + FeedMode.FOLLOWING -> { if (followedUsers.isNotEmpty()) { createFollowingFeedSubscription( relays = configuredRelays, followedUsers = followedUsers.toList(), - onEvent = { event, _, _, _ -> eventState.addItem(event) }, + onEvent = { event, _, _, _ -> + eventState.addItem(event) + }, ) } else { null } + } } } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt index 41402280c..cc0f51ebb 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt @@ -174,6 +174,8 @@ fun UserProfileScreen( rememberSubscription(relayStatuses, pubKeyHex, retryTrigger, relayManager = relayManager) { val configuredRelays = relayStatuses.keys if (configuredRelays.isNotEmpty()) { + postsLoading = true + postsError = null createUserPostsSubscription( relays = configuredRelays, pubKeyHex = pubKeyHex, @@ -225,6 +227,7 @@ fun UserProfileScreen( } followState.setFollowSuccess(updatedEvent, pubKeyHex) } catch (e: Exception) { + e.printStackTrace() followState.setFollowError(e.message ?: "Failed to update follow status", e) } } From 55d59fcfd25eb6bcd7256cca8a4b8bbefcd7f1c7 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Mon, 5 Jan 2026 13:12:39 +0200 Subject: [PATCH 066/137] fix follows not working --- .../commons/network/RelayConnectionManager.kt | 7 +- .../amethyst/desktop/ui/ComposeNoteDialog.kt | 9 -- .../amethyst/desktop/ui/UserProfileScreen.kt | 97 +++++++++++++------ 3 files changed, 69 insertions(+), 44 deletions(-) diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayConnectionManager.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayConnectionManager.kt index dc026c4d7..3e0f7159d 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayConnectionManager.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayConnectionManager.kt @@ -100,12 +100,11 @@ open class RelayConnectionManager( } /** - * Broadcasts an event to all configured relays. - * Events will be sent to relays as they become connected. + * Broadcasts an event to all connected relays. */ fun broadcastToAll(event: Event) { - val configuredRelays = relayStatuses.value.keys - send(event, configuredRelays) + val connected = connectedRelays.value + send(event, connected) } private fun updateRelayStatus( diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt index bf6ef13a4..a532439fa 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt @@ -177,24 +177,15 @@ private suspend fun publishNote( replyTo: com.vitorpamplona.quartz.nip01Core.core.Event?, ) { withContext(Dispatchers.IO) { - println("[ComposeNoteDialog] Starting publishNote: content length=${content.length}") - // Check read-only mode if (account.isReadOnly) { - println("[ComposeNoteDialog] Error: Cannot post in read-only mode") throw IllegalStateException("Cannot post in read-only mode") } - println("[ComposeNoteDialog] Signing event with pubkey: ${account.pubKeyHex.take(8)}...") - // Use shared PublishAction from commons val signedEvent = PublishAction.publishTextNote(content, account.signer, replyTo) - println("[ComposeNoteDialog] Event signed successfully, ID: ${signedEvent.id.take(8)}...") - // Broadcast to all configured relays - println("[ComposeNoteDialog] Broadcasting to relays...") relayManager.broadcastToAll(signedEvent) - println("[ComposeNoteDialog] Broadcast complete") } } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt index cc0f51ebb..9dc55d75a 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt @@ -122,6 +122,11 @@ fun UserProfileScreen( FollowState(myPubKeyHex = account?.pubKeyHex ?: "") } + // Store the user's contact list separately for reliable access + var myContactList by remember(account) { mutableStateOf(null) } + var contactListLoaded by remember(account) { mutableStateOf(false) } + var eoseReceivedCount by remember(account) { mutableStateOf(0) } + // Load current user's contact list (for follow state) rememberSubscription(relayStatuses, account, relayManager = relayManager) { val configuredRelays = relayStatuses.keys @@ -131,7 +136,22 @@ fun UserProfileScreen( pubKeyHex = account.pubKeyHex, onEvent = { event, _, _, _ -> if (event is ContactListEvent) { + // Store the most recent contact list (by createdAt timestamp) + if (myContactList == null || event.createdAt > myContactList!!.createdAt) { + myContactList = event + } + followState.updateContactList(event, pubKeyHex) + contactListLoaded = true + } + }, + onEose = { _, _ -> + eoseReceivedCount++ + + // Wait for EOSE from at least 2 relays or all relays before enabling button + val minEoseCount = minOf(2, configuredRelays.size) + if (eoseReceivedCount >= minEoseCount && !contactListLoaded) { + contactListLoaded = true } }, ) @@ -216,15 +236,19 @@ fun UserProfileScreen( Button( onClick = { scope.launch { + val currentStatus = followState.currentStatusOrNull() + followState.setFollowLoading() try { - val currentStatus = followState.currentStatusOrNull() val updatedEvent = if (currentStatus?.isFollowing == true) { - unfollowUser(pubKeyHex, account, relayManager, currentStatus.contactList) + unfollowUser(pubKeyHex, account, relayManager, myContactList) } else { - followUser(pubKeyHex, account, relayManager, currentStatus?.contactList) + followUser(pubKeyHex, account, relayManager, myContactList) } + + // Update both stored contact list and followState + myContactList = updatedEvent followState.setFollowSuccess(updatedEvent, pubKeyHex) } catch (e: Exception) { e.printStackTrace() @@ -232,28 +256,40 @@ fun UserProfileScreen( } } }, - enabled = followState.state.value !is com.vitorpamplona.amethyst.commons.state.LoadingState.Loading, + enabled = contactListLoaded && followState.state.value !is com.vitorpamplona.amethyst.commons.state.LoadingState.Loading, ) { val state = followState.state.collectAsState().value val isFollowing = (state as? com.vitorpamplona.amethyst.commons.state.LoadingState.Success)?.data?.isFollowing ?: false val isLoading = state is com.vitorpamplona.amethyst.commons.state.LoadingState.Loading - if (isLoading) { - androidx.compose.material3.CircularProgressIndicator( - modifier = Modifier.size(16.dp), - strokeWidth = 2.dp, - color = MaterialTheme.colorScheme.onPrimary, - ) - Spacer(Modifier.width(8.dp)) - Text(if (isFollowing) "Unfollowing..." else "Following...") - } else { - Icon( - if (isFollowing) Icons.Default.PersonRemove else Icons.Default.PersonAdd, - contentDescription = if (isFollowing) "Unfollow" else "Follow", - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.width(8.dp)) - Text(if (isFollowing) "Unfollow" else "Follow") + when { + !contactListLoaded -> { + androidx.compose.material3.CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.onPrimary, + ) + Spacer(Modifier.width(8.dp)) + Text("Loading...") + } + isLoading -> { + androidx.compose.material3.CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.onPrimary, + ) + Spacer(Modifier.width(8.dp)) + Text(if (isFollowing) "Unfollowing..." else "Following...") + } + else -> { + Icon( + if (isFollowing) Icons.Default.PersonRemove else Icons.Default.PersonAdd, + contentDescription = if (isFollowing) "Unfollow" else "Follow", + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.width(8.dp)) + Text(if (isFollowing) "Unfollow" else "Follow") + } } } @@ -460,14 +496,15 @@ private suspend fun followUser( currentContactList: ContactListEvent?, ): ContactListEvent = withContext(Dispatchers.IO) { - println("[UserProfile] Starting followUser: target=${pubKeyHex.take(8)}...") - // Use shared FollowAction from commons val updatedEvent = FollowAction.follow(pubKeyHex, account.signer, currentContactList) - println("[UserProfile] ContactListEvent created, broadcasting...") + val connectedRelays = relayManager.connectedRelays.value + if (connectedRelays.isEmpty()) { + throw IllegalStateException("Cannot follow: No connected relays") + } + relayManager.broadcastToAll(updatedEvent) - println("[UserProfile] Follow broadcast complete") updatedEvent } @@ -482,21 +519,19 @@ private suspend fun unfollowUser( currentContactList: ContactListEvent?, ): ContactListEvent = withContext(Dispatchers.IO) { - println("[UserProfile] Starting unfollowUser: target=${pubKeyHex.take(8)}...") - if (currentContactList != null) { - println("[UserProfile] Removing from existing contact list") - // Use shared FollowAction from commons val updatedEvent = FollowAction.unfollow(pubKeyHex, account.signer, currentContactList) - println("[UserProfile] ContactListEvent updated, broadcasting...") + val connectedRelays = relayManager.connectedRelays.value + if (connectedRelays.isEmpty()) { + throw IllegalStateException("Cannot unfollow: No connected relays") + } + relayManager.broadcastToAll(updatedEvent) - println("[UserProfile] Unfollow broadcast complete") updatedEvent } else { - println("[UserProfile] Error: No contact list to unfollow from") throw IllegalStateException("Cannot unfollow: No contact list available") } } From 7d3ac9ec8ad96ac568578a73f31ac4a6b84b9ad9 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Mon, 5 Jan 2026 13:43:00 +0200 Subject: [PATCH 067/137] add avatars --- commons/build.gradle.kts | 4 + .../commons/ui/components/UserAvatar.kt | 114 ++++++++++++++++++ .../commons/ui/theme/ThemeExtensions.kt | 38 ++++++ .../amethyst/commons/ui/note/NoteCard.kt | 31 +++-- .../amethyst/desktop/ui/UserProfileScreen.kt | 38 +++--- 5 files changed, 198 insertions(+), 27 deletions(-) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UserAvatar.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/theme/ThemeExtensions.kt diff --git a/commons/build.gradle.kts b/commons/build.gradle.kts index 095072fe3..d93e36172 100644 --- a/commons/build.gradle.kts +++ b/commons/build.gradle.kts @@ -66,6 +66,10 @@ kotlin { implementation(compose.materialIconsExtended) implementation(compose.components.uiToolingPreview) + // Image loading (Coil 3 - KMP) + implementation(libs.coil.compose) + implementation(libs.coil.okhttp) + // LruCache (KMP-ready) implementation(libs.androidx.collection) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UserAvatar.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UserAvatar.kt new file mode 100644 index 000000000..02478adeb --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UserAvatar.kt @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.ui.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Face +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.DefaultAlpha +import androidx.compose.ui.graphics.FilterQuality +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.Dp +import coil3.compose.AsyncImage +import com.vitorpamplona.amethyst.commons.robohash.CachedRobohash +import com.vitorpamplona.amethyst.commons.ui.theme.isLight +import com.vitorpamplona.amethyst.commons.ui.theme.onBackgroundColorFilter + +/** + * Shared avatar component that displays a user's profile picture with Robohash fallback. + * + * @param userHex The user's public key hex (used for Robohash generation) + * @param pictureUrl Optional URL to the user's profile picture + * @param size Size of the avatar (both width and height) + * @param modifier Additional modifiers to apply + * @param contentDescription Accessibility description + * @param loadProfilePicture Whether to load the profile picture (false = show robohash only) + * @param loadRobohash Whether to generate robohash (false = show generic icon) + */ +@Composable +fun UserAvatar( + userHex: String, + pictureUrl: String?, + size: Dp, + modifier: Modifier = Modifier, + contentDescription: String? = null, + loadProfilePicture: Boolean = true, + loadRobohash: Boolean = true, +) { + val avatarModifier = remember(size) { + modifier.clip(shape = CircleShape) + } + + if (pictureUrl != null && loadProfilePicture) { + // Show profile picture with robohash/icon as fallback + val fallbackPainter = + if (loadRobohash) { + rememberVectorPainter( + image = CachedRobohash.get(userHex, MaterialTheme.colorScheme.isLight), + ) + } else { + rememberVectorPainter( + image = Icons.Default.Face, + ) + } + + AsyncImage( + model = pictureUrl, + contentDescription = contentDescription, + modifier = avatarModifier, + placeholder = fallbackPainter, + fallback = fallbackPainter, + error = fallbackPainter, + alignment = Alignment.Center, + contentScale = ContentScale.Crop, + alpha = DefaultAlpha, + colorFilter = null, + filterQuality = DrawScope.DefaultFilterQuality, + ) + } else if (loadRobohash) { + // Show robohash only + Image( + imageVector = CachedRobohash.get(userHex, MaterialTheme.colorScheme.isLight), + contentDescription = contentDescription, + modifier = avatarModifier, + contentScale = ContentScale.Crop, + ) + } else { + // Show generic icon + Image( + imageVector = Icons.Default.Face, + contentDescription = contentDescription, + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground), + modifier = avatarModifier, + contentScale = ContentScale.Crop, + ) + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/theme/ThemeExtensions.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/theme/ThemeExtensions.kt new file mode 100644 index 000000000..543b3861c --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/theme/ThemeExtensions.kt @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.ui.theme + +import androidx.compose.material3.ColorScheme +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.luminance + +/** + * Determines if the color scheme is light mode. + * Based on primary color luminance. + */ +val ColorScheme.isLight: Boolean + get() = primary.luminance() < 0.5f + +/** + * Color filter for onBackground color (for tinting icons/images). + */ +val ColorScheme.onBackgroundColorFilter: ColorFilter + get() = ColorFilter.tint(onBackground) diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCard.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCard.kt index 7a05268b2..304929af4 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCard.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCard.kt @@ -28,6 +28,8 @@ 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.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.HorizontalDivider @@ -44,6 +46,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.commons.ui.components.UserAvatar import com.vitorpamplona.amethyst.commons.util.toTimeAgo /** @@ -53,6 +56,7 @@ data class NoteDisplayData( val id: String, val pubKeyHex: String, val pubKeyDisplay: String, + val profilePictureUrl: String? = null, val content: String, val createdAt: Long, ) @@ -85,19 +89,32 @@ fun NoteCard( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - // Author (truncated, clickable) - Text( - text = note.pubKeyDisplay.take(20) + if (note.pubKeyDisplay.length > 20) "..." else "", - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.primary, - maxLines = 1, + // Author with avatar + Row( + verticalAlignment = Alignment.CenterVertically, modifier = if (onAuthorClick != null) { Modifier.clickable { onAuthorClick(note.pubKeyHex) } } else { Modifier }, - ) + ) { + UserAvatar( + userHex = note.pubKeyHex, + pictureUrl = note.profilePictureUrl, + size = 32.dp, + contentDescription = "Profile picture of ${note.pubKeyDisplay}", + ) + + Spacer(Modifier.width(8.dp)) + + Text( + text = note.pubKeyDisplay.take(20) + if (note.pubKeyDisplay.length > 20) "..." else "", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary, + maxLines = 1, + ) + } // Timestamp Text( diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt index 9dc55d75a..c3dc70722 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt @@ -33,7 +33,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.PersonAdd @@ -45,7 +44,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -56,7 +54,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.account.AccountState @@ -68,6 +65,7 @@ import com.vitorpamplona.amethyst.commons.subscriptions.createMetadataSubscripti import com.vitorpamplona.amethyst.commons.subscriptions.createUserPostsSubscription import com.vitorpamplona.amethyst.commons.subscriptions.rememberSubscription import com.vitorpamplona.amethyst.commons.ui.components.LoadingState +import com.vitorpamplona.amethyst.commons.ui.components.UserAvatar import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull @@ -321,23 +319,23 @@ fun UserProfileScreen( containerColor = MaterialTheme.colorScheme.surfaceVariant, ), ) { - Column(modifier = Modifier.padding(24.dp)) { + Column(modifier = Modifier.padding(16.dp)) { Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.Top, ) { - // Profile picture placeholder - Surface( - modifier = Modifier.size(80.dp).clip(CircleShape), - color = MaterialTheme.colorScheme.primary, - ) { - // TODO: Load actual image from picture URL - } + // Profile picture with robohash fallback + UserAvatar( + userHex = pubKeyHex, + pictureUrl = picture, + size = 56.dp, + contentDescription = "Profile picture", + ) Column(modifier = Modifier.weight(1f)) { Text( displayName ?: (pubKeyHex.hexToByteArrayOrNull()?.toNpub()?.take(20) ?: pubKeyHex.take(20)), - style = MaterialTheme.typography.headlineSmall, + style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, ) Spacer(Modifier.height(4.dp)) @@ -350,7 +348,7 @@ fun UserProfileScreen( } if (about != null) { - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(12.dp)) Text( about!!, style = MaterialTheme.typography.bodyMedium, @@ -358,13 +356,13 @@ fun UserProfileScreen( ) } - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(12.dp)) - Row(horizontalArrangement = Arrangement.spacedBy(24.dp)) { + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { Column { Text( "$followersCount", - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.Bold, ) Text( @@ -376,7 +374,7 @@ fun UserProfileScreen( Column { Text( "$followingCount", - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.Bold, ) Text( @@ -389,12 +387,12 @@ fun UserProfileScreen( } } - Spacer(Modifier.height(24.dp)) + Spacer(Modifier.height(16.dp)) // User's posts Text( "Posts", - style = MaterialTheme.typography.titleLarge, + style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(bottom = 8.dp), ) From 2079493fc4ef82e4247eb67525a7e028632a8b07 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Mon, 5 Jan 2026 13:43:54 +0200 Subject: [PATCH 068/137] spotless --- .../amethyst/commons/ui/components/UserAvatar.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UserAvatar.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UserAvatar.kt index 02478adeb..fc9ef4ddc 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UserAvatar.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UserAvatar.kt @@ -32,7 +32,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.DefaultAlpha -import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.layout.ContentScale @@ -40,7 +39,6 @@ import androidx.compose.ui.unit.Dp import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.commons.robohash.CachedRobohash import com.vitorpamplona.amethyst.commons.ui.theme.isLight -import com.vitorpamplona.amethyst.commons.ui.theme.onBackgroundColorFilter /** * Shared avatar component that displays a user's profile picture with Robohash fallback. @@ -63,9 +61,10 @@ fun UserAvatar( loadProfilePicture: Boolean = true, loadRobohash: Boolean = true, ) { - val avatarModifier = remember(size) { - modifier.clip(shape = CircleShape) - } + val avatarModifier = + remember(size) { + modifier.clip(shape = CircleShape) + } if (pictureUrl != null && loadProfilePicture) { // Show profile picture with robohash/icon as fallback From 2fc7e505a39a83a4d21b2ec96a0d95657fe00483 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 5 Jan 2026 09:36:46 -0500 Subject: [PATCH 069/137] Update stability analiser --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 315e587d3..cbcc6baca 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -169,5 +169,5 @@ serialization = { id = 'org.jetbrains.kotlin.plugin.serialization', version.ref kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } androidKotlinMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" } vanniktech-mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" } -stability-analyzer = { id = "com.github.skydoves.compose.stability.analyzer", version = "0.6.1" } +stability-analyzer = { id = "com.github.skydoves.compose.stability.analyzer", version = "0.6.6" } composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" } From a9df6fcc61f13c60d11a8769e3f096567e31ecc5 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 5 Jan 2026 13:38:24 -0500 Subject: [PATCH 070/137] Adds the default bookmark option to be inside the bookmark list screen --- .../vitorpamplona/amethyst/model/Account.kt | 9 ++ .../model/nip51Lists/BookmarkListState.kt | 25 ++++ .../amethyst/ui/navigation/AppNavigation.kt | 2 +- .../ui/navigation/drawer/DrawerContent.kt | 9 -- .../default}/BookmarkListScreen.kt | 8 +- .../default}/dal/BookmarkPrivateFeedFilter.kt | 2 +- .../dal/BookmarkPrivateFeedViewModel.kt | 2 +- .../default}/dal/BookmarkPublicFeedFilter.kt | 2 +- .../dal/BookmarkPublicFeedViewModel.kt | 2 +- .../display/BookmarkGroupScreen.kt | 4 +- .../list/ListOfBookmarkGroupsFeedView.kt | 108 ++++++++++----- .../list/ListOfBookmarkGroupsScreen.kt | 7 + .../list/NewBookmarkGroupCreationDialog.kt | 105 --------------- .../ArticleBookmarkListManagementScreen.kt | 125 ++++++++++------- .../PostBookmarkListManagementScreen.kt | 127 +++++++++++------- .../profile/bookmarks/BookmarkTabHeader.kt | 2 +- amethyst/src/main/res/values/strings.xml | 2 + .../bookmarkList/BookmarkListEvent.kt | 15 +++ 18 files changed, 307 insertions(+), 249 deletions(-) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/{bookmarks => bookmarkgroups/default}/BookmarkListScreen.kt (94%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/{bookmarks => bookmarkgroups/default}/dal/BookmarkPrivateFeedFilter.kt (95%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/{bookmarks => bookmarkgroups/default}/dal/BookmarkPrivateFeedViewModel.kt (95%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/{bookmarks => bookmarkgroups/default}/dal/BookmarkPublicFeedFilter.kt (95%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/{bookmarks => bookmarkgroups/default}/dal/BookmarkPublicFeedViewModel.kt (95%) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/NewBookmarkGroupCreationDialog.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index c38e80510..a3aed3278 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1554,6 +1554,15 @@ class Account( } } + suspend fun removeBookmark(note: Note) { + if (!isWriteable() || note.isDraft()) return + + val event = bookmarkState.removeBookmark(note) + if (event != null) { + sendMyPublicAndPrivateOutbox(event) + } + } + suspend fun createAuthEvent( relay: NormalizedRelayUrl, challenge: String, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BookmarkListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BookmarkListState.kt index 6606229ec..19b2e1303 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BookmarkListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BookmarkListState.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists +import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note @@ -40,7 +41,9 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn +import java.lang.reflect.Modifier.isPrivate +@Stable class BookmarkListState( val signer: NostrSigner, val cache: LocalCache, @@ -274,4 +277,26 @@ class BookmarkListState( null } } + + suspend fun removeBookmark(note: Note): BookmarkListEvent? { + val bookmarkList = getBookmarkList() + + return if (bookmarkList != null) { + if (note is AddressableNote) { + BookmarkListEvent.remove( + earlierVersion = bookmarkList, + bookmarkIdTag = AddressBookmark(note.address, note.relayHintUrl()), + signer = signer, + ) + } else { + BookmarkListEvent.remove( + earlierVersion = bookmarkList, + bookmarkIdTag = EventBookmark(note.idHex, note.relayHintUrl()), + signer = signer, + ) + } + } else { + null + } + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 30d7e9a14..9266e299e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -58,12 +58,12 @@ import com.vitorpamplona.amethyst.ui.note.nip22Comments.ReplyCommentPostScreen import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountSwitcherAndLeftDrawerLayout import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.default.BookmarkListScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.display.BookmarkGroupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.list.ListOfBookmarkGroupsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.list.metadata.BookmarkGroupMetadataScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.membershipManagement.ArticleBookmarkListManagementScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.membershipManagement.PostBookmarkListManagementScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.BookmarkListScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomByAuthorScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.NewGroupDMScreen diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt index fb16001a6..3ae138470 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt @@ -46,7 +46,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Send import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.outlined.BookmarkBorder import androidx.compose.material.icons.outlined.CloudUpload import androidx.compose.material.icons.outlined.CollectionsBookmark import androidx.compose.material.icons.outlined.Drafts @@ -458,14 +457,6 @@ fun ListContent( NavigationRow( title = R.string.bookmarks, - icon = Icons.Outlined.BookmarkBorder, - tint = MaterialTheme.colorScheme.onBackground, - nav = nav, - route = Route.Bookmarks, - ) - - NavigationRow( - title = R.string.bookmark_lists, icon = Icons.Outlined.CollectionsBookmark, tint = MaterialTheme.colorScheme.onBackground, nav = nav, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/BookmarkListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/BookmarkListScreen.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/BookmarkListScreen.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/BookmarkListScreen.kt index ed958bead..111fc3876 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/BookmarkListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/BookmarkListScreen.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks +package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.default import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Column @@ -44,8 +44,8 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.dal.BookmarkPrivateFeedViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.dal.BookmarkPublicFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.default.dal.BookmarkPrivateFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.default.dal.BookmarkPublicFeedViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.TabRowHeight import kotlinx.coroutines.launch @@ -93,7 +93,7 @@ private fun RenderBookmarkScreen( isInvertedLayout = false, topBar = { Column { - TopBarWithBackButton(stringRes(id = R.string.bookmarks), nav::popBack) + TopBarWithBackButton(stringRes(id = R.string.bookmarks_title), nav::popBack) TabRow( containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.onBackground, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPrivateFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/dal/BookmarkPrivateFeedFilter.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPrivateFeedFilter.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/dal/BookmarkPrivateFeedFilter.kt index 85cdc1d77..ad15db9ac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPrivateFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/dal/BookmarkPrivateFeedFilter.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.dal +package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.default.dal import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPrivateFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/dal/BookmarkPrivateFeedViewModel.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPrivateFeedViewModel.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/dal/BookmarkPrivateFeedViewModel.kt index 3ddbd8e09..a0df51e0d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPrivateFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/dal/BookmarkPrivateFeedViewModel.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.dal +package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.default.dal import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPublicFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/dal/BookmarkPublicFeedFilter.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPublicFeedFilter.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/dal/BookmarkPublicFeedFilter.kt index 34a3962fb..e6e1ab1d8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPublicFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/dal/BookmarkPublicFeedFilter.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.dal +package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.default.dal import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPublicFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/dal/BookmarkPublicFeedViewModel.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPublicFeedViewModel.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/dal/BookmarkPublicFeedViewModel.kt index fb8ec6c8e..96b71053e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPublicFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/dal/BookmarkPublicFeedViewModel.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.dal +package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.default.dal import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupScreen.kt index e4ca1fc88..b4d21b5b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupScreen.kt @@ -42,7 +42,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Tab import androidx.compose.material3.TabRow import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -59,6 +58,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.ShorterTopAppBar import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -118,7 +118,7 @@ fun BookmarkGroupScreenView( Scaffold( topBar = { Column { - TopAppBar( + ShorterTopAppBar( title = { TitleAndDescription(bookmarkGroupViewModel) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsFeedView.kt index 9e603d023..dba0abacc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsFeedView.kt @@ -20,34 +20,43 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.list +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.BookmarkBorder import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.nip51Lists.BookmarkListState import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.BookmarkType import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.FeedPadding -import com.vitorpamplona.amethyst.ui.theme.Size40dp +import com.vitorpamplona.amethyst.ui.theme.Size40Modifier import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import kotlinx.coroutines.flow.StateFlow @Composable fun ListOfBookmarkGroupsFeedView( + defaultBookmarks: BookmarkListState, groupListFeedSource: StateFlow>, + openDefaultBookmarks: () -> Unit, onOpenItem: (String, BookmarkType) -> Unit, onRenameItem: (targetBookmarkGroup: LabeledBookmarkList) -> Unit, onItemDescriptionChange: (bookmarkGroup: LabeledBookmarkList) -> Unit, @@ -56,40 +65,73 @@ fun ListOfBookmarkGroupsFeedView( ) { val bookmarkGroupFeedState by groupListFeedSource.collectAsStateWithLifecycle() - if (bookmarkGroupFeedState.isEmpty()) { - BookmarkGroupsFeedEmpty(message = stringRes(R.string.bookmark_list_feed_empty_msg)) - } else { - LazyColumn( - state = rememberLazyListState(), - contentPadding = FeedPadding, - ) { - itemsIndexed( - bookmarkGroupFeedState, - key = { _: Int, item: LabeledBookmarkList -> item.identifier }, - ) { _, groupItem -> - BookmarkGroupItem( - modifier = Modifier.fillMaxSize().animateItem(), - bookmarkList = groupItem, - onClick = { bookmarkType -> onOpenItem(groupItem.identifier, bookmarkType) }, - onRename = { onRenameItem(groupItem) }, - onDescriptionChange = { onItemDescriptionChange(groupItem) }, - onClone = { cloneName, cloneDescription -> onItemClone(groupItem, cloneName, cloneDescription) }, - onDelete = { onDeleteItem(groupItem) }, - ) - HorizontalDivider(thickness = DividerThickness) - } + LazyColumn( + state = rememberLazyListState(), + contentPadding = FeedPadding, + ) { + item { + DefaultBookmarkList(defaultBookmarks, openDefaultBookmarks) + HorizontalDivider(thickness = DividerThickness) + } + + itemsIndexed( + bookmarkGroupFeedState, + key = { _: Int, item: LabeledBookmarkList -> item.identifier }, + ) { _, groupItem -> + BookmarkGroupItem( + modifier = Modifier.fillMaxSize().animateItem(), + bookmarkList = groupItem, + onClick = { bookmarkType -> onOpenItem(groupItem.identifier, bookmarkType) }, + onRename = { onRenameItem(groupItem) }, + onDescriptionChange = { onItemDescriptionChange(groupItem) }, + onClone = { cloneName, cloneDescription -> onItemClone(groupItem, cloneName, cloneDescription) }, + onDelete = { onDeleteItem(groupItem) }, + ) + HorizontalDivider(thickness = DividerThickness) } } } @Composable -fun BookmarkGroupsFeedEmpty(message: String = stringRes(R.string.feed_is_empty)) { - Column( - Modifier.fillMaxSize().padding(horizontal = Size40dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Text(message) - Spacer(modifier = StdVertSpacer) - } +fun DefaultBookmarkList( + defaultBookmarks: BookmarkListState, + openDefaultBookmarks: () -> Unit, +) { + val bookmarkState by defaultBookmarks.bookmarks.collectAsStateWithLifecycle() + + ListItem( + modifier = Modifier.clickable(onClick = openDefaultBookmarks), + headlineContent = { + Text(stringRes(R.string.bookmarks_title), maxLines = 1, overflow = TextOverflow.Ellipsis) + }, + supportingContent = { + Column( + modifier = Modifier.fillMaxWidth(), + ) { + Text( + stringRes(R.string.bookmarks_explainer), + overflow = TextOverflow.Ellipsis, + maxLines = 2, + ) + } + }, + leadingContent = { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + imageVector = Icons.Outlined.BookmarkBorder, + contentDescription = stringRes(R.string.bookmark_list_icon_label), + modifier = Size40Modifier, + ) + Spacer(StdVertSpacer) + BookmarkMembershipStatusAndNumberDisplay( + modifier = Modifier.align(Alignment.CenterHorizontally), + postBookmarksSize = bookmarkState.public.size + bookmarkState.private.size, + articleBookmarksSize = 0, + ) + } + }, + ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsScreen.kt index 8903b1b48..6e5ad7581 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsScreen.kt @@ -34,6 +34,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.nip51Lists.BookmarkListState import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route @@ -49,7 +50,9 @@ fun ListOfBookmarkGroupsScreen( nav: INav, ) { ListOfBookmarkGroupsFeed( + defaultBookmarks = accountViewModel.account.bookmarkState, listSource = accountViewModel.account.labeledBookmarkLists.listFeedFlow, + openDefaultBookmarks = { nav.nav(Route.Bookmarks) }, addBookmarkGroup = { nav.nav(Route.BookmarkGroupMetadataEdit()) }, openBookmarkGroup = { identifier, bookmarkType -> nav.nav(Route.BookmarkGroupView(identifier, bookmarkType)) @@ -84,7 +87,9 @@ fun ListOfBookmarkGroupsScreen( @Composable fun ListOfBookmarkGroupsFeed( + defaultBookmarks: BookmarkListState, listSource: StateFlow>, + openDefaultBookmarks: () -> Unit, addBookmarkGroup: () -> Unit, openBookmarkGroup: (identifier: String, bookmarkType: BookmarkType) -> Unit, renameBookmarkGroup: (bookmarkGroup: LabeledBookmarkList) -> Unit, @@ -109,7 +114,9 @@ fun ListOfBookmarkGroupsFeed( ).fillMaxHeight(), ) { ListOfBookmarkGroupsFeedView( + defaultBookmarks = defaultBookmarks, groupListFeedSource = listSource, + openDefaultBookmarks = openDefaultBookmarks, onOpenItem = openBookmarkGroup, onRenameItem = renameBookmarkGroup, onItemDescriptionChange = changeBookmarkGroupDescription, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/NewBookmarkGroupCreationDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/NewBookmarkGroupCreationDialog.kt deleted file mode 100644 index 2f8680c6e..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/NewBookmarkGroupCreationDialog.kt +++ /dev/null @@ -1,105 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.list - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer - -@Composable -fun NewBookmarkGroupCreationDialog( - modifier: Modifier = Modifier, - onDismiss: () -> Unit, - onCreateGroup: (name: String, description: String?) -> Unit, -) { - val newGroupName = remember { mutableStateOf("") } - val newGroupDescription = remember { mutableStateOf(null) } - - AlertDialog( - modifier = modifier, - onDismissRequest = onDismiss, - title = { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = "New Bookmark Group", - ) - } - }, - text = { - Column( - verticalArrangement = Arrangement.spacedBy(5.dp), - ) { - // For the new bookmark group name - TextField( - value = newGroupName.value, - onValueChange = { newGroupName.value = it }, - label = { - Text(text = "Group name") - }, - ) - Spacer(modifier = DoubleVertSpacer) - // For the group description - TextField( - value = - (if (newGroupDescription.value != null) newGroupDescription.value else "").toString(), - onValueChange = { newGroupDescription.value = it }, - label = { - Text(text = "Group description(optional)") - }, - ) - } - }, - confirmButton = { - Button( - onClick = { - onCreateGroup(newGroupName.value, newGroupDescription.value) - onDismiss() - }, - ) { - Text("Create Group") - } - }, - dismissButton = { - Button( - onClick = onDismiss, - ) { - Text(stringRes(R.string.cancel)) - } - }, - ) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/ArticleBookmarkListManagementScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/ArticleBookmarkListManagementScreen.kt index ec36b977e..a5614fdf9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/ArticleBookmarkListManagementScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/ArticleBookmarkListManagementScreen.kt @@ -44,7 +44,6 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.BookmarkType -import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.list.BookmarkGroupsFeedEmpty import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.NewListButton import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.core.Address @@ -75,8 +74,6 @@ private fun ListManagementView( accountViewModel: AccountViewModel, nav: INav, ) { - val bookmarkGroups by accountViewModel.account.labeledBookmarkLists.listFeedFlow - .collectAsStateWithLifecycle() Scaffold( modifier = modifier, topBar = { @@ -95,48 +92,86 @@ private fun ListManagementView( ).consumeWindowInsets(contentPadding) .imePadding(), ) { - if (bookmarkGroups.isEmpty()) { - BookmarkGroupsFeedEmpty(message = stringRes(R.string.bookmark_list_feed_empty_msg)) - } else { - LazyColumn( - state = rememberLazyListState(), - modifier = Modifier.fillMaxWidth(), - ) { - itemsIndexed(items = bookmarkGroups, key = { _: Int, item: LabeledBookmarkList -> item.identifier }) { _, bookmarkList -> - val maybePublicBookmark = bookmarkList.publicArticleBookmarks.firstOrNull { it.address == note.address } - val maybePrivateBookmark = bookmarkList.privateArticleBookmarks.firstOrNull { it.address == note.address } - BookmarkGroupManagementItem( - modifier = Modifier.fillMaxWidth().animateItem(), - listTitle = bookmarkList.title, - isPublicMemberBookmark = maybePublicBookmark != null, - isPrivateMemberBookmark = maybePrivateBookmark != null, - totalPostBookmarkSize = bookmarkList.publicPostBookmarks.size + bookmarkList.privatePostBookmarks.size, - totalArticleBookmarkSize = bookmarkList.publicArticleBookmarks.size + bookmarkList.privateArticleBookmarks.size, - onClick = { nav.nav(Route.BookmarkGroupView(bookmarkList.identifier, BookmarkType.ArticleBookmark)) }, - onAddBookmarkToGroup = { shouldBePrivate -> - accountViewModel.launchSigner { - accountViewModel.account.labeledBookmarkLists.addBookmarkToList( - bookmark = AddressBookmark(address = note.address, relayHint = note.relayHintUrl()), - bookmarkListIdentifier = bookmarkList.identifier, - isBookmarkPrivate = shouldBePrivate, - account = accountViewModel.account, - ) - } - }, - onRemoveBookmarkFromGroup = { - accountViewModel.launchSigner { - accountViewModel.account.labeledBookmarkLists.removeBookmarkFromList( - bookmark = AddressBookmark(address = note.address), - bookmarkListIdentifier = bookmarkList.identifier, - isBookmarkPrivate = maybePrivateBookmark != null, - account = accountViewModel.account, - ) - } - }, - ) - } - } - } + ListManagementViewBody(note, accountViewModel, nav) + } + } +} + +@Composable +private fun ListManagementViewBody( + note: AddressableNote, + accountViewModel: AccountViewModel, + nav: INav, +) { + val bookmarkGroups by accountViewModel.account.labeledBookmarkLists.listFeedFlow + .collectAsStateWithLifecycle() + + val defaultBookmarks by accountViewModel.account.bookmarkState.bookmarks + .collectAsStateWithLifecycle() + + LazyColumn( + state = rememberLazyListState(), + modifier = Modifier.fillMaxWidth(), + ) { + item { + val maybePublicBookmark = defaultBookmarks.public.contains(note) + val maybePrivateBookmark = defaultBookmarks.private.contains(note) + + BookmarkGroupManagementItem( + modifier = Modifier.fillMaxWidth().animateItem(), + listTitle = stringRes(R.string.bookmarks_title), + isPublicMemberBookmark = maybePublicBookmark, + isPrivateMemberBookmark = maybePrivateBookmark, + totalPostBookmarkSize = defaultBookmarks.public.size + defaultBookmarks.private.size, + totalArticleBookmarkSize = 0, + onClick = { + nav.nav(Route.Bookmarks) + }, + onAddBookmarkToGroup = { shouldBePrivate -> + accountViewModel.launchSigner { + accountViewModel.account.addBookmark(note, shouldBePrivate) + } + }, + onRemoveBookmarkFromGroup = { + accountViewModel.launchSigner { + accountViewModel.account.removeBookmark(note) + } + }, + ) + } + + itemsIndexed(items = bookmarkGroups, key = { _: Int, item: LabeledBookmarkList -> item.identifier }) { _, bookmarkList -> + val maybePublicBookmark = bookmarkList.publicArticleBookmarks.firstOrNull { it.address == note.address } + val maybePrivateBookmark = bookmarkList.privateArticleBookmarks.firstOrNull { it.address == note.address } + BookmarkGroupManagementItem( + modifier = Modifier.fillMaxWidth().animateItem(), + listTitle = bookmarkList.title, + isPublicMemberBookmark = maybePublicBookmark != null, + isPrivateMemberBookmark = maybePrivateBookmark != null, + totalPostBookmarkSize = bookmarkList.publicPostBookmarks.size + bookmarkList.privatePostBookmarks.size, + totalArticleBookmarkSize = bookmarkList.publicArticleBookmarks.size + bookmarkList.privateArticleBookmarks.size, + onClick = { nav.nav(Route.BookmarkGroupView(bookmarkList.identifier, BookmarkType.ArticleBookmark)) }, + onAddBookmarkToGroup = { shouldBePrivate -> + accountViewModel.launchSigner { + accountViewModel.account.labeledBookmarkLists.addBookmarkToList( + bookmark = AddressBookmark(address = note.address, relayHint = note.relayHintUrl()), + bookmarkListIdentifier = bookmarkList.identifier, + isBookmarkPrivate = shouldBePrivate, + account = accountViewModel.account, + ) + } + }, + onRemoveBookmarkFromGroup = { + accountViewModel.launchSigner { + accountViewModel.account.labeledBookmarkLists.removeBookmarkFromList( + bookmark = AddressBookmark(address = note.address), + bookmarkListIdentifier = bookmarkList.identifier, + isBookmarkPrivate = maybePrivateBookmark != null, + account = accountViewModel.account, + ) + } + }, + ) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt index 14bb6478d..3ee8966aa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt @@ -44,7 +44,8 @@ import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.BookmarkType -import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.list.BookmarkGroupsFeedEmpty +import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.membershipManagement.ListManagementView +import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.membershipManagement.ListManagementViewBody import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.NewListButton import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark @@ -74,8 +75,6 @@ private fun ListManagementView( accountViewModel: AccountViewModel, nav: INav, ) { - val bookmarkGroups by accountViewModel.account.labeledBookmarkLists.listFeedFlow - .collectAsStateWithLifecycle() Scaffold( modifier = modifier, topBar = { @@ -94,48 +93,86 @@ private fun ListManagementView( ).consumeWindowInsets(contentPadding) .imePadding(), ) { - if (bookmarkGroups.isEmpty()) { - BookmarkGroupsFeedEmpty(message = stringRes(R.string.bookmark_list_feed_empty_msg)) - } else { - LazyColumn( - state = rememberLazyListState(), - modifier = Modifier.fillMaxWidth(), - ) { - itemsIndexed(items = bookmarkGroups, key = { _: Int, item: LabeledBookmarkList -> item.identifier }) { _, bookmarkList -> - val maybePublicBookmark = bookmarkList.publicPostBookmarks.firstOrNull { it.eventId == note.idHex } - val maybePrivateBookmark = bookmarkList.privatePostBookmarks.firstOrNull { it.eventId == note.idHex } - BookmarkGroupManagementItem( - modifier = Modifier.fillMaxWidth().animateItem(), - listTitle = bookmarkList.title, - isPublicMemberBookmark = maybePublicBookmark != null, - isPrivateMemberBookmark = maybePrivateBookmark != null, - totalPostBookmarkSize = bookmarkList.publicPostBookmarks.size + bookmarkList.privatePostBookmarks.size, - totalArticleBookmarkSize = bookmarkList.publicArticleBookmarks.size + bookmarkList.privateArticleBookmarks.size, - onClick = { nav.nav(Route.BookmarkGroupView(bookmarkList.identifier, BookmarkType.PostBookmark)) }, - onAddBookmarkToGroup = { shouldBePrivate -> - accountViewModel.launchSigner { - accountViewModel.account.labeledBookmarkLists.addBookmarkToList( - bookmark = EventBookmark(eventId = note.idHex, relay = note.relayHintUrl(), author = note.author?.pubkeyHex), - bookmarkListIdentifier = bookmarkList.identifier, - isBookmarkPrivate = shouldBePrivate, - account = accountViewModel.account, - ) - } - }, - onRemoveBookmarkFromGroup = { - accountViewModel.launchSigner { - accountViewModel.account.labeledBookmarkLists.removeBookmarkFromList( - bookmark = EventBookmark(eventId = note.idHex), - bookmarkListIdentifier = bookmarkList.identifier, - isBookmarkPrivate = maybePrivateBookmark != null, - account = accountViewModel.account, - ) - } - }, - ) - } - } - } + ListManagementViewBody(note, accountViewModel, nav) + } + } +} + +@Composable +private fun ListManagementViewBody( + note: Note, + accountViewModel: AccountViewModel, + nav: INav, +) { + val bookmarkGroups by accountViewModel.account.labeledBookmarkLists.listFeedFlow + .collectAsStateWithLifecycle() + + val defaultBookmarks by accountViewModel.account.bookmarkState.bookmarks + .collectAsStateWithLifecycle() + + LazyColumn( + state = rememberLazyListState(), + modifier = Modifier.fillMaxWidth(), + ) { + item { + val maybePublicBookmark = defaultBookmarks.public.contains(note) + val maybePrivateBookmark = defaultBookmarks.private.contains(note) + + BookmarkGroupManagementItem( + modifier = Modifier.fillMaxWidth().animateItem(), + listTitle = stringRes(R.string.bookmarks_title), + isPublicMemberBookmark = maybePublicBookmark, + isPrivateMemberBookmark = maybePrivateBookmark, + totalPostBookmarkSize = defaultBookmarks.public.size + defaultBookmarks.private.size, + totalArticleBookmarkSize = 0, + onClick = { + nav.nav(Route.Bookmarks) + }, + onAddBookmarkToGroup = { shouldBePrivate -> + accountViewModel.launchSigner { + accountViewModel.account.addBookmark(note, shouldBePrivate) + } + }, + onRemoveBookmarkFromGroup = { + accountViewModel.launchSigner { + accountViewModel.account.removeBookmark(note) + } + }, + ) + } + + itemsIndexed(items = bookmarkGroups, key = { _: Int, item: LabeledBookmarkList -> item.identifier }) { _, bookmarkList -> + val maybePublicBookmark = bookmarkList.publicPostBookmarks.firstOrNull { it.eventId == note.idHex } + val maybePrivateBookmark = bookmarkList.privatePostBookmarks.firstOrNull { it.eventId == note.idHex } + BookmarkGroupManagementItem( + modifier = Modifier.fillMaxWidth().animateItem(), + listTitle = bookmarkList.title, + isPublicMemberBookmark = maybePublicBookmark != null, + isPrivateMemberBookmark = maybePrivateBookmark != null, + totalPostBookmarkSize = bookmarkList.publicPostBookmarks.size + bookmarkList.privatePostBookmarks.size, + totalArticleBookmarkSize = bookmarkList.publicArticleBookmarks.size + bookmarkList.privateArticleBookmarks.size, + onClick = { nav.nav(Route.BookmarkGroupView(bookmarkList.identifier, BookmarkType.PostBookmark)) }, + onAddBookmarkToGroup = { shouldBePrivate -> + accountViewModel.launchSigner { + accountViewModel.account.labeledBookmarkLists.addBookmarkToList( + bookmark = EventBookmark(eventId = note.idHex, relay = note.relayHintUrl(), author = note.author?.pubkeyHex), + bookmarkListIdentifier = bookmarkList.identifier, + isBookmarkPrivate = shouldBePrivate, + account = accountViewModel.account, + ) + } + }, + onRemoveBookmarkFromGroup = { + accountViewModel.launchSigner { + accountViewModel.account.labeledBookmarkLists.removeBookmarkFromList( + bookmark = EventBookmark(eventId = note.idHex), + bookmarkListIdentifier = bookmarkList.identifier, + isBookmarkPrivate = maybePrivateBookmark != null, + account = accountViewModel.account, + ) + } + }, + ) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/BookmarkTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/BookmarkTabHeader.kt index 730d1dc3e..41a7cdca6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/BookmarkTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/BookmarkTabHeader.kt @@ -36,5 +36,5 @@ fun BookmarkTabHeader( ) { val userBookmarks by observeUserBookmarkCount(baseUser, accountViewModel) - Text(text = "$userBookmarks ${stringRes(R.string.bookmarks)}") + Text(text = "$userBookmarks ${stringRes(R.string.bookmarks_title)}") } diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 7893b01aa..9542f59a9 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -391,6 +391,8 @@ Manual Zap Splits Bookmarks + Default Bookmarks + Your default Bookmarks that many clients support Drafts Private Bookmarks Public Bookmarks diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/BookmarkListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/BookmarkListEvent.kt index 1118b2135..3c4e2ba5d 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/BookmarkListEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/BookmarkListEvent.kt @@ -148,6 +148,21 @@ class BookmarkListEvent( ) } + suspend fun remove( + earlierVersion: BookmarkListEvent, + bookmarkIdTag: BookmarkIdTag, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): BookmarkListEvent { + val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + return resign( + privateTags = privateTags.remove(bookmarkIdTag.toTagIdOnly()), + tags = earlierVersion.tags.remove(bookmarkIdTag.toTagIdOnly()), + signer = signer, + createdAt = createdAt, + ) + } + suspend fun resign( tags: TagArray, privateTags: TagArray, From 1835c2238e3f67f951424a13caba729b4f139b64 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 5 Jan 2026 16:01:39 -0500 Subject: [PATCH 071/137] removes unecessary log --- .../src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 28b05c3f2..3a89e5538 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -1106,7 +1106,6 @@ object LocalCache : ILocalCache { val new = consumeBaseReplaceable(event, relay, wasVerified) if (new) { - println("AABBCC New ContactCard about ${event.aboutUser()}") val about = checkGetOrCreateUser(event.aboutUser()) ?: return new about.cards().addCard(note) } From 2c4279621d27e407c03aad1394c0beeb4476f229 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 5 Jan 2026 17:18:01 -0500 Subject: [PATCH 072/137] Makes the Kotlin Serialization lenient to avoid unnecessary crashes --- .../vitorpamplona/quartz/nip01Core/core/OptimizedJsonMapper.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/OptimizedJsonMapper.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/OptimizedJsonMapper.kt index 9e9b6de35..a671865a7 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/OptimizedJsonMapper.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/OptimizedJsonMapper.kt @@ -54,6 +54,7 @@ object JsonMapper { val jsonInstance = Json { ignoreUnknownKeys = true + isLenient = true } inline fun fromJson(json: String): T = jsonInstance.decodeFromString(json) From 69c53aca0657552c4e86d1d71f16f4e8ba98d2bb Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 5 Jan 2026 19:26:00 -0500 Subject: [PATCH 073/137] Improving error logs --- amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt | 2 +- .../main/java/com/vitorpamplona/amethyst/model/LocalCache.kt | 2 +- .../nip01Core/relay/client/single/basic/BasicRelayClient.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt index e62736ca8..8f28543b3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt @@ -200,7 +200,7 @@ class AppModules( val relayStats = RelayStats(client) // Logs debug messages when needed - val detailedLogger = if (isDebug) RelayLogger(client, true, false) else null + val detailedLogger = if (isDebug) RelayLogger(client, false, false) else null val relayReqStats = if (isDebug) RelayReqStats(client) else null val logger = if (isDebug) RelaySpeedLogger(client) else null diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 3a89e5538..1b452063f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -2889,7 +2889,7 @@ object LocalCache : ILocalCache { } } catch (e: Exception) { if (e is CancellationException) throw e - Log.w("LocalCache", "Cannot consume ${event.kind}", e) + Log.w("LocalCache", "Cannot consume ${event.toJson()} from ${relay?.url}", e) false } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt index 0d765370c..71e420f13 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt @@ -128,7 +128,7 @@ open class BasicRelayClient( } catch (e: Throwable) { if (e is CancellationException) throw e // doesn't expose parsing errors to lib users as errors - Log.e("BasicRelayClient", "Failure to parse message from Relay", e) + Log.e("BasicRelayClient", "Failure to parse message from Relay: $text", e) } } From 8a5e05c0acb8a0045fe42eb16ef84a44d60a5e71 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 5 Jan 2026 19:26:24 -0500 Subject: [PATCH 074/137] Simple check to make sure events are valid. --- .../quartz/nip01Core/jackson/EventDeserializer.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/EventDeserializer.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/EventDeserializer.kt index 2dd3c09b1..7ea980703 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/EventDeserializer.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/EventDeserializer.kt @@ -63,6 +63,10 @@ class EventDeserializer : StdDeserializer(Event::class.java) { } } + if (pubKey.isEmpty()) { + throw IllegalArgumentException("Event not found") + } + return EventFactory.create(id, pubKey, createdAt, kind, tags, content, sig) } } From 40f9622722e52f61585f3a3ddbc335c8fc099885 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 5 Jan 2026 19:41:23 -0500 Subject: [PATCH 075/137] Checks kind size before throwing --- .../quartz/nip01Core/core/AddressSerializer.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/AddressSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/AddressSerializer.kt index ca60a06cd..23c9cf33d 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/AddressSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/AddressSerializer.kt @@ -44,7 +44,13 @@ class AddressSerializer { return try { val parts = addressId.split(":", limit = 3) if (parts.size > 2 && parts[1].length == 64 && Hex.isHex(parts[1])) { - Address(parts[0].toInt(), parts[1], parts.getOrNull(2) ?: "") + if (parts[0].length > 5) { + // invalid kind + Log.w("AddressableId", "Error parsing. invalid kind $addressId") + null + } else { + Address(parts[0].toInt(), parts[1], parts.getOrNull(2) ?: "") + } } else { if (addressId.startsWith("naddr1")) { val addr = Nip19Parser.uriToRoute(addressId)?.entity @@ -60,7 +66,7 @@ class AddressSerializer { } } } catch (t: Throwable) { - Log.e("AddressableId", "Error parsing: $addressId: ${t.message}", t) + Log.w("AddressableId", "Error parsing: $addressId: ${t.message}", t) null } } From 43392478153fb736a60b213671c7bd71ab6b57d3 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Tue, 6 Jan 2026 06:49:59 +0200 Subject: [PATCH 076/137] key storage --- .../commons/account/AccountManager.kt | 90 ++++++- .../vitorpamplona/amethyst/desktop/Main.kt | 19 +- .../amethyst/desktop/ui/LoginScreen.kt | 15 +- docs/secure-key-storage-migration.md | 208 +++++++++++++++ gradle/libs.versions.toml | 2 + quartz/build.gradle.kts | 7 + .../nip01Core/crypto/KeyStoreEncryption.kt | 102 ++++++++ .../nip01Core/crypto/SecureKeyStorage.kt | 92 +++++++ .../nip01Core/crypto/SecureKeyStorage.kt | 69 +++++ .../nip01Core/crypto/SecureKeyStorage.kt | 242 ++++++++++++++++++ 10 files changed, 840 insertions(+), 6 deletions(-) create mode 100644 docs/secure-key-storage-migration.md create mode 100644 quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/KeyStoreEncryption.kt create mode 100644 quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt create mode 100644 quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt index 6fb4df3e3..5e334b976 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt @@ -23,6 +23,8 @@ package com.vitorpamplona.amethyst.commons.account import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import com.vitorpamplona.quartz.nip01Core.crypto.SecureKeyStorage +import com.vitorpamplona.quartz.nip01Core.crypto.SecureStorageException import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip19Bech32.decodePrivateKeyAsHexOrNull @@ -45,10 +47,64 @@ sealed class AccountState { ) : AccountState() } -class AccountManager { +class AccountManager( + private val secureStorage: SecureKeyStorage, +) { private val _accountState = MutableStateFlow(AccountState.LoggedOut) val accountState: StateFlow = _accountState.asStateFlow() + /** + * Loads the last saved account from secure storage. + * Call on app startup. + */ + suspend fun loadSavedAccount(): Result { + return try { + // For simplicity, we'll store the last logged-in npub in a simple file + // and use SecureKeyStorage to retrieve the private key + val lastNpub = getLastNpub() ?: return Result.failure(Exception("No saved account")) + + val privKeyHex = secureStorage.getPrivateKey(lastNpub) + ?: return Result.failure(Exception("Private key not found for $lastNpub")) + + val keyPair = KeyPair(privKey = privKeyHex.hexToByteArray()) + val signer = NostrSignerInternal(keyPair) + + val state = + AccountState.LoggedIn( + signer = signer, + pubKeyHex = keyPair.pubKey.toHexKey(), + npub = keyPair.pubKey.toNpub(), + nsec = keyPair.privKey?.toNsec(), + isReadOnly = false, + ) + _accountState.value = state + Result.success(state) + } catch (e: Exception) { + Result.failure(e) + } + } + + /** + * Saves the current account to secure storage. + */ + suspend fun saveCurrentAccount(): Result { + val current = currentAccount() ?: return Result.failure(Exception("No account logged in")) + if (current.isReadOnly || current.nsec == null) { + return Result.failure(Exception("Cannot save read-only account")) + } + + return try { + val privKeyHex = decodePrivateKeyAsHexOrNull(current.nsec) + ?: return Result.failure(Exception("Invalid nsec format")) + + secureStorage.savePrivateKey(current.npub, privKeyHex) + saveLastNpub(current.npub) + Result.success(Unit) + } catch (e: SecureStorageException) { + Result.failure(e) + } + } + fun generateNewAccount(): AccountState.LoggedIn { val keyPair = KeyPair() val signer = NostrSignerInternal(keyPair) @@ -115,11 +171,41 @@ class AccountManager { return Result.failure(IllegalArgumentException("Invalid key format. Use nsec1, npub1, or hex format.")) } - fun logout() { + suspend fun logout(deleteKey: Boolean = false) { + val current = currentAccount() + if (deleteKey && current != null) { + try { + secureStorage.deletePrivateKey(current.npub) + clearLastNpub() + } catch (e: SecureStorageException) { + // Log error but still logout + } + } _accountState.value = AccountState.LoggedOut } fun isLoggedIn(): Boolean = _accountState.value is AccountState.LoggedIn fun currentAccount(): AccountState.LoggedIn? = _accountState.value as? AccountState.LoggedIn + + // Simple file-based storage for last npub (non-sensitive data) + private fun getLastNpub(): String? { + val file = getPrefsFile() + return if (file.exists()) file.readText().trim().takeIf { it.isNotEmpty() } else null + } + + private fun saveLastNpub(npub: String) { + val file = getPrefsFile() + file.parentFile?.mkdirs() + file.writeText(npub) + } + + private fun clearLastNpub() { + getPrefsFile().delete() + } + + private fun getPrefsFile(): java.io.File { + val homeDir = System.getProperty("user.home") + return java.io.File(homeDir, ".amethyst/last_account.txt") + } } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt index b328301a6..2954046a1 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt @@ -60,6 +60,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -84,6 +85,11 @@ import com.vitorpamplona.amethyst.desktop.ui.FeedScreen import com.vitorpamplona.amethyst.desktop.ui.LoginScreen import com.vitorpamplona.amethyst.desktop.ui.NotificationsScreen import com.vitorpamplona.amethyst.desktop.ui.UserProfileScreen +import com.vitorpamplona.quartz.nip01Core.crypto.SecureKeyStorage +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch private val isMacOS = System.getProperty("os.name").lowercase().contains("mac") @@ -207,10 +213,17 @@ fun App( ) { var currentScreen by remember { mutableStateOf(DesktopScreen.Feed) } val relayManager = remember { DesktopRelayConnectionManager() } - val accountManager = remember { AccountManager() } + val secureStorage = remember { SecureKeyStorage() } + val accountManager = remember { AccountManager(secureStorage) } val accountState by accountManager.accountState.collectAsState() + val scope = remember { CoroutineScope(SupervisorJob() + Dispatchers.Main) } + // Try to load saved account on startup DisposableEffect(Unit) { + scope.launch { + accountManager.loadSavedAccount() + } + relayManager.addDefaultRelays() relayManager.connect() onDispose { @@ -376,6 +389,8 @@ fun ProfileScreen( account: AccountState.LoggedIn, accountManager: AccountManager, ) { + val scope = rememberCoroutineScope() + Column { Text( "Profile", @@ -393,7 +408,7 @@ fun ProfileScreen( Spacer(Modifier.height(24.dp)) OutlinedButton( - onClick = { accountManager.logout() }, + onClick = { scope.launch { accountManager.logout() } }, colors = androidx.compose.material3.ButtonDefaults.outlinedButtonColors( contentColor = Color.Red, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/LoginScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/LoginScreen.kt index 68761246e..d32026cef 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/LoginScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/LoginScreen.kt @@ -32,6 +32,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -40,6 +41,7 @@ import com.vitorpamplona.amethyst.commons.account.AccountManager import com.vitorpamplona.amethyst.commons.account.AccountState import com.vitorpamplona.amethyst.commons.ui.auth.LoginCard import com.vitorpamplona.amethyst.commons.ui.auth.NewKeyWarningCard +import kotlinx.coroutines.launch @Composable fun LoginScreen( @@ -48,6 +50,7 @@ fun LoginScreen( ) { var showNewKeyDialog by remember { mutableStateOf(false) } var generatedAccount by remember { mutableStateOf(null) } + val scope = rememberCoroutineScope() Column( modifier = Modifier.fillMaxSize().padding(32.dp), @@ -73,7 +76,11 @@ fun LoginScreen( LoginCard( onLogin = { keyInput -> accountManager.loginWithKey(keyInput).map { - onLoginSuccess() + // Save account to secure storage + scope.launch { + accountManager.saveCurrentAccount() + onLoginSuccess() + } } }, onGenerateNew = { @@ -89,7 +96,11 @@ fun LoginScreen( nsec = generatedAccount!!.nsec, onContinue = { showNewKeyDialog = false - onLoginSuccess() + // Save generated account + scope.launch { + accountManager.saveCurrentAccount() + onLoginSuccess() + } }, ) } diff --git a/docs/secure-key-storage-migration.md b/docs/secure-key-storage-migration.md new file mode 100644 index 000000000..bec77ca40 --- /dev/null +++ b/docs/secure-key-storage-migration.md @@ -0,0 +1,208 @@ +# SecureKeyStorage Migration Guide + +## Overview + +SecureKeyStorage is now in `quartz/nip01Core/crypto/` as a KMP module for secure nsec (private key) storage. + +**Location:** `com.vitorpamplona.quartz.nip01Core.crypto.SecureKeyStorage` + +## Platform Implementations + +| Platform | Backend | Encryption | Hardware-Backed | +|----------|---------|------------|-----------------| +| **Android** | EncryptedSharedPreferences + Android Keystore | AES-256-GCM | ✅ (StrongBox when available) | +| **Desktop macOS** | macOS Keychain (via java-keyring) | OS-managed | ✅ (T2/M1+ chips) | +| **Desktop Windows** | Credential Manager (via java-keyring) | DPAPI | ✅ (TPM when available) | +| **Desktop Linux** | Secret Service/KWallet (via java-keyring) | OS-managed | ⚠️ (depends on setup) | +| **Fallback** | Encrypted file (~/.amethyst/keys.enc) | AES-256-GCM (PBKDF2) | ❌ | + +## API + +```kotlin +// Create instance (Android requires Context) +val storage = SecureKeyStorage(context) // Android +val storage = SecureKeyStorage() // Desktop + +// Save private key +suspend fun savePrivateKey(npub: String, privKeyHex: String) + +// Retrieve private key +suspend fun getPrivateKey(npub: String): String? + +// Delete private key +suspend fun deletePrivateKey(npub: String): Boolean + +// Check if exists +suspend fun hasPrivateKey(npub: String): Boolean +``` + +## Migrating Amethyst + +### Current Implementation + +**File:** `amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt` + +- Uses `EncryptedSharedPreferences` directly +- Private keys stored in per-account encrypted prefs: `secret_keeper_$npub` +- Key: `NOSTR_PRIVKEY` + +**Current flow:** +```kotlin +// Save +val prefs = EncryptedStorage.preferences(context, npub) +prefs.edit().putString(PrefKeys.NOSTR_PRIVKEY, privKeyHex).apply() + +// Load +val privKey = prefs.getString(PrefKeys.NOSTR_PRIVKEY, null) +``` + +### New Implementation + +**Import:** +```kotlin +import com.vitorpamplona.quartz.nip01Core.crypto.SecureKeyStorage +import com.vitorpamplona.quartz.nip01Core.crypto.SecureStorageException +``` + +**New flow:** +```kotlin +// Initialize once (in Application or DI module) +val secureStorage = SecureKeyStorage(applicationContext) + +// Save +suspend fun saveAccount(npub: String, privKeyHex: String) { + try { + secureStorage.savePrivateKey(npub, privKeyHex) + } catch (e: SecureStorageException) { + Log.e("Account", "Failed to save key", e) + } +} + +// Load +suspend fun loadAccount(npub: String): String? { + return try { + secureStorage.getPrivateKey(npub) + } catch (e: SecureStorageException) { + Log.e("Account", "Failed to load key", e) + null + } +} + +// Delete +suspend fun deleteAccount(npub: String) { + secureStorage.deletePrivateKey(npub) +} +``` + +### Migration Steps + +1. **Update LocalPreferences.kt:** + - Add `SecureKeyStorage` instance + - Replace `EncryptedStorage.preferences()` calls with `SecureKeyStorage` methods + - Wrap in coroutines (`withContext(Dispatchers.IO)`) + +2. **Update AccountSecretsEncryptedStores.kt:** + - Already uses DataStore - can be replaced or kept for NWC secrets + - Consider using `SecureKeyStorage` for consistency + +3. **Handle Existing Keys:** + - **Option A (Automatic Migration):** On first launch, read from old storage → save to new storage → delete old + - **Option B (Keep Dual Storage):** Keep old code for read, use new for writes, phase out old storage later + - **Option C (Clean Break):** Require users to re-enter nsec on first launch after update + +4. **Desktop Integration:** + - Create `SecureKeyStorage()` instance (no context needed) + - Use same API calls as Android + - Fallback password prompt if OS keyring unavailable (auto-triggered) + +### Example Migration (Option A) + +```kotlin +// In LocalPreferences +private val secureStorage = SecureKeyStorage(applicationContext) + +suspend fun migrateToNewStorage(npub: String) { + // Check if already migrated + if (secureStorage.hasPrivateKey(npub)) return + + // Read from old storage + val oldPrefs = EncryptedStorage.preferences(applicationContext, npub) + val privKey = oldPrefs.getString(PrefKeys.NOSTR_PRIVKEY, null) ?: return + + // Save to new storage + secureStorage.savePrivateKey(npub, privKey) + + // Clean up old storage (optional) + oldPrefs.edit().remove(PrefKeys.NOSTR_PRIVKEY).apply() +} + +suspend fun loadCurrentAccountFromEncryptedStorage(npub: String?): AccountSettings? { + npub ?: return null + + // Auto-migrate if needed + migrateToNewStorage(npub) + + // Load from new storage + val privKey = secureStorage.getPrivateKey(npub) + + // ... rest of loading logic +} +``` + +## Desktop Fallback Behavior + +If OS keyring unavailable (headless server, permission denied): + +1. User prompted for master password on first `savePrivateKey()` call +2. Password cached in memory for session +3. Keys stored encrypted in `~/.amethyst/keys.enc` +4. Format: `:` (one per line) +5. Encryption: AES-256-GCM with PBKDF2-derived key (100k iterations) + +**Security Note:** Fallback is less secure than OS keyring. Warn users to use OS-integrated keychain when possible. + +## Testing + +### Android +```kotlin +@Test +fun testSecureStorage() = runTest { + val storage = SecureKeyStorage(context) + val npub = "npub1test..." + val privKey = "nsec1test..." + + storage.savePrivateKey(npub, privKey) + assertEquals(privKey, storage.getPrivateKey(npub)) + assertTrue(storage.hasPrivateKey(npub)) + + storage.deletePrivateKey(npub) + assertFalse(storage.hasPrivateKey(npub)) +} +``` + +### Desktop +```kotlin +@Test +fun testDesktopStorage() = runTest { + val storage = SecureKeyStorage() + + // Test with OS keyring (if available) + storage.savePrivateKey("npub1test...", "nsec1test...") + assertNotNull(storage.getPrivateKey("npub1test...")) +} +``` + +## Security Considerations + +- **Android:** Keys protected by hardware when available (TEE/StrongBox) +- **Desktop:** Keys stored in OS-native credential managers (encrypted at rest) +- **Fallback:** User-password-protected file (PBKDF2 + AES-256-GCM) +- **Never log private keys** - use `SecureStorageException` for errors +- **Clear from memory** - keys returned as strings (consider SecureString for future enhancement) + +## References + +- Android Implementation: `quartz/src/androidMain/kotlin/.../SecureKeyStorage.kt` +- Desktop Implementation: `quartz/src/jvmMain/kotlin/.../SecureKeyStorage.kt` +- java-keyring: https://github.com/javakeyring/java-keyring +- Android EncryptedSharedPreferences: https://developer.android.com/topic/security/data diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f2c9693c5..0918278b9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,6 +23,7 @@ firebaseBom = "34.7.0" fragmentKtx = "1.8.9" gms = "4.4.4" jacksonModuleKotlin = "2.20.1" +javaKeyring = "1.0.1" jna = "5.18.1" jtorctl = "0.4.5.7" junit = "4.13.2" @@ -118,6 +119,7 @@ firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging google-mlkit-language-id = { group = "com.google.mlkit", name = "language-id", version.ref = "languageId" } google-mlkit-translate = { group = "com.google.mlkit", name = "translate", version.ref = "translate" } jackson-module-kotlin = { group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version.ref = "jacksonModuleKotlin" } +java-keyring = { group = "com.github.javakeyring", name = "java-keyring", version.ref = "javaKeyring" } jna = { group = "net.java.dev.jna", name = "jna", version.ref = "jna" } jtorctl = { module = "info.guardianproject:jtorctl", version.ref = "jtorctl" } junit = { group = "junit", name = "junit", version.ref = "junit" } diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index a9f1d39e6..7bf19ba52 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -166,6 +166,9 @@ kotlin { // LibSodium for ChaCha encryption (NIP-44) implementation (libs.lazysodium.java) implementation (libs.jna) + + // Secure key storage via OS keychain (macOS/Windows/Linux) + implementation(libs.java.keyring) } } @@ -188,6 +191,10 @@ kotlin { // LibSodium for ChaCha encryption (NIP-44) implementation ("com.goterl:lazysodium-android:5.2.0@aar") implementation ("net.java.dev.jna:jna:5.18.1@aar") + + // Secure key storage via Android Keystore + implementation(libs.androidx.security.crypto.ktx) + implementation(libs.androidx.datastore.preferences) } } diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/KeyStoreEncryption.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/KeyStoreEncryption.kt new file mode 100644 index 000000000..548ad6b21 --- /dev/null +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/KeyStoreEncryption.kt @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.crypto + +import android.os.Build +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import android.security.keystore.StrongBoxUnavailableException +import java.security.KeyStore +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.spec.IvParameterSpec + +internal class KeyStoreEncryption { + companion object { + private const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES + private const val BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM + private const val PADDING = KeyProperties.ENCRYPTION_PADDING_NONE + private const val TRANSFORMATION = "$ALGORITHM/$BLOCK_MODE/$PADDING" + private const val PURPOSE = KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + private const val KEY_ALIAS = "AMETHYST_AES_KEY" + } + + private val cipher = Cipher.getInstance(TRANSFORMATION) + private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } + + private fun getKey(): SecretKey { + val existingKey = keyStore.getEntry(KEY_ALIAS, null) as? KeyStore.SecretKeyEntry + return existingKey?.secretKey ?: createKey() + } + + private fun createKeyStrongBoxIfAvailable(): SecretKey? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + try { + val keyParams = + KeyGenParameterSpec + .Builder(KEY_ALIAS, PURPOSE) + .setBlockModes(BLOCK_MODE) + .setEncryptionPaddings(PADDING) + .setIsStrongBoxBacked(true) + .build() + + val generator = KeyGenerator.getInstance(ALGORITHM, "AndroidKeyStore") + generator.init(keyParams) + generator.generateKey() + } catch (_: StrongBoxUnavailableException) { + null + } + } else { + null + } + + private fun createKeyRegular(): SecretKey { + val keyParams = + KeyGenParameterSpec + .Builder(KEY_ALIAS, PURPOSE) + .setBlockModes(BLOCK_MODE) + .setEncryptionPaddings(PADDING) + .build() + + val generator = KeyGenerator.getInstance(ALGORITHM, "AndroidKeyStore") + generator.init(keyParams) + return generator.generateKey() + } + + private fun createKey(): SecretKey = createKeyStrongBoxIfAvailable() ?: createKeyRegular() + + fun encrypt(bytes: ByteArray): ByteArray { + // Initializes the cipher in encrypt mode and encrypts data + cipher.init(Cipher.ENCRYPT_MODE, getKey()) + val iv = cipher.iv + val encrypted = cipher.doFinal(bytes) + return iv + encrypted + } + + fun decrypt(bytes: ByteArray): ByteArray { + // Extracts IV and decrypts the data + val iv = bytes.copyOfRange(0, 12) // GCM mode uses 12-byte IV + val data = bytes.copyOfRange(12, bytes.size) + cipher.init(Cipher.DECRYPT_MODE, getKey(), IvParameterSpec(iv)) + return cipher.doFinal(data) + } +} diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt new file mode 100644 index 000000000..2f76f72e6 --- /dev/null +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.crypto + +import android.content.Context +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * Android implementation of SecureKeyStorage using EncryptedSharedPreferences + * backed by Android Keystore (AES-256-GCM, hardware-backed when available). + */ +actual class SecureKeyStorage(private val context: Context) { + companion object { + private const val PREFS_NAME = "amethyst_secure_keys" + private const val KEY_PREFIX = "privkey_" + } + + private val masterKey: MasterKey by lazy { + MasterKey.Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + } + + private val encryptedPrefs by lazy { + EncryptedSharedPreferences.create( + context, + PREFS_NAME, + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, + ) + } + + actual suspend fun savePrivateKey(npub: String, privKeyHex: String) { + withContext(Dispatchers.IO) { + try { + encryptedPrefs.edit().putString(KEY_PREFIX + npub, privKeyHex).apply() + } catch (e: Exception) { + throw SecureStorageException("Failed to save private key", e) + } + } + } + + actual suspend fun getPrivateKey(npub: String): String? = + withContext(Dispatchers.IO) { + try { + encryptedPrefs.getString(KEY_PREFIX + npub, null) + } catch (e: Exception) { + throw SecureStorageException("Failed to retrieve private key", e) + } + } + + actual suspend fun deletePrivateKey(npub: String): Boolean = + withContext(Dispatchers.IO) { + try { + val key = KEY_PREFIX + npub + val existed = encryptedPrefs.contains(key) + if (existed) { + encryptedPrefs.edit().remove(key).apply() + } + existed + } catch (e: Exception) { + throw SecureStorageException("Failed to delete private key", e) + } + } + + actual suspend fun hasPrivateKey(npub: String): Boolean = + withContext(Dispatchers.IO) { + encryptedPrefs.contains(KEY_PREFIX + npub) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt new file mode 100644 index 000000000..188dd07c0 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.crypto + +/** + * Secure storage for private keys using platform-specific secure storage mechanisms. + * + * Android: Android Keystore + EncryptedDataStore (AES-256-GCM, StrongBox when available) + * Desktop: OS native credential managers (macOS Keychain, Windows Credential Manager, Linux Secret Service) + */ +expect class SecureKeyStorage { + /** + * Saves a private key securely for the given npub. + * + * @param npub The public key in npub (Bech32) format + * @param privKeyHex The private key in hexadecimal format + * @throws SecureStorageException if storage operation fails + */ + suspend fun savePrivateKey(npub: String, privKeyHex: String) + + /** + * Retrieves a private key for the given npub. + * + * @param npub The public key in npub (Bech32) format + * @return The private key in hexadecimal format, or null if not found + * @throws SecureStorageException if retrieval operation fails + */ + suspend fun getPrivateKey(npub: String): String? + + /** + * Deletes a private key for the given npub. + * + * @param npub The public key in npub (Bech32) format + * @return true if the key was deleted, false if it didn't exist + * @throws SecureStorageException if deletion operation fails + */ + suspend fun deletePrivateKey(npub: String): Boolean + + /** + * Checks if a private key exists for the given npub. + * + * @param npub The public key in npub (Bech32) format + * @return true if a private key exists, false otherwise + */ + suspend fun hasPrivateKey(npub: String): Boolean +} + +/** + * Exception thrown when secure storage operations fail. + */ +class SecureStorageException(message: String, cause: Throwable? = null) : Exception(message, cause) diff --git a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt b/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt new file mode 100644 index 000000000..b0dc7e6d1 --- /dev/null +++ b/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt @@ -0,0 +1,242 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.crypto + +import com.github.javakeyring.BackendNotSupportedException +import com.github.javakeyring.Keyring +import com.github.javakeyring.PasswordAccessException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.security.SecureRandom +import java.util.Base64 +import javax.crypto.Cipher +import javax.crypto.SecretKeyFactory +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.PBEKeySpec +import javax.crypto.spec.SecretKeySpec + +/** + * Desktop implementation of SecureKeyStorage using OS-native credential managers + * (macOS Keychain, Windows Credential Manager, Linux Secret Service/KWallet). + * + * Falls back to encrypted file storage with user-provided password if OS keyring + * is unavailable. + */ +actual class SecureKeyStorage { + companion object { + private const val SERVICE_NAME = "amethyst-desktop" + private const val FALLBACK_DIR = ".amethyst" + private const val FALLBACK_FILE = "keys.enc" + + // Encryption constants for fallback + private const val ALGORITHM = "AES" + private const val TRANSFORMATION = "AES/GCM/NoPadding" + private const val KEY_LENGTH = 256 + private const val ITERATION_COUNT = 100000 + private const val IV_LENGTH = 12 // GCM standard + } + + private var keyringAvailable: Boolean = true + private var fallbackPassword: String? = null + + actual suspend fun savePrivateKey(npub: String, privKeyHex: String) { + withContext(Dispatchers.IO) { + try { + if (keyringAvailable) { + saveToKeyring(npub, privKeyHex) + } else { + saveToFallback(npub, privKeyHex) + } + } catch (e: BackendNotSupportedException) { + keyringAvailable = false + println("OS keyring not available, using fallback encrypted storage") + saveToFallback(npub, privKeyHex) + } catch (e: Exception) { + throw SecureStorageException("Failed to save private key", e) + } + } + } + + actual suspend fun getPrivateKey(npub: String): String? = + withContext(Dispatchers.IO) { + try { + if (keyringAvailable) { + getFromKeyring(npub) + } else { + getFromFallback(npub) + } + } catch (e: BackendNotSupportedException) { + keyringAvailable = false + println("OS keyring not available, using fallback encrypted storage") + getFromFallback(npub) + } catch (e: PasswordAccessException) { + null // Key doesn't exist + } catch (e: Exception) { + throw SecureStorageException("Failed to retrieve private key", e) + } + } + + actual suspend fun deletePrivateKey(npub: String): Boolean = + withContext(Dispatchers.IO) { + try { + if (keyringAvailable) { + deleteFromKeyring(npub) + } else { + deleteFromFallback(npub) + } + } catch (e: BackendNotSupportedException) { + keyringAvailable = false + deleteFromFallback(npub) + } catch (e: Exception) { + throw SecureStorageException("Failed to delete private key", e) + } + } + + actual suspend fun hasPrivateKey(npub: String): Boolean = + getPrivateKey(npub) != null + + // Keyring-based storage + private fun saveToKeyring(npub: String, privKeyHex: String) { + val keyring = Keyring.create() + keyring.setPassword(SERVICE_NAME, npub, privKeyHex) + } + + private fun getFromKeyring(npub: String): String? = + try { + val keyring = Keyring.create() + keyring.getPassword(SERVICE_NAME, npub) + } catch (e: PasswordAccessException) { + null + } + + private fun deleteFromKeyring(npub: String): Boolean = + try { + val keyring = Keyring.create() + keyring.deletePassword(SERVICE_NAME, npub) + true + } catch (e: PasswordAccessException) { + false + } + + // Fallback encrypted file storage + private fun saveToFallback(npub: String, privKeyHex: String) { + val password = getFallbackPassword() + val encrypted = encryptData(privKeyHex, password) + + val fallbackFile = getFallbackFile() + val data = loadFallbackData().toMutableMap() + data[npub] = encrypted + + fallbackFile.parentFile?.mkdirs() + fallbackFile.writeText(data.entries.joinToString("\n") { "${it.key}:${it.value}" }) + } + + private fun getFromFallback(npub: String): String? { + val password = fallbackPassword ?: return null // No password set yet + val data = loadFallbackData() + val encrypted = data[npub] ?: return null + + return try { + decryptData(encrypted, password) + } catch (e: Exception) { + null + } + } + + private fun deleteFromFallback(npub: String): Boolean { + val fallbackFile = getFallbackFile() + if (!fallbackFile.exists()) return false + + val data = loadFallbackData().toMutableMap() + val existed = data.remove(npub) != null + + if (existed) { + if (data.isEmpty()) { + fallbackFile.delete() + } else { + fallbackFile.writeText(data.entries.joinToString("\n") { "${it.key}:${it.value}" }) + } + } + + return existed + } + + private fun loadFallbackData(): Map { + val fallbackFile = getFallbackFile() + if (!fallbackFile.exists()) return emptyMap() + + return fallbackFile.readLines() + .mapNotNull { line -> + val parts = line.split(":", limit = 2) + if (parts.size == 2) parts[0] to parts[1] else null + } + .toMap() + } + + private fun getFallbackFile(): File { + val homeDir = System.getProperty("user.home") + return File(homeDir, "$FALLBACK_DIR/$FALLBACK_FILE") + } + + private fun getFallbackPassword(): String { + if (fallbackPassword == null) { + println("OS keyring not available. Fallback encrypted storage requires a password.") + print("Enter master password: ") + fallbackPassword = readLine() ?: throw SecureStorageException("Password required for fallback storage") + } + return fallbackPassword!! + } + + private fun encryptData(plaintext: String, password: String): String { + val salt = ByteArray(16).apply { SecureRandom().nextBytes(this) } + val iv = ByteArray(IV_LENGTH).apply { SecureRandom().nextBytes(this) } + + val keySpec = PBEKeySpec(password.toCharArray(), salt, ITERATION_COUNT, KEY_LENGTH) + val secretKey = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(keySpec) + val key = SecretKeySpec(secretKey.encoded, ALGORITHM) + + val cipher = Cipher.getInstance(TRANSFORMATION) + cipher.init(Cipher.ENCRYPT_MODE, key, IvParameterSpec(iv)) + val encrypted = cipher.doFinal(plaintext.toByteArray()) + + val combined = salt + iv + encrypted + return Base64.getEncoder().encodeToString(combined) + } + + private fun decryptData(ciphertext: String, password: String): String { + val combined = Base64.getDecoder().decode(ciphertext) + + val salt = combined.copyOfRange(0, 16) + val iv = combined.copyOfRange(16, 16 + IV_LENGTH) + val encrypted = combined.copyOfRange(16 + IV_LENGTH, combined.size) + + val keySpec = PBEKeySpec(password.toCharArray(), salt, ITERATION_COUNT, KEY_LENGTH) + val secretKey = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(keySpec) + val key = SecretKeySpec(secretKey.encoded, ALGORITHM) + + val cipher = Cipher.getInstance(TRANSFORMATION) + cipher.init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv)) + val decrypted = cipher.doFinal(encrypted) + + return String(decrypted) + } +} From df9d648988bbe91ef89d586c856933b39cc63aad Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Tue, 6 Jan 2026 06:50:31 +0200 Subject: [PATCH 077/137] key storage spotless --- .../commons/account/AccountManager.kt | 10 +++--- .../vitorpamplona/amethyst/desktop/Main.kt | 2 +- .../nip01Core/crypto/SecureKeyStorage.kt | 12 +++++-- .../nip01Core/crypto/SecureKeyStorage.kt | 10 ++++-- .../nip01Core/crypto/SecureKeyStorage.kt | 34 +++++++++++++------ 5 files changed, 48 insertions(+), 20 deletions(-) diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt index 5e334b976..1b9321077 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt @@ -63,8 +63,9 @@ class AccountManager( // and use SecureKeyStorage to retrieve the private key val lastNpub = getLastNpub() ?: return Result.failure(Exception("No saved account")) - val privKeyHex = secureStorage.getPrivateKey(lastNpub) - ?: return Result.failure(Exception("Private key not found for $lastNpub")) + val privKeyHex = + secureStorage.getPrivateKey(lastNpub) + ?: return Result.failure(Exception("Private key not found for $lastNpub")) val keyPair = KeyPair(privKey = privKeyHex.hexToByteArray()) val signer = NostrSignerInternal(keyPair) @@ -94,8 +95,9 @@ class AccountManager( } return try { - val privKeyHex = decodePrivateKeyAsHexOrNull(current.nsec) - ?: return Result.failure(Exception("Invalid nsec format")) + val privKeyHex = + decodePrivateKeyAsHexOrNull(current.nsec) + ?: return Result.failure(Exception("Invalid nsec format")) secureStorage.savePrivateKey(current.npub, privKeyHex) saveLastNpub(current.npub) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt index 2954046a1..c442390d3 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt @@ -390,7 +390,7 @@ fun ProfileScreen( accountManager: AccountManager, ) { val scope = rememberCoroutineScope() - + Column { Text( "Profile", diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt index 2f76f72e6..f6a8754aa 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt @@ -30,14 +30,17 @@ import kotlinx.coroutines.withContext * Android implementation of SecureKeyStorage using EncryptedSharedPreferences * backed by Android Keystore (AES-256-GCM, hardware-backed when available). */ -actual class SecureKeyStorage(private val context: Context) { +actual class SecureKeyStorage( + private val context: Context, +) { companion object { private const val PREFS_NAME = "amethyst_secure_keys" private const val KEY_PREFIX = "privkey_" } private val masterKey: MasterKey by lazy { - MasterKey.Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS) + MasterKey + .Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build() } @@ -52,7 +55,10 @@ actual class SecureKeyStorage(private val context: Context) { ) } - actual suspend fun savePrivateKey(npub: String, privKeyHex: String) { + actual suspend fun savePrivateKey( + npub: String, + privKeyHex: String, + ) { withContext(Dispatchers.IO) { try { encryptedPrefs.edit().putString(KEY_PREFIX + npub, privKeyHex).apply() diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt index 188dd07c0..b7cd8018b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt @@ -34,7 +34,10 @@ expect class SecureKeyStorage { * @param privKeyHex The private key in hexadecimal format * @throws SecureStorageException if storage operation fails */ - suspend fun savePrivateKey(npub: String, privKeyHex: String) + suspend fun savePrivateKey( + npub: String, + privKeyHex: String, + ) /** * Retrieves a private key for the given npub. @@ -66,4 +69,7 @@ expect class SecureKeyStorage { /** * Exception thrown when secure storage operations fail. */ -class SecureStorageException(message: String, cause: Throwable? = null) : Exception(message, cause) +class SecureStorageException( + message: String, + cause: Throwable? = null, +) : Exception(message, cause) diff --git a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt b/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt index b0dc7e6d1..a8a7537f9 100644 --- a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt +++ b/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt @@ -58,7 +58,10 @@ actual class SecureKeyStorage { private var keyringAvailable: Boolean = true private var fallbackPassword: String? = null - actual suspend fun savePrivateKey(npub: String, privKeyHex: String) { + actual suspend fun savePrivateKey( + npub: String, + privKeyHex: String, + ) { withContext(Dispatchers.IO) { try { if (keyringAvailable) { @@ -111,11 +114,13 @@ actual class SecureKeyStorage { } } - actual suspend fun hasPrivateKey(npub: String): Boolean = - getPrivateKey(npub) != null + actual suspend fun hasPrivateKey(npub: String): Boolean = getPrivateKey(npub) != null // Keyring-based storage - private fun saveToKeyring(npub: String, privKeyHex: String) { + private fun saveToKeyring( + npub: String, + privKeyHex: String, + ) { val keyring = Keyring.create() keyring.setPassword(SERVICE_NAME, npub, privKeyHex) } @@ -138,7 +143,10 @@ actual class SecureKeyStorage { } // Fallback encrypted file storage - private fun saveToFallback(npub: String, privKeyHex: String) { + private fun saveToFallback( + npub: String, + privKeyHex: String, + ) { val password = getFallbackPassword() val encrypted = encryptData(privKeyHex, password) @@ -184,12 +192,12 @@ actual class SecureKeyStorage { val fallbackFile = getFallbackFile() if (!fallbackFile.exists()) return emptyMap() - return fallbackFile.readLines() + return fallbackFile + .readLines() .mapNotNull { line -> val parts = line.split(":", limit = 2) if (parts.size == 2) parts[0] to parts[1] else null - } - .toMap() + }.toMap() } private fun getFallbackFile(): File { @@ -206,7 +214,10 @@ actual class SecureKeyStorage { return fallbackPassword!! } - private fun encryptData(plaintext: String, password: String): String { + private fun encryptData( + plaintext: String, + password: String, + ): String { val salt = ByteArray(16).apply { SecureRandom().nextBytes(this) } val iv = ByteArray(IV_LENGTH).apply { SecureRandom().nextBytes(this) } @@ -222,7 +233,10 @@ actual class SecureKeyStorage { return Base64.getEncoder().encodeToString(combined) } - private fun decryptData(ciphertext: String, password: String): String { + private fun decryptData( + ciphertext: String, + password: String, + ): String { val combined = Base64.getDecoder().decode(ciphertext) val salt = combined.copyOfRange(0, 16) From 173a40394e94664d76ebb6561a92ad9f8500a1ba Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 6 Jan 2026 09:32:23 -0500 Subject: [PATCH 078/137] Smaller icon to avoid interfering with Trust scores --- .../main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt index 5f876fb25..56e05d901 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt @@ -215,13 +215,13 @@ val LightSelectedReactionBoxModifier = val DarkChannelNotePictureModifier = Modifier - .size(30.dp) + .size(25.dp) .clip(shape = CircleShape) .border(2.dp, DarkColorPalette.background, CircleShape) val LightChannelNotePictureModifier = Modifier - .size(30.dp) + .size(25.dp) .clip(shape = CircleShape) .border(2.dp, LightColorPalette.background, CircleShape) From fe0723889d42ba60c2845731788947837a067573 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Tue, 6 Jan 2026 14:34:17 +0000 Subject: [PATCH 079/137] New Crowdin translations by GitHub Action --- .../src/main/res/values-uz-rUZ/strings.xml | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/amethyst/src/main/res/values-uz-rUZ/strings.xml b/amethyst/src/main/res/values-uz-rUZ/strings.xml index cc4646375..7f89d981a 100644 --- a/amethyst/src/main/res/values-uz-rUZ/strings.xml +++ b/amethyst/src/main/res/values-uz-rUZ/strings.xml @@ -21,6 +21,7 @@ Noqonuniy xatti-harakat Boshqa sabab Ta’qib + Zo\'ravon Noma\'lum Relay belgisi Nomaʼlum muallif @@ -45,4 +46,26 @@ Sizda ommaviy kalit faol. Ommaviy kalitlar orqali faqat o‘qish mumkin. Xabar yozish uchun, maxfiy kalit bilan avtorizatsiyadan o‘ting Sizda ommaviy kalit faol. Ommaviy kalitlar orqali faqat o‘qish mumkin. Postni targ‘ib qilish uchun, maxfiy kalit bilan avtorizatsiyadan o‘ting Sizda ommaviy kalit faol. Ommaviy kalitlar orqali faqat o‘qish mumkin. Postga reaksiya qilish uchun, maxfiy kalit bilan avtorizatsiyadan o‘ting + Siz ommaviy kalitdan foydalanmoqdasiz va ommaviy kalitlar faqat o‘qish huquqiga ega. Obuna bo‘lish uchun shaxsiy kalit bilan tizimga kiring + Siz ommaviy kalitdan foydalanmoqdasiz va ommaviy kalitlar faqat o‘qish huquqiga ega. Yuklash uchun shaxsiy kalit bilan tizimga kiring + Siz ommaviy kalitdan foydalanmoqdasiz va ommaviy kalitlar faqat o‘qish huquqiga ega. Xabarlarni imzolash uchun shaxsiy kalit bilan tizimga kiring + Ruxsatsiz deshifrlash + Imzolovchi ushbu operatsiyani amalga oshirish uchun zarur boʻlgan shifrlashni ochishga ruxsat bermadi. Imzolovchi ilovangizda NIP-44 shifrlashni ochish funksiyasini yoqing va qaytadan urinib koʻring + Imzolovchi ilova topilmadi + Imzolovchi ilova oʻchirib tashlandimi? Imzolovchi oʻrnatilganligini va ushbu hisobga ega ekanligini tekshiring. Agar imzolovchi ilova oʻzgargan boʻlsa, tizimdan chiqing va qayta kiring. + Zeplar + Koʻrishlar soni + Targʻib qilish + Targ\'ib qilidi + Tahrirlangan + Tahrir № %1$s + Asl + Iqtibos qilish + Nusxa olish + Tahrir taklif qilish + Qo\'shish + "Kimga javob yozilmoqda: " + " va " + "bu kanalda: " + Profil banneri From d837fbb4c9bdf57f7b7e77a646f677c81c8845b2 Mon Sep 17 00:00:00 2001 From: davotoula Date: Tue, 6 Jan 2026 15:49:35 +0100 Subject: [PATCH 080/137] remove unused imports --- .../amethyst/model/nip51Lists/BookmarkListState.kt | 1 - .../membershipManagement/PostBookmarkListManagementScreen.kt | 2 -- 2 files changed, 3 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BookmarkListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BookmarkListState.kt index 19b2e1303..e97ab1585 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BookmarkListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BookmarkListState.kt @@ -41,7 +41,6 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn -import java.lang.reflect.Modifier.isPrivate @Stable class BookmarkListState( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt index 3ee8966aa..e38ea0a9a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt @@ -44,8 +44,6 @@ import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.BookmarkType -import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.membershipManagement.ListManagementView -import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.membershipManagement.ListManagementViewBody import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.NewListButton import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark From fd83560852fef0fc16305b383c08369a1c9f6cb2 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Tue, 6 Jan 2026 17:08:28 +0200 Subject: [PATCH 081/137] key storage implementation --- .../commons/account/AccountManager.kt | 15 +- .../vitorpamplona/amethyst/desktop/Main.kt | 7 +- .../amethyst/desktop/ui/LoginScreen.kt | 9 +- .../nip01Core/crypto/SecureKeyStorage.kt | 37 ++- .../nip01Core/crypto/SecureKeyStorage.kt | 40 +++- .../nip01Core/crypto/SecureKeyStorage.kt | 224 ++++++++++++++---- 6 files changed, 274 insertions(+), 58 deletions(-) diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt index 1b9321077..3afa19761 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt @@ -47,9 +47,22 @@ sealed class AccountState { ) : AccountState() } -class AccountManager( +class AccountManager private constructor( private val secureStorage: SecureKeyStorage, ) { + companion object { + /** + * Creates an AccountManager instance. + * + * @param context Platform-specific context (required on Android, ignored on Desktop) + * @return AccountManager instance + */ + fun create(context: Any? = null): AccountManager { + val storage = SecureKeyStorage.create(context) + return AccountManager(storage) + } + } + private val _accountState = MutableStateFlow(AccountState.LoggedOut) val accountState: StateFlow = _accountState.asStateFlow() diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt index c442390d3..eaf54d7d5 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt @@ -85,7 +85,6 @@ import com.vitorpamplona.amethyst.desktop.ui.FeedScreen import com.vitorpamplona.amethyst.desktop.ui.LoginScreen import com.vitorpamplona.amethyst.desktop.ui.NotificationsScreen import com.vitorpamplona.amethyst.desktop.ui.UserProfileScreen -import com.vitorpamplona.quartz.nip01Core.crypto.SecureKeyStorage import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -213,14 +212,14 @@ fun App( ) { var currentScreen by remember { mutableStateOf(DesktopScreen.Feed) } val relayManager = remember { DesktopRelayConnectionManager() } - val secureStorage = remember { SecureKeyStorage() } - val accountManager = remember { AccountManager(secureStorage) } + val accountManager = remember { AccountManager.create() } val accountState by accountManager.accountState.collectAsState() val scope = remember { CoroutineScope(SupervisorJob() + Dispatchers.Main) } // Try to load saved account on startup DisposableEffect(Unit) { - scope.launch { + scope.launch(Dispatchers.IO) { + // Load account on IO dispatcher to avoid blocking UI with password prompt (readLine) accountManager.loadSavedAccount() } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/LoginScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/LoginScreen.kt index d32026cef..f194564ae 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/LoginScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/LoginScreen.kt @@ -41,6 +41,7 @@ import com.vitorpamplona.amethyst.commons.account.AccountManager import com.vitorpamplona.amethyst.commons.account.AccountState import com.vitorpamplona.amethyst.commons.ui.auth.LoginCard import com.vitorpamplona.amethyst.commons.ui.auth.NewKeyWarningCard +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @Composable @@ -76,8 +77,8 @@ fun LoginScreen( LoginCard( onLogin = { keyInput -> accountManager.loginWithKey(keyInput).map { - // Save account to secure storage - scope.launch { + // Save account to secure storage (use IO dispatcher to avoid blocking UI) + scope.launch(Dispatchers.IO) { accountManager.saveCurrentAccount() onLoginSuccess() } @@ -96,8 +97,8 @@ fun LoginScreen( nsec = generatedAccount!!.nsec, onContinue = { showNewKeyDialog = false - // Save generated account - scope.launch { + // Save generated account (use IO dispatcher to avoid blocking UI) + scope.launch(Dispatchers.IO) { accountManager.saveCurrentAccount() onLoginSuccess() } diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt index f6a8754aa..771c7e971 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt @@ -29,25 +29,50 @@ import kotlinx.coroutines.withContext /** * Android implementation of SecureKeyStorage using EncryptedSharedPreferences * backed by Android Keystore (AES-256-GCM, hardware-backed when available). + * + * ## Security Features + * + * - **Hardware Security:** Uses Android Keystore (hardware-backed on supported devices with StrongBox) + * - **Encryption:** AES-256-GCM for both keys and values + * - **Key Derivation:** AES-256-SIV for preference keys, AES-256-GCM for values + * - **Application Context:** Uses applicationContext to prevent memory leaks + * - **Auto-backup Disabled:** EncryptedSharedPreferences automatically excluded from cloud backups + * + * **Note:** While the encryption keys are protected by hardware security modules (when available), + * the decrypted private keys returned by [getPrivateKey] are still subject to the String memory + * limitation described in [SecureKeyStorage]. */ -actual class SecureKeyStorage( - private val context: Context, -) { - companion object { +actual class SecureKeyStorage private actual constructor() { + actual companion object { private const val PREFS_NAME = "amethyst_secure_keys" private const val KEY_PREFIX = "privkey_" + + private lateinit var appContext: Context + + /** + * Creates a SecureKeyStorage instance for Android. + * + * @param context Android Context (will use applicationContext to avoid leaks) + * @return SecureKeyStorage instance + * @throws IllegalArgumentException if context is null or not a valid Context + */ + actual fun create(context: Any?): SecureKeyStorage { + require(context is Context) { "Android requires a valid Context" } + appContext = context.applicationContext + return SecureKeyStorage() + } } private val masterKey: MasterKey by lazy { MasterKey - .Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS) + .Builder(appContext, MasterKey.DEFAULT_MASTER_KEY_ALIAS) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build() } private val encryptedPrefs by lazy { EncryptedSharedPreferences.create( - context, + appContext, PREFS_NAME, masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt index b7cd8018b..d94171404 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt @@ -23,10 +23,41 @@ package com.vitorpamplona.quartz.nip01Core.crypto /** * Secure storage for private keys using platform-specific secure storage mechanisms. * - * Android: Android Keystore + EncryptedDataStore (AES-256-GCM, StrongBox when available) - * Desktop: OS native credential managers (macOS Keychain, Windows Credential Manager, Linux Secret Service) + * ## Platform Implementations + * + * **Android:** Android Keystore + EncryptedSharedPreferences (AES-256-GCM, StrongBox when available) + * + * **Desktop:** OS native credential managers (macOS Keychain, Windows Credential Manager, Linux Secret Service) + * with encrypted file fallback. + * + * ## Security Considerations + * + * **String Memory Limitation:** Private keys are returned as String (hexadecimal format). Unlike char arrays, + * Kotlin/JVM Strings are immutable and cannot be securely zeroed from memory after use. The private key + * will remain in memory until garbage collected, which may be delayed. This is an inherent limitation of + * the JVM platform. + * + * **Mitigation:** Private keys are only exposed through this API when needed for signing operations. + * They are encrypted at rest on all platforms. For maximum security, minimize the time private keys + * are held in memory and ensure they are dereferenced promptly after use. + * + * **Production Recommendation:** For high-security applications, consider using hardware security modules + * (HSM) or secure enclaves (Android StrongBox, iOS Secure Enclave) that never expose private keys to + * application memory. + * + * Use [SecureKeyStorage.create] factory method to create instances. */ -expect class SecureKeyStorage { +expect class SecureKeyStorage private constructor() { + companion object { + /** + * Creates a SecureKeyStorage instance. + * + * @param context Platform-specific context (required on Android, ignored on Desktop) + * @return SecureKeyStorage instance + */ + fun create(context: Any? = null): SecureKeyStorage + } + /** * Saves a private key securely for the given npub. * @@ -42,6 +73,9 @@ expect class SecureKeyStorage { /** * Retrieves a private key for the given npub. * + * **Security Warning:** The returned String cannot be securely zeroed from memory (JVM limitation). + * Dereference the returned value immediately after use to minimize exposure time. + * * @param npub The public key in npub (Bech32) format * @return The private key in hexadecimal format, or null if not found * @throws SecureStorageException if retrieval operation fails diff --git a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt b/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt index a8a7537f9..f64a8a755 100644 --- a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt +++ b/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt @@ -24,8 +24,14 @@ import com.github.javakeyring.BackendNotSupportedException import com.github.javakeyring.Keyring import com.github.javakeyring.PasswordAccessException import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import java.io.File +import java.io.RandomAccessFile +import java.nio.file.Files +import java.nio.file.StandardCopyOption +import java.nio.file.attribute.PosixFilePermission import java.security.SecureRandom import java.util.Base64 import javax.crypto.Cipher @@ -40,9 +46,30 @@ import javax.crypto.spec.SecretKeySpec * * Falls back to encrypted file storage with user-provided password if OS keyring * is unavailable. + * + * ## Fallback Storage Security + * + * When OS keyring is unavailable, the implementation uses: + * - **Encryption:** AES-256-GCM with PBKDF2 (100k iterations) + * - **File Permissions:** Owner-only read/write (600 for files, 700 for directories) on Unix systems + * - **Atomic Writes:** Temp file + atomic move to prevent corruption + * - **File Locking:** Prevents concurrent access race conditions + * + * **Password Memory Limitation:** The fallback password is stored as a String and cannot be + * securely zeroed from memory. It remains cached for the application lifetime to avoid repeated + * password prompts. This is acceptable for desktop applications where the user's session is + * already trusted, but may not be suitable for shared/multi-user systems. */ -actual class SecureKeyStorage { - companion object { +actual class SecureKeyStorage private actual constructor() { + actual companion object { + /** + * Creates a SecureKeyStorage instance for Desktop. + * + * @param context Ignored on Desktop (no context needed) + * @return SecureKeyStorage instance + */ + actual fun create(context: Any?): SecureKeyStorage = SecureKeyStorage() + private const val SERVICE_NAME = "amethyst-desktop" private const val FALLBACK_DIR = ".amethyst" private const val FALLBACK_FILE = "keys.enc" @@ -57,6 +84,7 @@ actual class SecureKeyStorage { private var keyringAvailable: Boolean = true private var fallbackPassword: String? = null + private val fallbackMutex = Mutex() // Protects concurrent access to fallback file actual suspend fun savePrivateKey( npub: String, @@ -143,52 +171,78 @@ actual class SecureKeyStorage { } // Fallback encrypted file storage - private fun saveToFallback( + private suspend fun saveToFallback( npub: String, privKeyHex: String, ) { - val password = getFallbackPassword() - val encrypted = encryptData(privKeyHex, password) + fallbackMutex.withLock { + val password = getFallbackPassword() + val encrypted = encryptData(privKeyHex, password) - val fallbackFile = getFallbackFile() - val data = loadFallbackData().toMutableMap() - data[npub] = encrypted + val fallbackFile = getFallbackFile() - fallbackFile.parentFile?.mkdirs() - fallbackFile.writeText(data.entries.joinToString("\n") { "${it.key}:${it.value}" }) - } + // Create directory with restrictive permissions + fallbackFile.parentFile?.let { dir -> + if (!dir.exists()) { + dir.mkdirs() + setRestrictivePermissions(dir) + } + } - private fun getFromFallback(npub: String): String? { - val password = fallbackPassword ?: return null // No password set yet - val data = loadFallbackData() - val encrypted = data[npub] ?: return null - - return try { - decryptData(encrypted, password) - } catch (e: Exception) { - null - } - } - - private fun deleteFromFallback(npub: String): Boolean { - val fallbackFile = getFallbackFile() - if (!fallbackFile.exists()) return false - - val data = loadFallbackData().toMutableMap() - val existed = data.remove(npub) != null - - if (existed) { - if (data.isEmpty()) { - fallbackFile.delete() - } else { - fallbackFile.writeText(data.entries.joinToString("\n") { "${it.key}:${it.value}" }) + withFileLock(fallbackFile) { + val data = loadFallbackDataUnsafe().toMutableMap() + data[npub] = encrypted + atomicWriteFallbackData(fallbackFile, data) } } - - return existed } - private fun loadFallbackData(): Map { + private suspend fun getFromFallback(npub: String): String? { + val password = fallbackPassword ?: return null // No password set yet + + return fallbackMutex.withLock { + val fallbackFile = getFallbackFile() + if (!fallbackFile.exists()) return@withLock null + + withFileLock(fallbackFile) { + val data = loadFallbackDataUnsafe() + val encrypted = data[npub] ?: return@withFileLock null + + try { + decryptData(encrypted, password) + } catch (e: Exception) { + null + } + } + } + } + + private suspend fun deleteFromFallback(npub: String): Boolean { + return fallbackMutex.withLock { + val fallbackFile = getFallbackFile() + if (!fallbackFile.exists()) return@withLock false + + withFileLock(fallbackFile) { + val data = loadFallbackDataUnsafe().toMutableMap() + val existed = data.remove(npub) != null + + if (existed) { + if (data.isEmpty()) { + fallbackFile.delete() + } else { + atomicWriteFallbackData(fallbackFile, data) + } + } + + existed + } + } + } + + /** + * Loads fallback data without locking. Caller must hold mutex and file lock. + */ + private fun loadFallbackDataUnsafe(): Map { val fallbackFile = getFallbackFile() if (!fallbackFile.exists()) return emptyMap() @@ -200,6 +254,56 @@ actual class SecureKeyStorage { }.toMap() } + /** + * Atomically writes fallback data using temp file + rename. + */ + private fun atomicWriteFallbackData( + fallbackFile: File, + data: Map, + ) { + val tempFile = File(fallbackFile.parentFile, "${fallbackFile.name}.tmp") + try { + // Write to temp file + tempFile.writeText(data.entries.joinToString("\n") { "${it.key}:${it.value}" }) + setRestrictivePermissions(tempFile) + + // Atomic rename + Files.move( + tempFile.toPath(), + fallbackFile.toPath(), + StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING, + ) + } finally { + // Clean up temp file if it still exists + if (tempFile.exists()) { + tempFile.delete() + } + } + } + + /** + * Executes block with file lock held. + */ + private fun withFileLock( + file: File, + block: () -> T, + ): T { + // Ensure lock file exists + val lockFile = File(file.parentFile, "${file.name}.lock") + lockFile.parentFile?.mkdirs() + if (!lockFile.exists()) { + lockFile.createNewFile() + setRestrictivePermissions(lockFile) + } + + return RandomAccessFile(lockFile, "rw").use { raf -> + raf.channel.lock().use { lock -> + block() + } + } + } + private fun getFallbackFile(): File { val homeDir = System.getProperty("user.home") return File(homeDir, "$FALLBACK_DIR/$FALLBACK_FILE") @@ -208,12 +312,52 @@ actual class SecureKeyStorage { private fun getFallbackPassword(): String { if (fallbackPassword == null) { println("OS keyring not available. Fallback encrypted storage requires a password.") - print("Enter master password: ") - fallbackPassword = readLine() ?: throw SecureStorageException("Password required for fallback storage") + val console = System.console() + fallbackPassword = + if (console != null) { + // Use Console.readPassword() for masked input + val password = console.readPassword("Enter master password: ") + password?.let { + val str = String(it) + it.fill('\u0000') // Clear the char array from memory + str + } ?: throw SecureStorageException("Password required for fallback storage") + } else { + // Fallback for non-interactive environments (testing, etc.) + print("Enter master password: ") + readLine() ?: throw SecureStorageException("Password required for fallback storage") + } } return fallbackPassword!! } + private fun setRestrictivePermissions(file: File) { + try { + val path = file.toPath() + // Set owner-only read/write permissions (600 for files, 700 for directories) + val permissions = + if (file.isDirectory) { + setOf( + PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, + PosixFilePermission.OWNER_EXECUTE, + ) + } else { + setOf( + PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, + ) + } + Files.setPosixFilePermissions(path, permissions) + } catch (e: UnsupportedOperationException) { + // Windows doesn't support POSIX permissions - file system security handles this + // No action needed + } catch (e: Exception) { + // Log but don't fail - permissions are a security enhancement, not critical + System.err.println("Warning: Could not set restrictive file permissions: ${e.message}") + } + } + private fun encryptData( plaintext: String, password: String, From fd47391afeb1698600e264b94b5806677f90b811 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 6 Jan 2026 10:08:29 -0500 Subject: [PATCH 082/137] Decreases the icon even further --- .../main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt index 56e05d901..a635810d9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt @@ -215,13 +215,13 @@ val LightSelectedReactionBoxModifier = val DarkChannelNotePictureModifier = Modifier - .size(25.dp) + .size(20.dp) .clip(shape = CircleShape) .border(2.dp, DarkColorPalette.background, CircleShape) val LightChannelNotePictureModifier = Modifier - .size(25.dp) + .size(20.dp) .clip(shape = CircleShape) .border(2.dp, LightColorPalette.background, CircleShape) From e26fb949331c38e8af5a61e2653a3f091bb60124 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 6 Jan 2026 17:38:47 -0500 Subject: [PATCH 083/137] Improvements on NIP-11 - Support for self, privacy_policy, terms_of_service, grasps - New UI for the Relay Information Screen - Improvements to the Debug Message - Compose-stable objects - Clickable elements for NIPs, external links, grasps, etc --- .../amethyst/model/AntiSpamFilter.kt | 4 +- .../amethyst/ui/note/NoteQuickActionMenu.kt | 8 + .../ui/note/ZapFormatterNoDecimals.kt | 6 + .../loggedIn/relays/RelayInformationScreen.kt | 1102 ++++++++++++----- amethyst/src/main/res/values/strings.xml | 45 +- .../amethyst/commons/util/TimeAgoFormatter.kt | 45 + .../nip01Core/relay/client/stats/RelayStat.kt | 50 +- .../relay/normalizer/NormalizedRelayUrl.kt | 3 + .../FlexibleIntListSerializer.kt | 14 +- .../nip11RelayInfo/Nip11RelayInformation.kt | 96 +- 10 files changed, 1008 insertions(+), 365 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt index 902a9015b..67b8afb51 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt @@ -95,7 +95,7 @@ class AntiSpamFilter { if (spammer.shouldHide() && relay != null) { Amethyst.instance.relayStats .get(relay) - .newSpam("$link1 $link2") + .newSpam(link1, link2) } flowSpam.tryEmit(AntiSpamState(this)) @@ -123,7 +123,7 @@ class AntiSpamFilter { if (spammer.shouldHide() && relay != null) { Amethyst.instance.relayStats .get(relay) - .newSpam("$link1 $link2") + .newSpam(link1, link2) } flowSpam.tryEmit(AntiSpamState(this)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt index 070403e87..c1bc1c0a1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -116,6 +116,14 @@ val njumpLink = { nip19BechAddress: String -> "https://njump.to/$nip19BechAddress" } +val nipLink = { nipNumber: String -> + "https://nostrhub.io/$nipNumber" +} + +val graspLink = { graspNumber: String -> + "https://gitworkshop.dev/danconwaydev.com/grasp/tree/master/$graspNumber.md" +} + val externalLinkForNote = { note: Note -> if (note is AddressableNote) { if (note.event?.bountyBaseReward() != null) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapFormatterNoDecimals.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapFormatterNoDecimals.kt index 2b33c9b50..e6b09d2ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapFormatterNoDecimals.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapFormatterNoDecimals.kt @@ -56,6 +56,12 @@ fun showAmountInteger(amount: BigDecimal?): String { } } +fun showAmountInteger(amount: Int?): String { + if (amount == null) return "0" + + return showAmountIntegerWithZero(BigDecimal.valueOf(amount.toLong())) +} + fun showAmountIntegerWithZero(amount: BigDecimal?): String { if (amount == null) return "0" if (amount.abs() < BigDecimal(0.01)) return "0" diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt index ce9cc9466..5e4fc7b20 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt @@ -20,65 +20,113 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Article +import androidx.compose.material.icons.automirrored.filled.ContactSupport +import androidx.compose.material.icons.automirrored.filled.Label +import androidx.compose.material.icons.automirrored.filled.List +import androidx.compose.material.icons.automirrored.filled.Message +import androidx.compose.material.icons.filled.AttachMoney +import androidx.compose.material.icons.filled.Bolt +import androidx.compose.material.icons.filled.Code +import androidx.compose.material.icons.filled.Dns +import androidx.compose.material.icons.filled.EditNote +import androidx.compose.material.icons.filled.EditOff +import androidx.compose.material.icons.filled.FilterAlt +import androidx.compose.material.icons.filled.Gavel +import androidx.compose.material.icons.filled.History +import androidx.compose.material.icons.filled.Key +import androidx.compose.material.icons.filled.Language +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.Payment +import androidx.compose.material.icons.filled.PrivacyTip +import androidx.compose.material.icons.filled.Storage +import androidx.compose.material.icons.filled.Tag +import androidx.compose.material.icons.filled.Topic +import androidx.compose.material.icons.filled.Translate +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Scaffold +import androidx.compose.material3.SuggestionChip import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -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.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.util.timeDiffAgoShortish import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled -import com.vitorpamplona.amethyst.ui.components.ClickableEmail -import com.vitorpamplona.amethyst.ui.components.ClickableUrl -import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer +import com.vitorpamplona.amethyst.ui.components.appendLink +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon import com.vitorpamplona.amethyst.ui.note.UserCompose -import com.vitorpamplona.amethyst.ui.note.timeAgo +import com.vitorpamplona.amethyst.ui.note.graspLink +import com.vitorpamplona.amethyst.ui.note.nipLink +import com.vitorpamplona.amethyst.ui.note.timeAgoNoDot import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser +import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.BackButton import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer -import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer -import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer -import com.vitorpamplona.amethyst.ui.theme.LargeRelayIconModifier -import com.vitorpamplona.amethyst.ui.theme.Size10dp +import com.vitorpamplona.amethyst.ui.theme.Size100dp import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer -import com.vitorpamplona.amethyst.ui.theme.StdPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer -import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList -import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayDebugMessage +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow +import com.vitorpamplona.quartz.nip01Core.relay.client.stats.ErrorDebugMessage +import com.vitorpamplona.quartz.nip01Core.relay.client.stats.IRelayDebugMessage +import com.vitorpamplona.quartz.nip01Core.relay.client.stats.NoticeDebugMessage +import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStat +import com.vitorpamplona.quartz.nip01Core.relay.client.stats.SpamDebugMessage import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl +import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract @Composable fun RelayInformationScreen( @@ -138,267 +186,177 @@ fun RelayInformationScreen( .toImmutableList() } - LazyColumn( - modifier = - Modifier - .padding(pad) - .consumeWindowInsets(pad) - .padding(bottom = Size10dp, start = Size10dp, end = Size10dp) - .fillMaxSize(), - ) { - item { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, - modifier = StdPadding.fillMaxWidth(), - ) { - Column { - RenderRelayIcon( - displayUrl = relay.displayUrl(), - iconUrl = relayInfo.icon, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), - pingInMs = - Amethyst.instance.relayStats - .get(relay) - .pingInMs, - iconModifier = LargeRelayIconModifier, - ) - } - - Spacer(modifier = DoubleHorzSpacer) - - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Title(relayInfo.name?.trim() ?: "") - Spacer(modifier = HalfVertSpacer) - SubtitleContent(relay.url) - } - } - } - item { - Section(stringRes(R.string.description)) - - SectionContent(relayInfo.description?.trim() ?: stringRes(R.string.no_description)) - } - relayInfo.pubkey?.let { - item { - Section(stringRes(R.string.owner)) - DisplayOwnerInformation(it, accountViewModel, nav) - } - } - relayInfo.contact?.let { - item { - Section(stringRes(R.string.contact)) - - Box(modifier = Modifier.padding(start = 10.dp)) { - if (it.startsWith("https:")) { - ClickableUrl(urlText = it, url = it) - } else if (it.startsWith("mailto:") || it.contains('@')) { - ClickableEmail(it) - } else { - SectionContent(it) - } - } - } - } - relayInfo.software?.let { - item { - Section(stringRes(R.string.software)) - - DisplaySoftwareInformation(it) - - Section(stringRes(R.string.version)) - - SectionContent(relayInfo.version ?: "") - } - } - relayInfo.supported_nips?.let { - if (it.isNotEmpty()) { - item { - Section(stringRes(R.string.supports)) - - DisplaySupportedNips(it, relayInfo.supported_nip_extensions) - } - } - } - relayInfo.fees?.admission?.let { - item { - if (it.isNotEmpty()) { - Section(stringRes(R.string.admission_fees)) - - it.forEach { item -> SectionContent("${item.amount?.div(1000) ?: 0} sats") } - } - } - } - - relayInfo.payments_url?.let { - item { - Section(stringRes(R.string.payments_url)) - - Box(modifier = Modifier.padding(start = 10.dp)) { - ClickableUrl( - urlText = it, - url = it, - ) - } - } - } - - relayInfo.limitation?.let { - item { - Section(stringRes(R.string.limitations)) - val authRequiredText = - if (it.auth_required ?: false) stringRes(R.string.yes) else stringRes(R.string.no) - - val paymentRequiredText = - if (it.payment_required ?: false) stringRes(R.string.yes) else stringRes(R.string.no) - - val restrictedWritesText = - if (it.restricted_writes ?: false) stringRes(R.string.yes) else stringRes(R.string.no) - - Column { - SectionContent( - "${stringRes(R.string.message_length)}: ${it.max_message_length ?: 0}", - ) - SectionContent( - "${stringRes(R.string.subscriptions)}: ${it.max_subscriptions ?: 0}", - ) - SectionContent("${stringRes(R.string.filters)}: ${it.max_filters ?: 0}") - SectionContent( - "${stringRes(R.string.subscription_id_length)}: ${it.max_subid_length ?: 0}", - ) - SectionContent("${stringRes(R.string.minimum_prefix)}: ${it.min_prefix ?: 0}") - SectionContent( - "${stringRes(R.string.maximum_event_tags)}: ${it.max_event_tags ?: 0}", - ) - SectionContent( - "${stringRes(R.string.content_length)}: ${it.max_content_length ?: 0}", - ) - SectionContent( - "${stringRes(R.string.max_limit)}: ${it.max_limit ?: 0}", - ) - SectionContent("${stringRes(R.string.minimum_pow)}: ${it.min_pow_difficulty ?: 0}") - SectionContent("${stringRes(R.string.auth)}: $authRequiredText") - SectionContent("${stringRes(R.string.payment)}: $paymentRequiredText") - SectionContent("${stringRes(R.string.restricted_writes)}: $restrictedWritesText") - } - } - } - relayInfo.relay_countries?.let { - item { - Section(stringRes(R.string.countries)) - - FlowRow { it.forEach { item -> SectionContent(item) } } - } - } - relayInfo.language_tags?.let { - item { - Section(stringRes(R.string.languages)) - - FlowRow { it.forEach { item -> SectionContent(item) } } - } - } - relayInfo.tags?.let { - item { - Section(stringRes(R.string.tags)) - - FlowRow { it.forEach { item -> SectionContent(item) } } - } - } - relayInfo.posting_policy?.let { - item { - Section(stringRes(R.string.posting_policy)) - - Box(Modifier.padding(10.dp)) { - ClickableUrl( - it, - it, - ) - } - } - } - - item { - Section(stringRes(R.string.relay_error_messages)) - } - - items(messages) { msg -> - Row { - RenderDebugMessage(msg, accountViewModel, nav) - } - - Spacer(modifier = StdVertSpacer) - } - } + RelayInformationBody(relay, relayInfo, Amethyst.instance.relayStats.get(relay), messages, pad, accountViewModel, nav) } } @Composable -private fun RenderDebugMessage( - msg: RelayDebugMessage, +fun RelayInformationBody( + relay: NormalizedRelayUrl, + relayInfo: Nip11RelayInformation, + relayStats: RelayStat, + messages: ImmutableList, + pad: PaddingValues, accountViewModel: AccountViewModel, - newNav: INav, + nav: INav, ) { - SelectionContainer { - val context = LocalContext.current - val color = - remember { - mutableStateOf(Color.Transparent) - } - TranslatableRichTextViewer( - content = - remember { - "${timeAgo(msg.time, context)}, ${msg.type.name}: ${msg.message}" - }, - canPreview = false, - quotesLeft = 0, - modifier = Modifier.fillMaxWidth(), - tags = EmptyTagList, - backgroundColor = color, - id = msg.hashCode().toString(), - accountViewModel = accountViewModel, - nav = newNav, - ) - } -} + LazyColumn( + modifier = + Modifier + .padding(pad) + .consumeWindowInsets(pad) + .fillMaxSize(), + contentPadding = PaddingValues(10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + // 1. Header Section + item { + RelayHeader(relay, relayStats, relayInfo, accountViewModel) + } -@Composable -@OptIn(ExperimentalLayoutApi::class) -private fun DisplaySupportedNips( - supportedNips: List, - supportedNipExtensions: List?, -) { - FlowRow { - supportedNips.forEach { item -> - val text = item.toString().padStart(2, '0') - Box(Modifier.padding(10.dp)) { - ClickableUrl( - urlText = text, - url = "https://github.com/nostr-protocol/nips/blob/master/$text.md", - ) + val targetAudience = + relayInfo.tags != null || + relayInfo.language_tags != null || + relayInfo.relay_countries != null + + if (targetAudience) { + item { SectionHeader(stringRes(R.string.target_audience)) } + item { TargetAudienceCard(relayInfo, nav) } + } + + relayInfo.pubkey?.let { + item { + SectionHeader(stringRes(R.string.owner)) + DisplayOwnerInformation(it, accountViewModel, nav) } } - supportedNipExtensions?.forEach { item -> - val text = item.padStart(2, '0') - Box(Modifier.padding(10.dp)) { - ClickableUrl( - urlText = text, - url = "https://github.com/nostr-protocol/nips/blob/master/$text.md", - ) + relayInfo.self?.let { + item { + SectionHeader(stringRes(R.string.self)) + DisplayOwnerInformation(it, accountViewModel, nav) } } + + item { SectionHeader(stringRes(R.string.policies_and_links)) } + item { PoliciesCard(relayInfo) } + + relayInfo.fees?.let { fees -> + item { SectionHeader(stringRes(R.string.fees_and_payments)) } + item { FeesCard(fees, relayInfo.payments_url) } + } + + relayInfo.limitation?.let { + item { SectionHeader(stringRes(R.string.limitations)) } + item { LimitationsCard(it) } + } + + val atLeastOneSoftware = + relayInfo.software != null || + relayInfo.version != null || + relayInfo.supported_grasps != null || + relayInfo.supported_nips != null + + if (atLeastOneSoftware) { + item { SectionHeader(stringRes(R.string.software)) } + item { SoftwareCard(relayInfo) } + } + + item { + SectionHeader(stringRes(R.string.relay_error_messages)) + } + + items(messages) { msg -> + RenderDebugMessage(msg) + + Spacer(modifier = StdVertSpacer) + } } } @Composable -private fun DisplaySoftwareInformation(software: String) { - val url = software.replace("git+", "") - Box(modifier = Modifier.padding(start = 10.dp)) { - ClickableUrl( - urlText = url, - url = url, - ) +private fun RenderDebugMessage(msg: IRelayDebugMessage) { + val context = LocalContext.current + + Column(Modifier.padding(horizontal = 12.dp)) { + Row( + modifier = Modifier.padding(vertical = 6.dp), + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + // Timestamp with Monospace font for alignment + Text( + text = timeAgoNoDot(msg.time, context), + style = MaterialTheme.typography.labelSmall.copy(fontFamily = FontFamily.Monospace), + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + ) + + // Type Tag + Text( + text = + when (msg) { + is ErrorDebugMessage -> stringRes(R.string.errors) + is NoticeDebugMessage -> stringRes(R.string.relay_notice) + is SpamDebugMessage -> stringRes(R.string.spam) + }, + style = + MaterialTheme.typography.labelSmall.copy( + fontWeight = FontWeight.Bold, + fontFamily = FontFamily.Monospace, + ), + color = + when (msg) { + is ErrorDebugMessage -> MaterialTheme.colorScheme.error + is NoticeDebugMessage -> MaterialTheme.colorScheme.primary + is SpamDebugMessage -> MaterialTheme.colorScheme.outline + }, + ) + } + + when (msg) { + is ErrorDebugMessage -> + SelectionContainer { + Text( + text = msg.message, + style = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace), + color = MaterialTheme.colorScheme.onSurface, + ) + } + is NoticeDebugMessage -> + SelectionContainer { + Text( + text = msg.message, + style = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace), + color = MaterialTheme.colorScheme.onSurface, + ) + } + is SpamDebugMessage -> + SelectionContainer { + val uri = LocalUriHandler.current + val start = stringRes(R.string.duplicated_post) + Text( + text = + remember { + buildAnnotatedString { + append(start) + append(" ") + appendLink(msg.link1) { + runCatching { + uri.openUri(msg.link1) + } + } + appendLink(msg.link2) { + runCatching { + uri.openUri(msg.link2) + } + } + } + }, + style = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace), + color = MaterialTheme.colorScheme.onSurface, + ) + } + } } } @@ -408,50 +366,616 @@ private fun DisplayOwnerInformation( accountViewModel: AccountViewModel, nav: INav, ) { - LoadUser(baseUserHex = userHex, accountViewModel) { - CrossfadeIfEnabled(it, accountViewModel = accountViewModel) { + LoadUser(baseUserHex = userHex, accountViewModel) { loadedUser -> + CrossfadeIfEnabled(loadedUser, accountViewModel = accountViewModel) { if (it != null) { - UserCompose( - baseUser = it, - accountViewModel = accountViewModel, - nav = nav, - ) + Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)) { + UserCompose( + baseUser = it, + accountViewModel = accountViewModel, + nav = nav, + ) + } } } } } @Composable -fun Title(text: String) { +private fun RelayHeader( + relay: NormalizedRelayUrl, + relayStats: RelayStat, + relayInfo: Nip11RelayInformation, + accountViewModel: AccountViewModel, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) { + RenderRelayIcon( + displayUrl = relay.displayUrl(), + iconUrl = relayInfo.icon, + loadProfilePicture = accountViewModel.settings.showProfilePictures(), + loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + pingInMs = relayStats.pingInMs, + iconModifier = + Modifier + .size(Size100dp) + .clip(shape = CircleShape), + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = relayInfo.description ?: relay.displayUrl(), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } +} + +@Composable +fun FeesCard( + fees: Nip11RelayInformation.RelayInformationFees, + payUrl: String?, +) { + OutlinedCard(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)) { + Column(modifier = Modifier.padding(16.dp)) { + fees.admission?.forEach { + FeeRow(stringRes(R.string.admission), it) + } + fees.subscription?.forEach { + FeeRow(stringRes(R.string.subscription), it) + } + fees.publication?.forEach { + FeeRow(stringRes(R.string.publication), it) + } + payUrl?.let { + val uri = LocalUriHandler.current + ClickableInfoRow(Icons.Default.Payment, stringRes(R.string.payments_url), it.removePrefix("https://")) { + runCatching { + uri.openUri(it) + } + } + } + } + } +} + +@Composable +fun LimitationsCard(lim: Nip11RelayInformation.RelayInformationLimitation) { + OutlinedCard(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)) { + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + val atLeastOneAccessControl = + lim.auth_required != null || + lim.payment_required != null || + lim.restricted_writes != null || + lim.min_pow_difficulty != null || + lim.min_prefix != null + + if (atLeastOneAccessControl) { + Column { + Text(stringRes(R.string.access_control), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary) + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) + + val yes = stringRes(R.string.yes) + val no = stringRes(R.string.no) + + lim.auth_required?.let { + InfoRow(Icons.Default.History, stringRes(R.string.auth_required), if (it) yes else no) + } + lim.payment_required?.let { + InfoRow(Icons.Default.Lock, stringRes(R.string.payment_required), if (it) yes else no) + } + lim.restricted_writes?.let { + InfoRow(Icons.Default.EditOff, stringRes(R.string.restricted_writes), if (it) yes else no) + } + + val minPoW = lim.min_pow_difficulty + + if (minPoW != null && minPoW > 0) { + InfoRow(Icons.Default.Bolt, stringRes(R.string.minimum_pow), stringRes(R.string.amount_in_bits, minPoW)) + } else { + lim.min_prefix?.let { + if (it > 0) { + InfoRow(Icons.Default.Key, stringRes(R.string.minimum_prefix), stringRes(R.string.amount_in_bits, it * 8)) + } + } + } + } + } + + val atLeastOneConnectivity = + lim.max_message_length.isNotNullAndNotZero() || + lim.max_subscriptions.isNotNullAndNotZero() || + lim.max_filters.isNotNullAndNotZero() || + lim.max_limit.isNotNullAndNotZero() || + lim.default_limit.isNotNullAndNotZero() || + lim.max_subid_length.isNotNullAndNotZero() + + if (atLeastOneConnectivity) { + Column { + Text(stringRes(R.string.connectivity), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary) + + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) + + lim.max_message_length?.let { + InfoRow(Icons.AutoMirrored.Default.Message, stringRes(R.string.max_message_length), "${it / 1024} kb") + } + lim.max_subscriptions?.let { + InfoRow(Icons.Default.Dns, stringRes(R.string.max_subs), it.toString()) + } + lim.max_filters?.let { + InfoRow(Icons.Default.FilterAlt, stringRes(R.string.max_filters_per_sub), it.toString()) + } + lim.max_limit?.let { + InfoRow(Icons.AutoMirrored.Default.List, stringRes(R.string.max_limit_events_returning), it.toString()) + } + lim.default_limit?.let { + InfoRow(Icons.AutoMirrored.Default.List, stringRes(R.string.max_limit_events_returning), it.toString()) + } + lim.max_subid_length?.let { + InfoRow(Icons.AutoMirrored.Default.Label, stringRes(R.string.max_subid_length), it.toString()) + } + } + } + + val atLeastOneContentSize = + lim.max_event_tags != null || + lim.max_content_length != null + + if (atLeastOneContentSize) { + Column { + Text(stringRes(R.string.content_size), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary) + + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) + + lim.max_event_tags?.let { + InfoRow(Icons.Default.Tag, stringRes(R.string.maximum_event_tags), it.toString()) + } + + lim.max_content_length?.let { + InfoRow(Icons.AutoMirrored.Default.Article, stringRes(R.string.max_content_length), "${it / 1024} kb") + } + } + } + + val atLeastOneRestriction = + lim.created_at_lower_limit.isNotNullAndNotZero() || + lim.created_at_upper_limit.isNotNullAndNotZero() + + if (atLeastOneRestriction) { + Column { + Text(stringRes(R.string.event_retention), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary) + + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) + + lim.created_at_lower_limit?.let { + if (it > 0) { + InfoRow(Icons.Default.History, stringRes(R.string.discards_older_than), stringRes(R.string.time_in_the_past, timeDiffAgoShortish(it))) + } + } + lim.created_at_upper_limit?.let { + if (it > 0) { + InfoRow(Icons.Default.History, stringRes(R.string.accepts_up_to), stringRes(R.string.time_in_the_future, timeDiffAgoShortish(it))) + } + } + } + } + } + } +} + +@OptIn(ExperimentalContracts::class) +fun Int?.isNotNullAndNotZero(): Boolean { + contract { + returns(true) implies (this@isNotNullAndNotZero != null) + } + + return this != null && this != 0 +} + +@Composable +fun SoftwareCard(relayInfo: Nip11RelayInformation) { + OutlinedCard(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)) { + Column(modifier = Modifier.padding(16.dp)) { + val uri = LocalUriHandler.current + relayInfo.software?.let { + if (it.contains("https://")) { + ClickableInfoRow(Icons.Default.Code, stringRes(R.string.software), it.removePrefix("git+https://").removePrefix("https://")) { + runCatching { + uri.openUri(it.removePrefix("git+")) + } + } + } else { + InfoRow(Icons.Default.Code, stringRes(R.string.software), it) + } + } + + relayInfo.version?.let { + InfoRow(Icons.Default.Storage, stringRes(R.string.version), it) + } + + relayInfo.supported_nips?.let { nips -> + Text( + stringRes(R.string.supports), + modifier = Modifier.padding(top = 8.dp), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary, + ) + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) + + val uri = LocalUriHandler.current + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + nips.forEach { nip -> + val nipStr = nip.padStart(2, '0') + SuggestionChip( + onClick = { + runCatching { + uri.openUri(nipLink(nipStr)) + } + }, + label = { + Text(nipStr) + }, + ) + } + } + } + + relayInfo.supported_grasps?.let { grasps -> + Text( + stringRes(R.string.supported_grasps), + modifier = Modifier.padding(top = 8.dp), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary, + ) + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) + + val uri = LocalUriHandler.current + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + grasps.forEach { grasp -> + val graspStr = grasp.padStart(2, '0') + SuggestionChip( + onClick = { + runCatching { + uri.openUri(graspLink(graspStr)) + } + }, + label = { + Text(graspStr) + }, + ) + } + } + } + } + } +} + +@Composable +fun TargetAudienceCard( + relay: Nip11RelayInformation, + nav: INav, +) { + OutlinedCard(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)) { + Column(modifier = Modifier.padding(16.dp)) { + relay.tags?.let { tags -> + if (tags.size > 2) { + Column { + Text(stringRes(R.string.topics), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary) + + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) + + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + tags.forEach { tag -> + SuggestionChip( + onClick = { nav.nav(Route.Hashtag(tag)) }, + label = { + Text(tag) + }, + ) + } + } + } + } else if (tags.isNotEmpty()) { + InfoRow(Icons.Default.Topic, stringRes(R.string.topics), tags.joinToString()) + } + } + relay.relay_countries?.let { countries -> + val allCountries = stringRes(R.string.all_countries) + if (countries.size > 2) { + Column { + Text(stringRes(R.string.countries), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary) + + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) + + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + countries.forEach { country -> + if (country == "*") { + SuggestionChip( + onClick = { }, + label = { + Text(allCountries) + }, + ) + } else { + SuggestionChip( + onClick = { }, + label = { + Text(country) + }, + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + } + } else if (countries.isNotEmpty()) { + InfoRow( + Icons.Default.Language, + stringRes(R.string.countries), + countries.joinToString { + if (it == "*") allCountries else it + }, + ) + } + } + relay.language_tags?.let { languages -> + val allLang = stringRes(R.string.all_languages) + if (languages.size > 2) { + Column { + Text(stringRes(R.string.languages), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary) + + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) + + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + languages.forEach { lang -> + if (lang == "*") { + SuggestionChip( + onClick = { }, + label = { + Text(allLang) + }, + ) + } else { + SuggestionChip( + onClick = { }, + label = { + Text(lang) + }, + ) + } + } + } + } + } else if (languages.isNotEmpty()) { + InfoRow( + Icons.Default.Translate, + stringRes(R.string.languages), + languages.joinToString { + if (it == "*") allLang else it + }, + ) + } + } + } + } +} + +@Composable +fun PoliciesCard(relay: Nip11RelayInformation) { + OutlinedCard(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)) { + Column(modifier = Modifier.padding(16.dp)) { + val uri = LocalUriHandler.current + relay.contact?.let { + if (it.contains("@")) { + ClickableInfoRow(Icons.AutoMirrored.Default.ContactSupport, stringRes(R.string.contact), it) { + runCatching { + uri.openUri("mailto:$it") + } + } + } else { + InfoRow(Icons.AutoMirrored.Default.ContactSupport, stringRes(R.string.contact), it) + } + } + + relay.posting_policy?.let { + ClickableInfoRow(Icons.Default.EditNote, stringRes(R.string.posting_policy), it) { + runCatching { + uri.openUri(it) + } + } + } + + val pp = relay.privacy_policy + + if (pp != null) { + ClickableInfoRow(Icons.Default.PrivacyTip, stringRes(R.string.privacy_policy), pp.removePrefix("https://")) { + runCatching { + uri.openUri(pp) + } + } + } else { + InfoRow(Icons.Default.PrivacyTip, stringRes(R.string.privacy_policy), stringRes(R.string.not_available_acronym)) + } + + val ts = relay.terms_of_service + if (ts != null) { + ClickableInfoRow(Icons.Default.Gavel, stringRes(R.string.terms_and_conditions), ts.removePrefix("https://")) { + runCatching { + uri.openUri(ts) + } + } + } else { + InfoRow(Icons.Default.Gavel, stringRes(R.string.terms_and_conditions), stringRes(R.string.not_available_acronym)) + } + } + } +} + +@Composable +fun SectionHeader(title: String) { Text( - text = text, + text = title, + style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, - fontSize = 24.sp, + modifier = Modifier.padding(top = 5.dp), + color = MaterialTheme.colorScheme.primary, ) } @Composable -fun SubtitleContent(text: String) { - Text( - text = text, - ) +private fun InfoRow( + icon: ImageVector, + label: String, + value: String, +) { + Row( + modifier = Modifier.padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(icon, contentDescription = null, modifier = Modifier.size(18.dp), tint = MaterialTheme.colorScheme.secondary) + Spacer(Modifier.width(12.dp)) + Text(text = label, style = MaterialTheme.typography.labelLarge, maxLines = 1) + Text(text = value, textAlign = TextAlign.End, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f), overflow = TextOverflow.Ellipsis, maxLines = 1) + } } @Composable -fun Section(text: String) { - Spacer(modifier = DoubleVertSpacer) - Text( - text = text, - fontWeight = FontWeight.Bold, - fontSize = 20.sp, - ) - Spacer(modifier = DoubleVertSpacer) +private fun ClickableInfoRow( + icon: ImageVector, + label: String, + value: String, + onClickValue: () -> Unit, +) { + Row( + modifier = Modifier.padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(icon, contentDescription = null, modifier = Modifier.size(18.dp), tint = MaterialTheme.colorScheme.secondary) + Spacer(Modifier.width(12.dp)) + Text(text = label, style = MaterialTheme.typography.labelLarge, maxLines = 1) + Text(text = value, textAlign = TextAlign.End, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.SemiBold, modifier = Modifier.clickable(onClick = onClickValue).weight(1f), overflow = TextOverflow.Ellipsis, maxLines = 1) + } } @Composable -fun SectionContent(text: String) { - Text( - modifier = Modifier.padding(start = 10.dp), - text = text, - ) +fun FeeRow( + label: String, + fee: Nip11RelayInformation.RelayInformationFee, +) { + fee.amount?.let { + val period = fee.period + val combinedLabel = + if (period != null) { + label + " (${timeDiffAgoShortish(period)})" + } else { + label + } + + if (fee.unit == "msats") { + InfoRow(Icons.Default.AttachMoney, combinedLabel, "${it / 1000} sats") + } else { + InfoRow(Icons.Default.AttachMoney, combinedLabel, "$it ${fee.unit}") + } + } +} + +@Composable +@Preview(showBackground = true, name = "Nost.wine Relay Info", device = "spec:width=2160px,height=5640px,dpi=440") +fun RelayHeaderPreview() { + ThemeComparisonRow { + RelayInformationBody( + relay = NormalizedRelayUrl("wss://nostr.wine/"), + relayInfo = + Nip11RelayInformation( + name = "Nostr.wine", + icon = "https://image.nostr.build/30acdce4a81926f386622a07343228ae99fa68d012d54c538c0b2129dffe400c.png", + description = "A paid nostr relay for wine enthusiasts and everyone else", + software = "https://nostr.wine", + contact = "wino@nostr.wine", + version = "0.3.3", + self = "4918eb332a41b71ba9a74b1dc64276cfff592e55107b93baae38af3520e55975", + pubkey = "4918eb332a41b71ba9a74b1dc64276cfff592e55107b93baae38af3520e55975", + payments_url = "https://nostr.wine/invoices", + privacy_policy = "https://nostr.wine/terms", + terms_of_service = "https://nostr.wine/terms", + tags = listOf("Bitcoin", "Amethyst"), + supported_nips = listOf("1", "2", "4", "9", "11", "40", "42", "50", "70", "77"), + relay_countries = listOf("*"), + language_tags = listOf("*"), + limitation = + Nip11RelayInformation.RelayInformationLimitation( + auth_required = false, + created_at_lower_limit = 94608000, + created_at_upper_limit = 300, + max_event_tags = 4000, + max_limit = 1000, + default_limit = 20, + max_message_length = 524288, + max_subid_length = 71, + max_subscriptions = 50, + min_pow_difficulty = 0, + payment_required = true, + restricted_writes = true, + ), + fees = + Nip11RelayInformation.RelayInformationFees( + admission = + listOf( + Nip11RelayInformation.RelayInformationFee( + amount = 3000, + unit = "msats", + period = 2628003, + ), + Nip11RelayInformation.RelayInformationFee( + amount = 8000, + unit = "sats", + period = 7884009, + ), + ), + ), + supported_grasps = listOf("GRASP-01"), + ), + pad = PaddingValues(0.dp), + relayStats = RelayStat(), + messages = + persistentListOf( + NoticeDebugMessage( + time = TimeUtils.now(), + message = "Subscription closed: AccountNotificationsEoseFromRandomRelaysManagerugZU9o auth-required: At least one matching event requires AUTH", + ), + ErrorDebugMessage( + time = TimeUtils.now() - 24000, + message = "No such subscription", + ), + SpamDebugMessage( + time = TimeUtils.now() - 24000, + link1 = "http://test1.com", + link2 = "http://test2.com", + ), + ), + accountViewModel = mockAccountViewModel(), + nav = EmptyNav(), + ) + } } diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 9542f59a9..212407e46 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -17,6 +17,8 @@ Could not decrypt the message Group Picture Explicit Content + Relay Notice + Duplicated Post Spam The number of spamming events coming from this relay Impersonation @@ -133,6 +135,7 @@ Relay Address Posts Bytes + Error Errors The number of connection errors in this session Home Feed @@ -747,28 +750,64 @@ The amount in bytes that was received from this relay, including filters and events An error occurred trying to get relay information from %1$s Owner + Service Key + Running %1$s + Running %1$s (%2$s) Version Software Contact Supported NIPs + Supported Grasps Admission Fees + Admission + Subscription + Publication + Payments %1$s Payments url + Target Audience + Policies & Links + Fees & Payments Limitations Countries Languages Tags + Topics + All Countries + All Languages Posting policy + Privacy Policy + Terms & Conditions + N/A Errors and Notices from this Relay Message length Subscriptions Filters Subscription id length - Minimum prefix - Maximum event tags + Min Prefix + Max Event Tags Content length - Minimum PoW + Max Content Length + Discards older than + Accepts up to + %1$s in the future + %1$s ago + %1$s zeros + %1$s bits + Event Retention + Content Size + Connectivity + Access Control + Min PoW Difficulty Auth + Auth Required Payment + Payment Required + Max Message Length + Max Subscriptions + Max Filters per Sub + Max Limit (Events Returning) + Default Limit (Events Returning) + Max SubID Length Cashu Token Redeem Send to Zap Wallet diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/TimeAgoFormatter.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/TimeAgoFormatter.kt index 88ef514b9..12307ecd2 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/TimeAgoFormatter.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/TimeAgoFormatter.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.commons.util +import com.sun.org.apache.xalan.internal.lib.ExsltDatetime.time import com.vitorpamplona.quartz.utils.TimeUtils import java.text.SimpleDateFormat import java.util.Locale @@ -81,6 +82,50 @@ fun timeAgo( } } +fun timeDiffAgoLong(timeDifference: Int): String = + when { + timeDifference > TimeUtils.ONE_YEAR -> { + (timeDifference / TimeUtils.ONE_YEAR).toString() + " years" + } + timeDifference > TimeUtils.ONE_MONTH -> { + (timeDifference / TimeUtils.ONE_MONTH).toString() + " months" + } + timeDifference > TimeUtils.ONE_DAY -> { + (timeDifference / TimeUtils.ONE_DAY).toString() + " days" + } + timeDifference > TimeUtils.ONE_HOUR -> { + (timeDifference / TimeUtils.ONE_HOUR).toString() + " hours" + } + timeDifference > TimeUtils.ONE_MINUTE -> { + (timeDifference / TimeUtils.ONE_MINUTE).toString() + " minutes" + } + else -> { + "now" + } + } + +fun timeDiffAgoShortish(timeDifference: Int): String = + when { + timeDifference > TimeUtils.ONE_YEAR -> { + (timeDifference / TimeUtils.ONE_YEAR).toString() + " yrs" + } + timeDifference > TimeUtils.ONE_MONTH -> { + (timeDifference / TimeUtils.ONE_MONTH).toString() + " mos" + } + timeDifference > TimeUtils.ONE_DAY -> { + (timeDifference / TimeUtils.ONE_DAY).toString() + " days" + } + timeDifference > TimeUtils.ONE_HOUR -> { + (timeDifference / TimeUtils.ONE_HOUR).toString() + " hrs" + } + timeDifference > TimeUtils.ONE_MINUTE -> { + (timeDifference / TimeUtils.ONE_MINUTE).toString() + " mins" + } + else -> { + "now" + } + } + /** * Formats a Unix timestamp as a date string. * For recent dates (< 1 day), returns the provided today string. diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/stats/RelayStat.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/stats/RelayStat.kt index c7576094f..b51b8428c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/stats/RelayStat.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/stats/RelayStat.kt @@ -21,8 +21,10 @@ package com.vitorpamplona.quartz.nip01Core.relay.client.stats import androidx.collection.LruCache +import androidx.compose.runtime.Stable import com.vitorpamplona.quartz.utils.TimeUtils +@Stable class RelayStat( var receivedBytes: Int = 0, var sentBytes: Int = 0, @@ -31,12 +33,11 @@ class RelayStat( var pingInMs: Int = 0, var compression: Boolean = false, ) { - val messages = LruCache(100) + val messages = LruCache(100) fun newNotice(notice: String?) { val debugMessage = - RelayDebugMessage( - type = RelayDebugMessageType.NOTICE, + NoticeDebugMessage( message = notice ?: "No error message provided", ) @@ -47,8 +48,7 @@ class RelayStat( errorCounter++ val debugMessage = - RelayDebugMessage( - type = RelayDebugMessageType.ERROR, + ErrorDebugMessage( message = error ?: "No error message provided", ) @@ -63,27 +63,35 @@ class RelayStat( sentBytes += bytesUsedInMemory } - fun newSpam(spamDescriptor: String) { + fun newSpam( + link1: String, + link2: String, + ) { spamCounter++ - val debugMessage = - RelayDebugMessage( - type = RelayDebugMessageType.SPAM, - message = spamDescriptor, - ) + val debugMessage = SpamDebugMessage(link1, link2) messages.put(debugMessage, debugMessage) } } -class RelayDebugMessage( - val type: RelayDebugMessageType, - val message: String, - val time: Long = TimeUtils.now(), -) - -enum class RelayDebugMessageType { - SPAM, - NOTICE, - ERROR, +@Stable +sealed interface IRelayDebugMessage { + val time: Long } + +class SpamDebugMessage( + val link1: String, + val link2: String, + override val time: Long = TimeUtils.now(), +) : IRelayDebugMessage + +class NoticeDebugMessage( + val message: String, + override val time: Long = TimeUtils.now(), +) : IRelayDebugMessage + +class ErrorDebugMessage( + val message: String, + override val time: Long = TimeUtils.now(), +) : IRelayDebugMessage diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/normalizer/NormalizedRelayUrl.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/normalizer/NormalizedRelayUrl.kt index dd9ec6f7c..1fb5ef244 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/normalizer/NormalizedRelayUrl.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/normalizer/NormalizedRelayUrl.kt @@ -20,6 +20,9 @@ */ package com.vitorpamplona.quartz.nip01Core.relay.normalizer +import androidx.compose.runtime.Stable + +@Stable data class NormalizedRelayUrl( val url: String, ) : Comparable { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip11RelayInfo/FlexibleIntListSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip11RelayInfo/FlexibleIntListSerializer.kt index f29779ceb..eaf719fbc 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip11RelayInfo/FlexibleIntListSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip11RelayInfo/FlexibleIntListSerializer.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.quartz.nip11RelayInfo import com.vitorpamplona.quartz.utils.Log +import com.vitorpamplona.quartz.utils.text import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.builtins.ListSerializer @@ -32,15 +33,14 @@ import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonDecoder import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.int import kotlinx.serialization.json.jsonPrimitive -object FlexibleIntListSerializer : KSerializer?> { - private val listSerializer = ListSerializer(Int.serializer()) +object FlexibleIntListSerializer : KSerializer?> { + private val listSerializer = ListSerializer(String.serializer()) override val descriptor: SerialDescriptor = listSerializer.descriptor - override fun deserialize(decoder: Decoder): List? { + override fun deserialize(decoder: Decoder): List? { require(decoder is JsonDecoder) { "This serializer can only be used with Json format" } return when (val element = decoder.decodeJsonElement()) { @@ -51,7 +51,7 @@ object FlexibleIntListSerializer : KSerializer?> { is JsonArray -> { element.mapNotNull { arrayElement -> try { - arrayElement.jsonPrimitive.int + arrayElement.jsonPrimitive.text } catch (e: Exception) { // Skip elements that aren't valid integers (strings, booleans, floats, etc.) Log.w("FlexibleIntListSerializer", "Invalid element in array: $arrayElement", e) @@ -63,7 +63,7 @@ object FlexibleIntListSerializer : KSerializer?> { // Handle single integer format (malformed but found in the wild): 1 is JsonPrimitive if !element.isString -> { try { - listOf(element.int) + listOf(element.text) } catch (e: Exception) { // Can't parse as integer (e.g., float, boolean), treat as missing data Log.w("FlexibleIntListSerializer", "Invalid primitive: $element", e) @@ -79,7 +79,7 @@ object FlexibleIntListSerializer : KSerializer?> { @OptIn(ExperimentalSerializationApi::class) override fun serialize( encoder: Encoder, - value: List?, + value: List?, ) { if (value == null) { encoder.encodeNull() diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip11RelayInfo/Nip11RelayInformation.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip11RelayInfo/Nip11RelayInformation.kt index 593f20494..cdcaad58d 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip11RelayInfo/Nip11RelayInformation.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip11RelayInfo/Nip11RelayInformation.kt @@ -21,19 +21,22 @@ package com.vitorpamplona.quartz.nip11RelayInfo import androidx.compose.runtime.Stable +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.JsonMapper import kotlinx.serialization.Serializable +@Stable @Serializable class Nip11RelayInformation( val id: String? = null, val name: String? = null, val description: String? = null, val icon: String? = null, - val pubkey: String? = null, + val pubkey: HexKey? = null, + val self: HexKey? = null, val contact: String? = null, @Serializable(with = FlexibleIntListSerializer::class) - val supported_nips: List? = null, + val supported_nips: List? = null, val supported_nip_extensions: List? = null, val software: String? = null, val version: String? = null, @@ -42,53 +45,60 @@ class Nip11RelayInformation( val language_tags: List? = null, val tags: List? = null, val posting_policy: String? = null, + val privacy_policy: String? = null, + val terms_of_service: String? = null, val payments_url: String? = null, val retention: List? = null, val fees: RelayInformationFees? = null, val nip50: List? = null, + val supported_grasps: List? = null, ) { companion object { fun fromJson(json: String): Nip11RelayInformation = JsonMapper.fromJson(json) } + + @Stable + @Serializable + class RelayInformationFee( + val amount: Int? = null, + val unit: String? = null, + val period: Int? = null, + val kinds: List? = null, + ) + + @Stable + @Serializable + class RelayInformationFees( + val admission: List? = null, + val subscription: List? = null, + val publication: List? = null, + ) + + @Stable + @Serializable + class RelayInformationLimitation( + val max_message_length: Int? = null, + val max_subscriptions: Int? = null, + val max_filters: Int? = null, + val max_limit: Int? = null, + val default_limit: Int? = null, + val max_subid_length: Int? = null, + val min_prefix: Int? = null, + val max_event_tags: Int? = null, + val max_content_length: Int? = null, + val min_pow_difficulty: Int? = null, + val auth_required: Boolean? = null, + val payment_required: Boolean? = null, + val restricted_writes: Boolean? = null, + val created_at_lower_limit: Int? = null, + val created_at_upper_limit: Int? = null, + ) + + @Stable + @Serializable + class RelayInformationRetentionData( + val kinds: ArrayList? = null, + val time: Int? = null, + val count: Int? = null, + ) } - -@Stable -@Serializable -class RelayInformationFee( - val amount: Int? = null, - val unit: String? = null, - val period: Int? = null, - val kinds: List? = null, -) - -@Serializable -class RelayInformationFees( - val admission: List? = null, - val subscription: List? = null, - val publication: List? = null, -) - -@Serializable -class RelayInformationLimitation( - val max_message_length: Int? = null, - val max_subscriptions: Int? = null, - val max_filters: Int? = null, - val max_limit: Int? = null, - val max_subid_length: Int? = null, - val min_prefix: Int? = null, - val max_event_tags: Int? = null, - val max_content_length: Int? = null, - val min_pow_difficulty: Int? = null, - val auth_required: Boolean? = null, - val payment_required: Boolean? = null, - val restricted_writes: Boolean? = null, - val created_at_lower_limit: Int? = null, - val created_at_upper_limit: Int? = null, -) - -@Serializable -class RelayInformationRetentionData( - val kinds: ArrayList? = null, - val time: Int? = null, - val count: Int? = null, -) From 3c85220c7c50e31adc397f2d5cb5ee2f323e3286 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 6 Jan 2026 18:05:30 -0500 Subject: [PATCH 084/137] fixes package name --- .../com/vitorpamplona/amethyst/ui/layouts/DisappearingTopBar.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingTopBar.kt index 26c9a82be..c701fb2a5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingTopBar.kt @@ -18,7 +18,7 @@ * 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.myapplication +package com.vitorpamplona.amethyst.ui.layouts import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.AnimationState From f33e2fbcfb22717c0218566b3dd87a97d2c0c9ce Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 6 Jan 2026 18:06:24 -0500 Subject: [PATCH 085/137] Disables top bar reappearance to avoid the feed moving when coming back from page to page. --- .../vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt | 2 -- .../com/vitorpamplona/amethyst/ui/layouts/DisappearingTopBar.kt | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt index 9cd3b289d..9525b7ef2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt @@ -34,8 +34,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.myapplication.DisappearingBottomBar import com.vitorpamplona.myapplication.DisappearingFloatingButton -import com.vitorpamplona.myapplication.DisappearingTopBar -import com.vitorpamplona.myapplication.enterAlwaysScrollBehavior @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingTopBar.kt index c701fb2a5..d03aca957 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingTopBar.kt @@ -58,7 +58,7 @@ fun DisappearingTopBar( scrollBehavior: CustomEnterAlwaysScrollBehavior, content: @Composable (ColumnScope.() -> Unit), ) { - ResetDisappearingOnResume(scrollBehavior) + // ResetDisappearingOnResume(scrollBehavior) // Set up support for resizing the top app bar when vertically dragging the bar itself. val appBarDragModifier = From 93b139da622d24455e359f11fc4f448b49507b2c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 6 Jan 2026 18:39:51 -0500 Subject: [PATCH 086/137] Fixes test case --- .../quartz/nip11RelayInfo/Nip11RelayInformationTest.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip11RelayInfo/Nip11RelayInformationTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip11RelayInfo/Nip11RelayInformationTest.kt index 3f8653f2d..75e267810 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip11RelayInfo/Nip11RelayInformationTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip11RelayInfo/Nip11RelayInformationTest.kt @@ -74,7 +74,7 @@ class Nip11RelayInformationTest { assertEquals("purplepag.es", info.name) assertEquals("Nostr's Purple Pages", info.description) assertEquals("fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52", info.pubkey) - assertEquals(listOf(1, 2, 9, 11), info.supported_nips) + assertEquals(listOf("1", "2", "9", "11"), info.supported_nips) assertEquals("1.0.4", info.version) assertEquals("git+https://github.com/hoytech/strfry.git", info.software) } @@ -88,7 +88,7 @@ class Nip11RelayInformationTest { assertEquals("Nostr's Purple Pages", info.description) assertEquals("fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52", info.pubkey) // Single integer should be converted to a list with one element - assertEquals(listOf(1), info.supported_nips) + assertEquals(listOf("1"), info.supported_nips) assertEquals("1.0.4", info.version) assertEquals("git+https://github.com/hoytech/strfry.git", info.software) } @@ -132,7 +132,7 @@ class Nip11RelayInformationTest { assertNotNull(info) assertEquals("test relay", info.name) // Should skip invalid elements and keep only valid integers - assertEquals(listOf(1, 3, 11), info.supported_nips) + assertEquals(listOf("1", "3", "11"), info.supported_nips) } @Test @@ -204,6 +204,6 @@ class Nip11RelayInformationTest { assertNotNull(info) assertEquals("test relay", info.name) - assertEquals((1..100).toList(), info.supported_nips) + assertEquals((1..100).map { it.toString() }, info.supported_nips) } } From 109150e693f4da23a66198d2251bb8ce50efe3fe Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 6 Jan 2026 18:53:18 -0500 Subject: [PATCH 087/137] These logs make everything go slow :( --- .../com/vitorpamplona/amethyst/service/logging/Logging.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/logging/Logging.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/logging/Logging.kt index 3175a2824..be4565568 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/logging/Logging.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/logging/Logging.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.service.logging import android.os.Build -import android.os.Looper import android.os.StrictMode import android.os.StrictMode.ThreadPolicy import android.os.StrictMode.VmPolicy @@ -59,8 +58,8 @@ class Logging { }.penaltyLog() .build(), ) - Looper.getMainLooper().setMessageLogging(LogMonitor()) - ChoreographerHelper.start() + // Looper.getMainLooper().setMessageLogging(LogMonitor()) + // ChoreographerHelper.start() // Enable recomposition tracking ONLY in debug builds ComposeStabilityAnalyzer.setEnabled(BuildConfig.DEBUG) From b65546487d3fcbe556daff6c9eb7f61dd1d1c1e1 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 6 Jan 2026 18:57:56 -0500 Subject: [PATCH 088/137] Adjusts tests to be more flexible when reading NIPs from NIP-11 --- .../quartz/nip11RelayInfo/Nip11RelayInformationTest.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip11RelayInfo/Nip11RelayInformationTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip11RelayInfo/Nip11RelayInformationTest.kt index 75e267810..358474f3f 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip11RelayInfo/Nip11RelayInformationTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip11RelayInfo/Nip11RelayInformationTest.kt @@ -132,7 +132,7 @@ class Nip11RelayInformationTest { assertNotNull(info) assertEquals("test relay", info.name) // Should skip invalid elements and keep only valid integers - assertEquals(listOf("1", "3", "11"), info.supported_nips) + assertEquals(listOf("1", "invalid", "3", "4.5", "true", "11"), info.supported_nips) } @Test @@ -144,7 +144,7 @@ class Nip11RelayInformationTest { assertNotNull(info) assertEquals("test relay", info.name) // All elements invalid, should result in empty list - assertEquals(emptyList(), info.supported_nips) + assertEquals(listOf("one", "two", "three"), info.supported_nips) } @Test @@ -156,7 +156,7 @@ class Nip11RelayInformationTest { assertNotNull(info) assertEquals("test relay", info.name) // Cannot parse float as integer, should be null - assertNull(info.supported_nips) + assertEquals(listOf("1.5"), info.supported_nips) } @Test @@ -168,7 +168,7 @@ class Nip11RelayInformationTest { assertNotNull(info) assertEquals("test relay", info.name) // Cannot parse boolean as integer, should be null - assertNull(info.supported_nips) + assertEquals(listOf("true"), info.supported_nips) } @Test From deb5da48666451f9a261734833859548f37a8c8b Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Wed, 7 Jan 2026 00:00:42 +0000 Subject: [PATCH 089/137] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-pl-rPL/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index 11b8200fe..95a8ed79b 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -357,6 +357,8 @@ Zablokuj Ręczny podział zapów Zakładki + Domyślne zakładki + Twoje domyślne zakładki obsługiwane przez wielu klientów Projekty Prywatne Zakładki Publiczne zakładki From a9141763e19d4e34c4f466c3297e53181a03f9f1 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Wed, 7 Jan 2026 07:12:00 +0200 Subject: [PATCH 090/137] previous PR fixes --- commons/build.gradle.kts | 10 ++++ .../commons/ui/components/LoadingState.kt | 24 ++++++--- .../commons/ui/screens/PlaceholderScreens.kt | 14 ++--- .../moko-resources/base/strings.xml | 53 +++++++++++++++++++ .../amethyst/commons/ui/auth/KeyInputField.kt | 26 +++++++-- .../amethyst/commons/ui/auth/LoginCard.kt | 20 +++++-- .../commons/ui/auth/NewKeyWarningCard.kt | 24 ++++++--- .../amethyst/desktop/ui/LoginScreen.kt | 6 ++- gradle/libs.versions.toml | 4 ++ 9 files changed, 153 insertions(+), 28 deletions(-) create mode 100644 commons/src/commonMain/moko-resources/base/strings.xml diff --git a/commons/build.gradle.kts b/commons/build.gradle.kts index d93e36172..427ed28b4 100644 --- a/commons/build.gradle.kts +++ b/commons/build.gradle.kts @@ -5,6 +5,7 @@ plugins { alias(libs.plugins.androidLibrary) alias(libs.plugins.jetbrainsComposeCompiler) alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.mokoResources) } android { @@ -75,6 +76,10 @@ kotlin { // Immutable collections api(libs.kotlinx.collections.immutable) + + // Moko Resources for KMP string resources + api(libs.moko.resources) + api(libs.moko.resources.compose) } } @@ -124,3 +129,8 @@ kotlin { } } } + +multiplatformResources { + resourcesPackage.set("com.vitorpamplona.amethyst.commons") + resourcesClassName.set("SharedRes") +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/LoadingState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/LoadingState.kt index b549466dd..2ed9eb625 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/LoadingState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/LoadingState.kt @@ -34,6 +34,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.SharedRes +import dev.icerock.moko.resources.compose.stringResource /** * A centered loading state with a progress indicator and message. @@ -67,8 +69,10 @@ fun EmptyState( modifier: Modifier = Modifier, description: String? = null, onRefresh: (() -> Unit)? = null, - refreshLabel: String = "Refresh", + refreshLabel: String? = null, ) { + val actualRefreshLabel = refreshLabel ?: stringResource(SharedRes.strings.action_refresh) + Column( modifier = modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, @@ -90,7 +94,7 @@ fun EmptyState( if (onRefresh != null) { Spacer(Modifier.height(16.dp)) OutlinedButton(onClick = onRefresh) { - Text(refreshLabel) + Text(actualRefreshLabel) } } } @@ -104,8 +108,10 @@ fun ErrorState( message: String, modifier: Modifier = Modifier, onRetry: (() -> Unit)? = null, - retryLabel: String = "Try Again", + retryLabel: String? = null, ) { + val actualRetryLabel = retryLabel ?: stringResource(SharedRes.strings.action_try_again) + Column( modifier = modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, @@ -119,7 +125,7 @@ fun ErrorState( if (onRetry != null) { Spacer(Modifier.height(16.dp)) Button(onClick = onRetry) { - Text(retryLabel) + Text(actualRetryLabel) } } } @@ -131,11 +137,13 @@ fun ErrorState( @Composable fun FeedEmptyState( modifier: Modifier = Modifier, - title: String = "Feed is empty", + title: String? = null, onRefresh: (() -> Unit)? = null, ) { + val actualTitle = title ?: stringResource(SharedRes.strings.feed_empty) + EmptyState( - title = title, + title = actualTitle, modifier = modifier, onRefresh = onRefresh, ) @@ -150,8 +158,10 @@ fun FeedErrorState( modifier: Modifier = Modifier, onRetry: (() -> Unit)? = null, ) { + val formattedMessage = stringResource(SharedRes.strings.error_loading_feed).format(errorMessage) + ErrorState( - message = "Error loading feed: $errorMessage", + message = formattedMessage, modifier = modifier, onRetry = onRetry, ) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screens/PlaceholderScreens.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screens/PlaceholderScreens.kt index 4048a5c02..3a30ba107 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screens/PlaceholderScreens.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screens/PlaceholderScreens.kt @@ -28,6 +28,8 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.SharedRes +import dev.icerock.moko.resources.compose.stringResource /** * Generic placeholder screen with title and description. @@ -58,8 +60,8 @@ fun PlaceholderScreen( @Composable fun SearchPlaceholder(modifier: Modifier = Modifier) { PlaceholderScreen( - title = "Search", - description = "Search for users, notes, and hashtags.", + title = stringResource(SharedRes.strings.screen_search_title), + description = stringResource(SharedRes.strings.screen_search_description), modifier = modifier, ) } @@ -70,8 +72,8 @@ fun SearchPlaceholder(modifier: Modifier = Modifier) { @Composable fun MessagesPlaceholder(modifier: Modifier = Modifier) { PlaceholderScreen( - title = "Messages", - description = "Your encrypted direct messages will appear here.", + title = stringResource(SharedRes.strings.screen_messages_title), + description = stringResource(SharedRes.strings.screen_messages_description), modifier = modifier, ) } @@ -82,8 +84,8 @@ fun MessagesPlaceholder(modifier: Modifier = Modifier) { @Composable fun NotificationsPlaceholder(modifier: Modifier = Modifier) { PlaceholderScreen( - title = "Notifications", - description = "Mentions, replies, and reactions will appear here.", + title = stringResource(SharedRes.strings.screen_notifications_title), + description = stringResource(SharedRes.strings.screen_notifications_description), modifier = modifier, ) } diff --git a/commons/src/commonMain/moko-resources/base/strings.xml b/commons/src/commonMain/moko-resources/base/strings.xml new file mode 100644 index 000000000..7403080ba --- /dev/null +++ b/commons/src/commonMain/moko-resources/base/strings.xml @@ -0,0 +1,53 @@ + + + + Welcome to Amethyst + Sign in to your Nostr account + A Nostr client for desktop + Login with your Nostr key + Use nsec for full access or npub for read-only + Login with Key + Login + Generate New Key + Generate New + Enter your private key (nsec) or public key (npub) + nsec or npub + nsec1... or npub1... + Show key + Hide key + + + IMPORTANT: Save your keys! + Your secret key (nsec) is the ONLY way to access your account. If you lose it, your account is gone forever. Save it somewhere safe! + Public Key (shareable): + Secret Key (NEVER share this!): + I've saved my keys, continue + + + Copy + Paste + Cancel + OK + Save + Delete + Share + + + Invalid key format. Please check and try again. + Network error. Please check your connection. + An error occurred. Please try again. + + + Refresh + Try Again + Feed is empty + Error loading feed: %s + + + Search + Search for users, notes, and hashtags. + Messages + Your encrypted direct messages will appear here. + Notifications + Mentions, replies, and reactions will appear here. + diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/KeyInputField.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/KeyInputField.kt index e27629c9f..21e0c4b94 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/KeyInputField.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/KeyInputField.kt @@ -41,6 +41,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.SharedRes +import dev.icerock.moko.resources.compose.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview /** * Text field for entering Nostr keys (nsec or npub) with visibility toggle. @@ -50,8 +53,8 @@ fun KeyInputField( value: String, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, - label: String = "nsec or npub", - placeholder: String = "nsec1... or npub1...", + label: String = stringResource(SharedRes.strings.login_key_label), + placeholder: String = stringResource(SharedRes.strings.login_key_placeholder), errorMessage: String? = null, ) { var showKey by remember { mutableStateOf(false) } @@ -73,7 +76,7 @@ fun KeyInputField( IconButton(onClick = { showKey = !showKey }) { Icon( if (showKey) Icons.Default.VisibilityOff else Icons.Default.Visibility, - contentDescription = if (showKey) "Hide key" else "Show key", + contentDescription = if (showKey) stringResource(SharedRes.strings.login_hide_key) else stringResource(SharedRes.strings.login_show_key), ) } }, @@ -108,3 +111,20 @@ fun SelectableKeyText( ) } } + +@Preview +@Composable +fun KeyInputFieldPreview() { + KeyInputField( + value = "nsec1example1234567890", + onValueChange = {}, + ) +} + +@Preview +@Composable +fun SelectableKeyTextPreview() { + SelectableKeyText( + key = "npub1example1234567890abcdefghijklmnopqrstuvwxyz1234567890", + ) +} diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/LoginCard.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/LoginCard.kt index a80912b91..f97e9179b 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/LoginCard.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/LoginCard.kt @@ -43,6 +43,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.SharedRes +import dev.icerock.moko.resources.compose.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview /** * Login card with Nostr key input field and action buttons. @@ -60,8 +63,8 @@ fun LoginCard( onGenerateNew: () -> Unit, modifier: Modifier = Modifier, cardWidth: Dp = 400.dp, - title: String = "Login with your Nostr key", - subtitle: String = "Use nsec for full access or npub for read-only", + title: String = stringResource(SharedRes.strings.login_card_title), + subtitle: String = stringResource(SharedRes.strings.login_card_subtitle), ) { var keyInput by remember { mutableStateOf("") } var errorMessage by remember { mutableStateOf(null) } @@ -118,16 +121,25 @@ fun LoginCard( modifier = Modifier.weight(1f), enabled = keyInput.isNotBlank(), ) { - Text("Login") + Text(stringResource(SharedRes.strings.login_button)) } OutlinedButton( onClick = onGenerateNew, modifier = Modifier.weight(1f), ) { - Text("Generate New") + Text(stringResource(SharedRes.strings.login_generate_button)) } } } } } + +@Preview +@Composable +fun LoginCardPreview() { + LoginCard( + onLogin = { Result.success(Unit) }, + onGenerateNew = {}, + ) +} diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/NewKeyWarningCard.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/NewKeyWarningCard.kt index 4fa976aa9..9952ee4fb 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/NewKeyWarningCard.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/NewKeyWarningCard.kt @@ -36,6 +36,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.SharedRes +import dev.icerock.moko.resources.compose.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview /** * Warning card displayed after generating a new Nostr key pair. @@ -66,7 +69,7 @@ fun NewKeyWarningCard( modifier = Modifier.padding(24.dp), ) { Text( - "IMPORTANT: Save your keys!", + stringResource(SharedRes.strings.new_key_warning_title), style = MaterialTheme.typography.titleMedium, color = Color.Red, ) @@ -74,8 +77,7 @@ fun NewKeyWarningCard( Spacer(Modifier.height(16.dp)) Text( - "Your secret key (nsec) is the ONLY way to access your account. " + - "If you lose it, your account is gone forever. Save it somewhere safe!", + stringResource(SharedRes.strings.new_key_warning_message), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface, ) @@ -83,7 +85,7 @@ fun NewKeyWarningCard( Spacer(Modifier.height(16.dp)) Text( - "Public Key (shareable):", + stringResource(SharedRes.strings.new_key_public_label), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -93,7 +95,7 @@ fun NewKeyWarningCard( nsec?.let { secretKey -> Text( - "Secret Key (NEVER share this!):", + stringResource(SharedRes.strings.new_key_secret_label), style = MaterialTheme.typography.labelMedium, color = Color.Red, ) @@ -106,8 +108,18 @@ fun NewKeyWarningCard( onClick = onContinue, modifier = Modifier.fillMaxWidth(), ) { - Text("I've saved my keys, continue") + Text(stringResource(SharedRes.strings.new_key_continue_button)) } } } } + +@Preview +@Composable +fun NewKeyWarningCardPreview() { + NewKeyWarningCard( + npub = "npub1example1234567890abcdefghijklmnopqrstuvwxyz", + nsec = "nsec1example1234567890abcdefghijklmnopqrstuvwxyz", + onContinue = {}, + ) +} diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/LoginScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/LoginScreen.kt index f194564ae..be84b6375 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/LoginScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/LoginScreen.kt @@ -37,10 +37,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.SharedRes import com.vitorpamplona.amethyst.commons.account.AccountManager import com.vitorpamplona.amethyst.commons.account.AccountState import com.vitorpamplona.amethyst.commons.ui.auth.LoginCard import com.vitorpamplona.amethyst.commons.ui.auth.NewKeyWarningCard +import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -59,7 +61,7 @@ fun LoginScreen( verticalArrangement = Arrangement.Center, ) { Text( - "Welcome to Amethyst", + stringResource(SharedRes.strings.login_title), style = MaterialTheme.typography.headlineLarge, color = MaterialTheme.colorScheme.onBackground, ) @@ -67,7 +69,7 @@ fun LoginScreen( Spacer(Modifier.height(8.dp)) Text( - "A Nostr client for desktop", + stringResource(SharedRes.strings.login_subtitle_desktop), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0918278b9..5717cae99 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -40,6 +40,7 @@ lightcompressor-enhanced = "1.6.0" markdown = "f92ef49c9d" media3 = "1.9.0" mockk = "1.14.7" +mokoResources = "0.25.2" kotlinx-coroutines-test = "1.10.2" navigationCompose = "2.9.6" okhttp = "5.3.2" @@ -135,6 +136,8 @@ markdown-ui = { group = "com.github.vitorpamplona.compose-richtext", name = "ric markdown-ui-material3 = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-ui-material3", version.ref = "markdown" } mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } mockk-android = { group = "io.mockk", name = "mockk-android", version.ref = "mockk" } +moko-resources = { group = "dev.icerock.moko", name = "resources", version.ref = "mokoResources" } +moko-resources-compose = { group = "dev.icerock.moko", name = "resources-compose", version.ref = "mokoResources" } kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-test"} okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } okhttpCoroutines = { group = "com.squareup.okhttp3", name = "okhttp-coroutines", version.ref = "okhttp" } @@ -173,3 +176,4 @@ androidKotlinMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.lib vanniktech-mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" } stability-analyzer = { id = "com.github.skydoves.compose.stability.analyzer", version = "0.6.1" } composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" } +mokoResources = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "mokoResources" } From 48bd284ef19936fce8d39a3c4d3419d20cda44f2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 7 Jan 2026 09:24:40 -0500 Subject: [PATCH 091/137] Update AccountManager.kt --- .../com/vitorpamplona/amethyst/commons/account/AccountManager.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt index 924d8e70a..4730eafcf 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt @@ -48,6 +48,7 @@ sealed class AccountState { ) : AccountState() } +@Stable class AccountManager private constructor( private val secureStorage: SecureKeyStorage, ) { From dcbe7df007e2499c23c811a6d32721601ba0221a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 7 Jan 2026 10:07:26 -0500 Subject: [PATCH 092/137] Fixes test --- .../vitorpamplona/quartz/nip01Core/jackson/JacksonMapperTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapperTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapperTest.kt index e9cf90625..b9cc65956 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapperTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapperTest.kt @@ -142,7 +142,7 @@ class JacksonMapperTest { serialized, ) - val deserialized = JacksonMapper.fromJson(serialized) + val deserialized = JacksonMapper.fromJsonToEventTemplate(serialized) assertEquals(followCardTemplate.kind, deserialized.kind) assertEquals(followCardTemplate.createdAt, deserialized.createdAt) From 7a7ce697094fe7acbbf2974c82647301d88b45a5 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 7 Jan 2026 10:54:09 -0500 Subject: [PATCH 093/137] Fixes missing tagsAll --- .../amethyst/commons/filters/FilterBuilders.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuilders.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuilders.kt index 33bee5095..a93c6cf52 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuilders.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuilders.kt @@ -276,6 +276,7 @@ class FilterBuilder { private var authors: List? = null private var kinds: List? = null private var tags: MutableMap>? = null + private var tagsAll: MutableMap>? = null private var since: Long? = null private var until: Long? = null private var limit: Int? = null @@ -313,6 +314,14 @@ class FilterBuilder { tags!![name] = values } + fun tagAll( + name: String, + values: List, + ) { + if (tagsAll == null) tagsAll = mutableMapOf() + tagsAll!![name] = values + } + fun pTag(vararg pubKeys: String) { tag("p", pubKeys.toList()) } @@ -337,7 +346,7 @@ class FilterBuilder { this.search = search } - fun build(): Filter = Filter(ids, authors, kinds, tags, since, until, limit, search) + fun build(): Filter = Filter(ids, authors, kinds, tags, tagsAll, since, until, limit, search) } /** From ce01e8f70cfe5021def19508df322abb32532cae Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 7 Jan 2026 11:09:45 -0500 Subject: [PATCH 094/137] moves keystore to commons to avoid unnecessary dependencies on Quartz --- commons/build.gradle.kts | 7 +++++++ .../amethyst/commons/keystorage}/KeyStoreEncryption.kt | 2 +- .../amethyst/commons/keystorage}/SecureKeyStorage.kt | 2 +- .../amethyst/commons/keystorage}/SecureKeyStorage.kt | 2 +- .../amethyst/commons/account/AccountManager.kt | 4 ++-- .../amethyst/commons/keystorage}/SecureKeyStorage.kt | 2 +- quartz/build.gradle.kts | 7 ------- 7 files changed, 13 insertions(+), 13 deletions(-) rename {quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto => commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/keystorage}/KeyStoreEncryption.kt (98%) rename {quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto => commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/keystorage}/SecureKeyStorage.kt (98%) rename {quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/keystorage}/SecureKeyStorage.kt (98%) rename {quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto => commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/keystorage}/SecureKeyStorage.kt (99%) diff --git a/commons/build.gradle.kts b/commons/build.gradle.kts index 427ed28b4..90ccd7d18 100644 --- a/commons/build.gradle.kts +++ b/commons/build.gradle.kts @@ -104,6 +104,9 @@ kotlin { // Desktop-specific Compose implementation(compose.desktop.currentOs) implementation(compose.uiTooling) + + // Secure key storage via OS keychain (macOS/Windows/Linux) + implementation(libs.java.keyring) } } @@ -112,6 +115,10 @@ kotlin { dependencies { // Android-specific Compose tooling implementation(libs.androidx.ui.tooling.preview) + + // Secure key storage via Android Keystore + implementation(libs.androidx.security.crypto.ktx) + implementation(libs.androidx.datastore.preferences) } } diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/KeyStoreEncryption.kt b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/keystorage/KeyStoreEncryption.kt similarity index 98% rename from quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/KeyStoreEncryption.kt rename to commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/keystorage/KeyStoreEncryption.kt index 548ad6b21..34813cb6e 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/KeyStoreEncryption.kt +++ b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/keystorage/KeyStoreEncryption.kt @@ -18,7 +18,7 @@ * 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.nip01Core.crypto +package com.vitorpamplona.amethyst.commons.keystorage import android.os.Build import android.security.keystore.KeyGenParameterSpec diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/keystorage/SecureKeyStorage.kt similarity index 98% rename from quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt rename to commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/keystorage/SecureKeyStorage.kt index 771c7e971..9ee5d1f57 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt +++ b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/keystorage/SecureKeyStorage.kt @@ -18,7 +18,7 @@ * 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.nip01Core.crypto +package com.vitorpamplona.amethyst.commons.keystorage import android.content.Context import androidx.security.crypto.EncryptedSharedPreferences diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/keystorage/SecureKeyStorage.kt similarity index 98% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/keystorage/SecureKeyStorage.kt index d94171404..6dc292df1 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/keystorage/SecureKeyStorage.kt @@ -18,7 +18,7 @@ * 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.nip01Core.crypto +package com.vitorpamplona.amethyst.commons.keystorage /** * Secure storage for private keys using platform-specific secure storage mechanisms. diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt index 4730eafcf..a2cffbd11 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt @@ -21,11 +21,11 @@ package com.vitorpamplona.amethyst.commons.account import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.commons.keystorage.SecureKeyStorage +import com.vitorpamplona.amethyst.commons.keystorage.SecureStorageException import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair -import com.vitorpamplona.quartz.nip01Core.crypto.SecureKeyStorage -import com.vitorpamplona.quartz.nip01Core.crypto.SecureStorageException import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip19Bech32.decodePrivateKeyAsHexOrNull diff --git a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/keystorage/SecureKeyStorage.kt similarity index 99% rename from quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt rename to commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/keystorage/SecureKeyStorage.kt index f64a8a755..9a74a4adf 100644 --- a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/SecureKeyStorage.kt +++ b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/keystorage/SecureKeyStorage.kt @@ -18,7 +18,7 @@ * 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.nip01Core.crypto +package com.vitorpamplona.amethyst.commons.keystorage import com.github.javakeyring.BackendNotSupportedException import com.github.javakeyring.Keyring diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index 7bf19ba52..a9f1d39e6 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -166,9 +166,6 @@ kotlin { // LibSodium for ChaCha encryption (NIP-44) implementation (libs.lazysodium.java) implementation (libs.jna) - - // Secure key storage via OS keychain (macOS/Windows/Linux) - implementation(libs.java.keyring) } } @@ -191,10 +188,6 @@ kotlin { // LibSodium for ChaCha encryption (NIP-44) implementation ("com.goterl:lazysodium-android:5.2.0@aar") implementation ("net.java.dev.jna:jna:5.18.1@aar") - - // Secure key storage via Android Keystore - implementation(libs.androidx.security.crypto.ktx) - implementation(libs.androidx.datastore.preferences) } } From d3e0cdd1351129f10ae1f19e039f49ca663a3207 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 7 Jan 2026 11:16:26 -0500 Subject: [PATCH 095/137] Fixes FilledFilter test --- .../commons/filters/FilterBuildersTest.kt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuildersTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuildersTest.kt index 655c4d99f..e88092656 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuildersTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuildersTest.kt @@ -418,7 +418,7 @@ class FilterBuildersTest { fun testGlobalFeedScenario() { val filter = FilterBuilders.textNotesGlobal(limit = 50) - assertTrue(filter.isFilledFilter()) + assertTrue(!filter.isEmpty()) assertEquals(listOf(1), filter.kinds) assertEquals(50, filter.limit) } @@ -428,7 +428,7 @@ class FilterBuildersTest { val followedUsers = listOf(testPubKey, testPubKey2) val filter = FilterBuilders.textNotesFromAuthors(followedUsers, limit = 50) - assertTrue(filter.isFilledFilter()) + assertTrue(!filter.isEmpty()) assertEquals(listOf(1), filter.kinds) assertEquals(followedUsers, filter.authors) assertEquals(50, filter.limit) @@ -440,9 +440,9 @@ class FilterBuildersTest { val postsFilter = FilterBuilders.textNotesFromAuthors(listOf(testPubKey), limit = 50) val contactListFilter = FilterBuilders.contactList(testPubKey) - assertTrue(metadataFilter.isFilledFilter()) - assertTrue(postsFilter.isFilledFilter()) - assertTrue(contactListFilter.isFilledFilter()) + assertTrue(!metadataFilter.isEmpty()) + assertTrue(!postsFilter.isEmpty()) + assertTrue(!contactListFilter.isEmpty()) assertEquals(listOf(0), metadataFilter.kinds) assertEquals(listOf(1), postsFilter.kinds) @@ -453,7 +453,7 @@ class FilterBuildersTest { fun testNotificationsScenario() { val filter = FilterBuilders.notificationsForUser(testPubKey, limit = 100) - assertTrue(filter.isFilledFilter()) + assertTrue(!filter.isEmpty()) assertEquals(listOf(1, 7, 6, 16, 9735), filter.kinds) assertNotNull(filter.tags) assertEquals(listOf(testPubKey), filter.tags!!["p"]) @@ -470,7 +470,7 @@ class FilterBuildersTest { limit(100) } - assertTrue(filter.isFilledFilter()) + assertTrue(!filter.isEmpty()) assertEquals(listOf(1), filter.kinds) assertNotNull(filter.tags) assertEquals(listOf(testEventId), filter.tags!!["e"]) @@ -485,7 +485,7 @@ class FilterBuildersTest { limit(50) } - assertTrue(filter.isFilledFilter()) + assertTrue(!filter.isEmpty()) assertEquals(listOf(7), filter.kinds) assertNotNull(filter.tags) assertEquals(listOf(testEventId), filter.tags!!["e"]) @@ -500,7 +500,7 @@ class FilterBuildersTest { limit(50) } - assertTrue(filter.isFilledFilter()) + assertTrue(!filter.isEmpty()) assertEquals(listOf(1), filter.kinds) assertNotNull(filter.tags) assertEquals(listOf("bitcoin"), filter.tags!!["t"]) From e84eca2bcac1c1480ee0edee8f4e5bf48520c81f Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Wed, 7 Jan 2026 16:42:01 +0000 Subject: [PATCH 096/137] New Crowdin translations by GitHub Action --- .../src/main/res/values-hu-rHU/strings.xml | 49 +++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index b889daa52..c4fce36db 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -15,6 +15,8 @@ Nem sikerült visszafejteni az üzenetet Csoport profilképe Szókimondó tartalom + Átjátszóval kapcsolatos megjegyzések + Duplikált bejegyzés Kéretlen tartalom Az erről az átjátszóról érkező kéretlen tartalmú események száma Profilutánzás @@ -121,6 +123,7 @@ Átjátszó címe Bejegyzések Bájt + Hiba Hibák Ebben a munkamenetben lévő kapcsolati hibák száma Fő hírfolyam @@ -360,6 +363,8 @@ Letiltás Kézi Zap-megosztás Könyvjelző + Alapértelmezett könyvjelzők + Az alapértelmezett könyvjelzők, amelyeket sok kliens támogat Piszkozatok Privát könyvjelzők Nyilvános könyvjelzők @@ -482,7 +487,7 @@ Tor és Orbot beállítása Kapcsolódás az Orbot beállításain keresztül Módosítás - Tor Beállítások + Tor-beállítások Kapcsolat bontása az Orbottal / Torral? Az Ön adatai azonnal átkerülnek a normál hálózatra Igen @@ -643,28 +648,64 @@ Az átjátszótól kapott bájt-mennyiség, beleértve a szűrőket és eseményeket is Hiba lépett fel, amikor megpróbálta lekérni az átjátszó-információt innen: %1$s Tulajdonos + Szolgáltatási kulcs + %1$s futtatása + %1$s (%2$s) futtatása Verzió Szoftver Kapcsolat Támogatott NIP-ek + Támogatott tartalomkezelők Csatlakozási díj + Felvétel + Előfizetés + Közzététel + %1$s kifizetések Fizetési webcím + Célközönség + Irányelvek és hivatkozások + Költségek és kifizetések Korlátozások Országok Nyelvek Címkék + Témák + Összes ország + Összes nyelv Közzétételi szabályzat + Adatvédelmi irányelvek + Általános szerződési feltételek + Nem érhető el Hibák és megjegyzések ettől az átjátszótól Üzenethossz Előfizetések Szűrők Előfizetés azonosítójának hossza - Minimális előtag - Maximális eseménycímkék + Minimális előtaghossz + Eseménycímkék száma legfeljebb Tartalomhossz - Minimális PoW + Tartalom hossza legfeljebb + Ettől régebbiek elvetése: + Elfogadások legfeljebb + %1$s a jövőben + %1$s ezelőtt + %1$s nulla + %1$s bit + Eseménymegőrzés + Tartalom mérete + Kapcsolat + Hozzáférés-vezérlés + Spam-szűrés erőssége Hitelesítés + Hitelesítés szükséges Fizetés + Fizetés szükséges + Üzenet hossza legfeljebb + Előfizetések legfeljebb + Szűrők száma feliratkozásonként + Korlátozás (visszatérő események) + Alapértelmezett korlátozás (visszatérő események) + Feliratkozás-azonosító hossza legfeljebb Cashu token Beváltás Küldés ide: Zap Wallet From e05aa611eb524e72ac06224d8dbb2b1ecaed2bd1 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Wed, 7 Jan 2026 16:43:22 +0000 Subject: [PATCH 097/137] New Crowdin translations by GitHub Action --- .../src/main/res/values-hu-rHU/strings.xml | 49 +++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index b889daa52..c4fce36db 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -15,6 +15,8 @@ Nem sikerült visszafejteni az üzenetet Csoport profilképe Szókimondó tartalom + Átjátszóval kapcsolatos megjegyzések + Duplikált bejegyzés Kéretlen tartalom Az erről az átjátszóról érkező kéretlen tartalmú események száma Profilutánzás @@ -121,6 +123,7 @@ Átjátszó címe Bejegyzések Bájt + Hiba Hibák Ebben a munkamenetben lévő kapcsolati hibák száma Fő hírfolyam @@ -360,6 +363,8 @@ Letiltás Kézi Zap-megosztás Könyvjelző + Alapértelmezett könyvjelzők + Az alapértelmezett könyvjelzők, amelyeket sok kliens támogat Piszkozatok Privát könyvjelzők Nyilvános könyvjelzők @@ -482,7 +487,7 @@ Tor és Orbot beállítása Kapcsolódás az Orbot beállításain keresztül Módosítás - Tor Beállítások + Tor-beállítások Kapcsolat bontása az Orbottal / Torral? Az Ön adatai azonnal átkerülnek a normál hálózatra Igen @@ -643,28 +648,64 @@ Az átjátszótól kapott bájt-mennyiség, beleértve a szűrőket és eseményeket is Hiba lépett fel, amikor megpróbálta lekérni az átjátszó-információt innen: %1$s Tulajdonos + Szolgáltatási kulcs + %1$s futtatása + %1$s (%2$s) futtatása Verzió Szoftver Kapcsolat Támogatott NIP-ek + Támogatott tartalomkezelők Csatlakozási díj + Felvétel + Előfizetés + Közzététel + %1$s kifizetések Fizetési webcím + Célközönség + Irányelvek és hivatkozások + Költségek és kifizetések Korlátozások Országok Nyelvek Címkék + Témák + Összes ország + Összes nyelv Közzétételi szabályzat + Adatvédelmi irányelvek + Általános szerződési feltételek + Nem érhető el Hibák és megjegyzések ettől az átjátszótól Üzenethossz Előfizetések Szűrők Előfizetés azonosítójának hossza - Minimális előtag - Maximális eseménycímkék + Minimális előtaghossz + Eseménycímkék száma legfeljebb Tartalomhossz - Minimális PoW + Tartalom hossza legfeljebb + Ettől régebbiek elvetése: + Elfogadások legfeljebb + %1$s a jövőben + %1$s ezelőtt + %1$s nulla + %1$s bit + Eseménymegőrzés + Tartalom mérete + Kapcsolat + Hozzáférés-vezérlés + Spam-szűrés erőssége Hitelesítés + Hitelesítés szükséges Fizetés + Fizetés szükséges + Üzenet hossza legfeljebb + Előfizetések legfeljebb + Szűrők száma feliratkozásonként + Korlátozás (visszatérő események) + Alapértelmezett korlátozás (visszatérő események) + Feliratkozás-azonosító hossza legfeljebb Cashu token Beváltás Küldés ide: Zap Wallet From 806bc2729a83370461629a929f800f6cdcfaf068 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 7 Jan 2026 11:51:45 -0500 Subject: [PATCH 098/137] Fixes places for the test and common code --- .../amethyst/commons/compose/TextFieldValueExtensionTest.kt | 0 .../com/vitorpamplona/amethyst/commons/preview/BlurhashTest.kt | 0 .../vitorpamplona/amethyst/commons/preview/MetaTagsParserTest.kt | 0 .../commons/richtext/ExpandableTextCutOffCalculatorTest.kt | 0 .../vitorpamplona/amethyst/commons/richtext/GalleryParserTest.kt | 0 .../vitorpamplona/amethyst/commons/richtext/RichTextParserTest.kt | 0 .../kotlin}/com/vitorpamplona/amethyst/commons/Base83Test.kt | 0 .../kotlin}/com/vitorpamplona/amethyst/commons/SRGBTest.kt | 0 .../vitorpamplona/amethyst/commons/emojicoder/EmojiCoderTest.kt | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename commons/src/androidTest/{java => kotlin}/com/vitorpamplona/amethyst/commons/compose/TextFieldValueExtensionTest.kt (100%) rename commons/src/androidTest/{java => kotlin}/com/vitorpamplona/amethyst/commons/preview/BlurhashTest.kt (100%) rename commons/src/androidTest/{java => kotlin}/com/vitorpamplona/amethyst/commons/preview/MetaTagsParserTest.kt (100%) rename commons/src/androidTest/{java => kotlin}/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculatorTest.kt (100%) rename commons/src/androidTest/{java => kotlin}/com/vitorpamplona/amethyst/commons/richtext/GalleryParserTest.kt (100%) rename commons/src/androidTest/{java => kotlin}/com/vitorpamplona/amethyst/commons/richtext/RichTextParserTest.kt (100%) rename commons/src/{test/java => commonTest/kotlin}/com/vitorpamplona/amethyst/commons/Base83Test.kt (100%) rename commons/src/{test/java => commonTest/kotlin}/com/vitorpamplona/amethyst/commons/SRGBTest.kt (100%) rename commons/src/{test/java => commonTest/kotlin}/com/vitorpamplona/amethyst/commons/emojicoder/EmojiCoderTest.kt (100%) diff --git a/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/compose/TextFieldValueExtensionTest.kt b/commons/src/androidTest/kotlin/com/vitorpamplona/amethyst/commons/compose/TextFieldValueExtensionTest.kt similarity index 100% rename from commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/compose/TextFieldValueExtensionTest.kt rename to commons/src/androidTest/kotlin/com/vitorpamplona/amethyst/commons/compose/TextFieldValueExtensionTest.kt diff --git a/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/preview/BlurhashTest.kt b/commons/src/androidTest/kotlin/com/vitorpamplona/amethyst/commons/preview/BlurhashTest.kt similarity index 100% rename from commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/preview/BlurhashTest.kt rename to commons/src/androidTest/kotlin/com/vitorpamplona/amethyst/commons/preview/BlurhashTest.kt diff --git a/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/preview/MetaTagsParserTest.kt b/commons/src/androidTest/kotlin/com/vitorpamplona/amethyst/commons/preview/MetaTagsParserTest.kt similarity index 100% rename from commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/preview/MetaTagsParserTest.kt rename to commons/src/androidTest/kotlin/com/vitorpamplona/amethyst/commons/preview/MetaTagsParserTest.kt diff --git a/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculatorTest.kt b/commons/src/androidTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculatorTest.kt similarity index 100% rename from commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculatorTest.kt rename to commons/src/androidTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculatorTest.kt diff --git a/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/richtext/GalleryParserTest.kt b/commons/src/androidTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/GalleryParserTest.kt similarity index 100% rename from commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/richtext/GalleryParserTest.kt rename to commons/src/androidTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/GalleryParserTest.kt diff --git a/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParserTest.kt b/commons/src/androidTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserTest.kt similarity index 100% rename from commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParserTest.kt rename to commons/src/androidTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserTest.kt diff --git a/commons/src/test/java/com/vitorpamplona/amethyst/commons/Base83Test.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/Base83Test.kt similarity index 100% rename from commons/src/test/java/com/vitorpamplona/amethyst/commons/Base83Test.kt rename to commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/Base83Test.kt diff --git a/commons/src/test/java/com/vitorpamplona/amethyst/commons/SRGBTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/SRGBTest.kt similarity index 100% rename from commons/src/test/java/com/vitorpamplona/amethyst/commons/SRGBTest.kt rename to commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/SRGBTest.kt diff --git a/commons/src/test/java/com/vitorpamplona/amethyst/commons/emojicoder/EmojiCoderTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/emojicoder/EmojiCoderTest.kt similarity index 100% rename from commons/src/test/java/com/vitorpamplona/amethyst/commons/emojicoder/EmojiCoderTest.kt rename to commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/emojicoder/EmojiCoderTest.kt From 5cb0231cf95378aa6bce70ed37292abaa47dd80f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 7 Jan 2026 12:06:11 -0500 Subject: [PATCH 099/137] Fixes the choice of padding to match what it was on Amethyst --- .../amethyst/commons/keystorage/KeyStoreEncryption.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/keystorage/KeyStoreEncryption.kt b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/keystorage/KeyStoreEncryption.kt index 34813cb6e..55a63141c 100644 --- a/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/keystorage/KeyStoreEncryption.kt +++ b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/keystorage/KeyStoreEncryption.kt @@ -34,7 +34,7 @@ internal class KeyStoreEncryption { companion object { private const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES private const val BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM - private const val PADDING = KeyProperties.ENCRYPTION_PADDING_NONE + private const val PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7 private const val TRANSFORMATION = "$ALGORITHM/$BLOCK_MODE/$PADDING" private const val PURPOSE = KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT private const val KEY_ALIAS = "AMETHYST_AES_KEY" From e3832ee19bc914fa0afc4081917f62b02e605ca7 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 7 Jan 2026 12:06:28 -0500 Subject: [PATCH 100/137] Moves Filters from Common to Desktop --- .../amethyst/commons/filters/FilterBuildersTest.kt | 2 ++ .../amethyst/commons/subscriptions/FeedSubscription.kt | 1 - .../amethyst/commons/subscriptions}/FilterBuilders.kt | 2 +- .../amethyst/commons/subscriptions/ProfileSubscription.kt | 1 - 4 files changed, 3 insertions(+), 3 deletions(-) rename commons/src/{commonMain/kotlin/com/vitorpamplona/amethyst/commons/filters => jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions}/FilterBuilders.kt (99%) diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuildersTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuildersTest.kt index e88092656..4afb6e0ea 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuildersTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuildersTest.kt @@ -20,6 +20,8 @@ */ package com.vitorpamplona.amethyst.commons.filters +import com.vitorpamplona.amethyst.commons.subscriptions.FilterBuilders +import com.vitorpamplona.amethyst.commons.subscriptions.buildFilter import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FeedSubscription.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FeedSubscription.kt index 9f65c2379..abe3dc240 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FeedSubscription.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FeedSubscription.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.commons.subscriptions -import com.vitorpamplona.amethyst.commons.filters.FilterBuilders import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuilders.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FilterBuilders.kt similarity index 99% rename from commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuilders.kt rename to commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FilterBuilders.kt index a93c6cf52..011d910fa 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuilders.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FilterBuilders.kt @@ -18,7 +18,7 @@ * 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.filters +package com.vitorpamplona.amethyst.commons.subscriptions import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/ProfileSubscription.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/ProfileSubscription.kt index 446f7e1a0..2355c0fac 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/ProfileSubscription.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/ProfileSubscription.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.commons.subscriptions -import com.vitorpamplona.amethyst.commons.filters.FilterBuilders import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl From cf174c77347c4daa9218481cd825bd1eb912e4de Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 7 Jan 2026 12:22:57 -0500 Subject: [PATCH 101/137] Removes dependency on okhttp for the HTMLParser --- .../vitorpamplona/amethyst/service/previews/HtmlParser.kt | 5 ++--- .../vitorpamplona/amethyst/service/previews/UrlPreview.kt | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/HtmlParser.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/HtmlParser.kt index 9f77cd2f2..e76b66440 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/HtmlParser.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/HtmlParser.kt @@ -24,7 +24,6 @@ import com.vitorpamplona.amethyst.commons.preview.MetaTag import com.vitorpamplona.amethyst.commons.preview.MetaTagsParser import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import okhttp3.MediaType import okio.BufferedSource import okio.ByteString.Companion.decodeHex import okio.Options @@ -56,11 +55,11 @@ class HtmlParser { suspend fun parseHtml( source: BufferedSource, - type: MediaType, + type: Charset?, ): Sequence = withContext(Dispatchers.IO) { // sniff charset from Content-Type header or BOM - val sniffedCharset = type.charset() ?: source.readBomAsCharset() + val sniffedCharset = type ?: source.readBomAsCharset() if (sniffedCharset != null) { val content = source.readByteArray().toString(sniffedCharset) return@withContext MetaTagsParser.parse(content) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreview.kt index 3d25f3886..c7ea3eed4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreview.kt @@ -62,7 +62,7 @@ class UrlPreview { response.headers["Content-Type"]?.toMediaType() ?: throw IllegalArgumentException("Website returned unknown mimetype: ${response.headers["Content-Type"]}") if (mimeType.type == "text" && mimeType.subtype == "html") { - val metaTags = HtmlParser().parseHtml(response.body.source(), mimeType) + val metaTags = HtmlParser().parseHtml(response.body.source(), mimeType.charset()) val data = OpenGraphParser().extractUrlInfo(metaTags) UrlInfoItem(url, data.title, data.description, data.image, mimeType.toString()) } else if (mimeType.type == "image") { From 5051eb6d40e644f87b9605e5461d03f1c23419c5 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Wed, 7 Jan 2026 17:25:00 +0000 Subject: [PATCH 102/137] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-pl-rPL/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index 95a8ed79b..111fc65f4 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -661,9 +661,17 @@ Minimalny prefiks Maksymalna ilość tagów wydarzeń Długość zawartości + %1$s bitów + Rozmiar treści + Podłączenie + Kontrola dostępu Minimalny poW Autoryzacja Płatność + Wymagana płatność + Maksymalna długość wiadomości + Maks. subskrypcji + Maks. liczba filtrów na Suba Cashu Token Wykup Wyślij do Zap Wallet From 1783fbab59343fd9a3c3ab4b9cb9ef0a8c7feb83 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 7 Jan 2026 13:18:17 -0500 Subject: [PATCH 103/137] Separating ListChange --- .../amethyst/model/ListChange.kt | 39 +++++++++++++++++++ .../amethyst/ui/dal/ChangesFlowFilter.kt | 19 +-------- 2 files changed, 40 insertions(+), 18 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/ListChange.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ListChange.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ListChange.kt new file mode 100644 index 000000000..1c0530571 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ListChange.kt @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model + +sealed class ListChange { + data class Addition( + val item: T, + ) : ListChange() + + data class Deletion( + val item: T, + ) : ListChange() + + data class SetAddition( + val item: Set, + ) : ListChange() + + data class SetDeletion( + val item: Set, + ) : ListChange() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChangesFlowFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChangesFlowFilter.kt index 792ef828d..dab6bd150 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChangesFlowFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChangesFlowFilter.kt @@ -20,26 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.dal +import com.vitorpamplona.amethyst.model.ListChange import kotlinx.coroutines.flow.MutableSharedFlow interface ChangesFlowFilter : IAdditiveFeedFilter { fun changesFlow(): MutableSharedFlow> } - -sealed class ListChange { - data class Addition( - val item: T, - ) : ListChange() - - data class Deletion( - val item: T, - ) : ListChange() - - data class SetAddition( - val item: Set, - ) : ListChange() - - data class SetDeletion( - val item: Set, - ) : ListChange() -} From 60dafd9267da49c46b54ac25b600456180a92352 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 7 Jan 2026 13:26:47 -0500 Subject: [PATCH 104/137] Moves reactions to packages for their nips --- .../vitorpamplona/amethyst/model/Account.kt | 4 +- .../model/nip18Reposts/RepostAction.kt | 39 ---------------- .../model/nip25Reactions/ReactionAction.kt | 45 ------------------- .../nip18Reposts}/RepostAction.kt | 2 +- .../nip25Reactions}/ReactionAction.kt | 2 +- .../amethyst/desktop/ui/NoteActions.kt | 4 +- 6 files changed, 6 insertions(+), 90 deletions(-) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip18Reposts/RepostAction.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip25Reactions/ReactionAction.kt rename commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/{actions => model/nip18Reposts}/RepostAction.kt (98%) rename commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/{actions => model/nip25Reactions}/ReactionAction.kt (99%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 3efb1c9d3..5b24119a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -24,6 +24,8 @@ import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.commons.model.IAccount +import com.vitorpamplona.amethyst.commons.model.nip18Reposts.RepostAction +import com.vitorpamplona.amethyst.commons.model.nip25Reactions.ReactionAction import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListDecryptionCache @@ -44,8 +46,6 @@ import com.vitorpamplona.amethyst.model.nip02FollowLists.Kind3FollowListState import com.vitorpamplona.amethyst.model.nip03Timestamp.OtsState import com.vitorpamplona.amethyst.model.nip17Dms.DmInboxRelayState import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState -import com.vitorpamplona.amethyst.model.nip18Reposts.RepostAction -import com.vitorpamplona.amethyst.model.nip25Reactions.ReactionAction import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatListDecryptionCache import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip18Reposts/RepostAction.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip18Reposts/RepostAction.kt deleted file mode 100644 index 265a60cbe..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip18Reposts/RepostAction.kt +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.model.nip18Reposts - -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.amethyst.commons.actions.RepostAction as CommonsRepostAction - -/** - * Android wrapper - pure proxy to commons implementation. - * All logic and validation handled in commons. - */ -class RepostAction { - companion object { - suspend fun repost( - note: Note, - signer: NostrSigner, - ): Event? = CommonsRepostAction.repost(note, signer) - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip25Reactions/ReactionAction.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip25Reactions/ReactionAction.kt deleted file mode 100644 index 192625b0b..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip25Reactions/ReactionAction.kt +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.model.nip25Reactions - -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip17Dm.NIP17Factory -import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent -import com.vitorpamplona.amethyst.commons.actions.ReactionAction as CommonsReactionAction - -/** - * Android wrapper - pure proxy to commons implementation. - * All logic and validation handled in commons. - */ -class ReactionAction { - companion object { - suspend fun reactTo( - note: Note, - reaction: String, - by: User, - signer: NostrSigner, - onPublic: (ReactionEvent) -> Unit, - onPrivate: suspend (NIP17Factory.Result) -> Unit, - ) = CommonsReactionAction.reactTo(note, reaction, by, signer, onPublic, onPrivate) - } -} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/RepostAction.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip18Reposts/RepostAction.kt similarity index 98% rename from commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/RepostAction.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip18Reposts/RepostAction.kt index 16f478856..0f68e4f6c 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/RepostAction.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip18Reposts/RepostAction.kt @@ -18,7 +18,7 @@ * 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.actions +package com.vitorpamplona.amethyst.commons.model.nip18Reposts import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/ReactionAction.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip25Reactions/ReactionAction.kt similarity index 99% rename from commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/ReactionAction.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip25Reactions/ReactionAction.kt index 45dbc088f..19cbb4d3e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/ReactionAction.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip25Reactions/ReactionAction.kt @@ -18,7 +18,7 @@ * 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.actions +package com.vitorpamplona.amethyst.commons.model.nip25Reactions import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt index 328693e26..fcc12c918 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt @@ -40,10 +40,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.account.AccountState -import com.vitorpamplona.amethyst.commons.actions.ReactionAction -import com.vitorpamplona.amethyst.commons.actions.RepostAction import com.vitorpamplona.amethyst.commons.icons.Reply import com.vitorpamplona.amethyst.commons.icons.Repost +import com.vitorpamplona.amethyst.commons.model.nip18Reposts.RepostAction +import com.vitorpamplona.amethyst.commons.model.nip25Reactions.ReactionAction import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager import com.vitorpamplona.quartz.nip01Core.core.Event import kotlinx.coroutines.Dispatchers From 13aaaeacd8cc4a09946632038a12765f6476cf6b Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 7 Jan 2026 13:30:30 -0500 Subject: [PATCH 105/137] Fixes imports --- .../src/main/java/com/vitorpamplona/amethyst/model/Channel.kt | 1 - .../com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt | 2 +- .../loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt index 336610ae9..8ea68969b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder -import com.vitorpamplona.amethyst.ui.dal.ListChange import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.utils.cache.LargeCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt index 9219c2b92..c49b79eab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt @@ -21,11 +21,11 @@ package com.vitorpamplona.amethyst.model.privateChats import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.model.ListChange import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.NotesGatherer import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder -import com.vitorpamplona.amethyst.ui.dal.ListChange import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip14Subject.subject diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt index dd6676f11..9b434ffce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt @@ -25,9 +25,9 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.ListChange import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.ChangesFlowFilter -import com.vitorpamplona.amethyst.ui.dal.ListChange import com.vitorpamplona.amethyst.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey From 331124c92ade4b0a7af43b190b3900f5fd4a0f54 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Wed, 7 Jan 2026 18:42:55 +0000 Subject: [PATCH 106/137] New Crowdin translations by GitHub Action --- .../src/main/res/values-pl-rPL/strings.xml | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index 111fc65f4..bb966d36b 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -15,6 +15,8 @@ Nie można odszyfrować wiadomości Zdjęcie grupy Niedozwolona zawartość + Uwagi transmitera + Zduplikowany post Spam Liczba spamu z tego transmitera Podszywanie się @@ -121,6 +123,7 @@ Adres transmitera Posty Bajtów + Błąd Błędy Liczba błędów połączenia w tej sesji Główny kanał @@ -642,17 +645,34 @@ Ilość w bajtach, która została otrzymana z tego transmitera, w tym filtry i wydarzenia Wystąpił błąd podczas próby uzyskania informacji o transmiterze z %1$s Operator + Klucz dostępu do usług + Uruchamianie %1$s + Uruchamianie %1$s (%2$s) Wersja Oprogramowanie Kontakt Obsługiwane Nipy + Obsługiwany Grasps Opłaty za wstęp + Wstęp + Subskrypcja + Publikacja + Płatności %1$s Adres URL płatności + Odbiorcy docelowi + Zasady i linki + Opłaty i należności Ograniczenia Kraje Języki Tagi + Tematy + Wszystkie kraje + Wszystkie języki Polityka publikowania + Polityka Prywatności + Zasady i warunki użytkowania + Nie dotyczy Błędy i powiadomienia z tego transmitera Długość wiadomości Subskrybcje @@ -661,17 +681,28 @@ Minimalny prefiks Maksymalna ilość tagów wydarzeń Długość zawartości + Maks. długość treści + Odrzuć starsze niż + Akceptuje do + %1$s w przyszłości + %1$s temu + %1$s zer %1$s bitów + Zapisywanie zdarzeń Rozmiar treści Podłączenie Kontrola dostępu Minimalny poW Autoryzacja + Wymagane uwierzytelnienie Płatność Wymagana płatność Maksymalna długość wiadomości Maks. subskrypcji Maks. liczba filtrów na Suba + Maksymalny limit (Powracające zdarzenia) + Domyślny limit (Powracające zdarzenia) + Maks. długość SubID Cashu Token Wykup Wyślij do Zap Wallet From 96ae0472719fab66227778b1b66b1186d385d80c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 7 Jan 2026 14:31:23 -0500 Subject: [PATCH 107/137] Removes unnecessary file from commons --- .../amethyst/commons/navigation/AppScreen.kt | 57 ------------------- 1 file changed, 57 deletions(-) delete mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/navigation/AppScreen.kt diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/navigation/AppScreen.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/navigation/AppScreen.kt deleted file mode 100644 index e3502dfc2..000000000 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/navigation/AppScreen.kt +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.commons.navigation - -/** - * Main application screens shared between Desktop and Android. - * Each platform implements its own navigation using these identifiers. - */ -enum class AppScreen( - val label: String, - val route: String, -) { - Feed("Feed", "feed"), - Search("Search", "search"), - Messages("Messages", "messages"), - Notifications("Notifications", "notifications"), - Profile("Profile", "profile"), - Settings("Settings", "settings"), -} - -/** - * Primary navigation destinations (shown in bottom bar on mobile, sidebar on desktop). - */ -val primaryScreens = - listOf( - AppScreen.Feed, - AppScreen.Search, - AppScreen.Messages, - AppScreen.Notifications, - AppScreen.Profile, - ) - -/** - * Secondary navigation destinations (settings, etc.) - */ -val secondaryScreens = - listOf( - AppScreen.Settings, - ) From dceabbb51e002f5733412c61d44947786d9f79f7 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 7 Jan 2026 14:33:45 -0500 Subject: [PATCH 108/137] delete duplicated file --- quartz/relayDB.txt | 2539 -------------------------------------------- 1 file changed, 2539 deletions(-) delete mode 100644 quartz/relayDB.txt diff --git a/quartz/relayDB.txt b/quartz/relayDB.txt deleted file mode 100644 index 0977f44b3..000000000 --- a/quartz/relayDB.txt +++ /dev/null @@ -1,2539 +0,0 @@ -wss://nostr.wine -wss://relay.orangepill.dev -wss://xmr.usenostr.org -wss://nostr.portemonero.com -wss://nostr.xmr.rocks -wss://relay.nostr.band -wss://filter.nostr.wine -wss://nostr.milou.lol -wss://nostr.mutinywallet.com -wss://nostr-pub.wellorder.net -wss://nostr.zebedee.cloud -wss://nos.lol -wss://brb.io -wss://bitcoiner.social -wss://nostr.decentony.com -wss://relay.nostriches.org -wss://paid.spore.ws -wss://eden.nostr.land -wss://puravida.nostr.land -wss://5dzvuefllevkhk7miqynaviguedxfnofrayu2xwfwtlkdg4radjdlyqd.onion -wss://relay-jp.nostr.wirednet.jp -wss://relay.nostrich.land -wss://nostr.holybea.com -wss://nostr-relay.nokotaro.com -wss://nostr-paid.h3z.jp -wss://nostrja-kari.heguro.com -wss://nostr.mom -wss://nostr.fediverse.jp -wss://nostr.h3z.jp -wss://universe.nostrich.land -wss://nostr.uselessshit.co -wss://atlas.nostr.land -wss://relay.snort.social -wss://universe.nostrich.landlangenlanges -wss://nostr.slothy.win -wss://nostr.plebchain.org -wss://nostr-relay.untethr.me -wss://relay.nostr.com.au -wss://nostr.inosta.cc -wss://relay.nostrati.com -wss://nostr.bitcoiner.social -wss://relay.nostrplebs.com -wss://relay.nostr.info -wss://nostr-relay.wlvs.space -wss://nostr.oxtr.dev -wss://nostr.onsats.org -wss://relay.wellorder.net -wss://relay.plebstr.com -wss://no.str.cr -wss://nostr.walletofsatoshi.com -wss://nostr.mwmdev.com -wss://relay.nostr.bg -wss://nostr.rocks -wss://nostr.fmt.wiz.biz -wss://nostr.orangepill.dev -wss://nostr-pub.semisol.dev -wss://nostr.sandwich.farm -wss://relay.nostr.ch -wss://relay.orange-crush.com -wss://private.red.gb.net -wss://nostr.lnprivate.network -wss://nostr.lu.ke -wss://relay.nostr.wirednet.jp -wss://lightningrelay.com -wss://relay.nostrgraph.net -wss://relay.nostrica.com -wss://relay.mostr.pub -wss://nostr-sg.com -wss://nostr.zkid.social -wss://relay.nostr.vet -wss://relay.nostr3.io -wss://relay.arsip.my.id -wss://relay.current.fyi -wss://global.relay.red -wss://nostr.island.network -wss://node01.nostress.cc -wss://relay.nostr.net.in -wss://relay.utxo.one -wss://relay-1.arsip.my.id -wss://nostrical.com -wss://nostro.cc -wss://rsslay.nostr.moe -wss://relay.nostr.or.jp -wss://nostream.unift.xyz -wss://blastr.f7z.xyz -wss://relay.honk.pw -wss://universe.nostrich.landlangja -wss://nostream.ocha.one -wss://paid-relay.nost.love -wss://offchain.pub -wss://nostr-usa.ka1gbeoa21bnm.us-west-2.cs.amazonlightsail.com -wss://nostr.terminus.money -wss://nostr.shawnyeager.net -wss://public.nostr.swissrouting.com -wss://nostrex.fly.dev -wss://relay.727whisky.com -wss://relay.cryptocculture.com -wss://relay.bleskop.com -wss://nostr.lorentz.is -wss://nostr.actn.io -wss://nostr-relay.lnmarkets.com -wss://nostr.openchain.fr -wss://relay.punkhub.me -wss://nostr.blipme.app -wss://nostr.swiss-enigma.ch -wss://nostr-verified.wellorder.net -wss://at.nostrworks.com -wss://21sats.net -wss://e.nos.lol -wss://nostr.mikedilger.com -wss://nostr.azte.co -wss://noster.bitcoiner.social -wss://relay.nostr.moe -wss://nostream.nostrly.io -wss://nostr.gives.africa -wss://nostr.21sats.net -wss://bitcoinmaximalists.online -wss://paid.nostrified.org -wss://nostr.1sat.org -wss://nostr.relayer.se -wss://sg.qemura.xyz -wss://nostr.coinos.io -wss://nostr.bitcoinplebs.de -wss://nostrich.friendship.tw -wss://nostr.sg -wss://eosla.com -wss://nostr.sidnlabs.nl -wss://nostr.foundrydigital.com -wss://relay.nostrview.com -wss://nostr.semisol.dev -wss://relay.f7z.io -wss://wlvs.space -wss://nostr.v0l.io -wss://nostr-relay.digitalmob.ro -wss://rsslay.fiatjaf.com -wss://relay.theorangepillapp.com -wss://relay.zeh.app -wss://nostr.zoomout.chat -wss://relay.stoner.com -wss://nostr.cercatrova.me -wss://relay.ryzizub.com -wss://nostr-1.nbo.angani.co -wss://nostr21.com -wss://spore.ws -wss://nostrue.com -wss://no-str.org -wss://relay.taxi -wss://ragnar-relay.com -wss://relay.austrich.net -wss://relay.nostr-latam.link -wss://1.noztr.com -wss://relay.nostr.scot -wss://test.relay.nostrich.day -wss://jiggytom.ddns.net -wss://nostr.bongbong.com -wss://relay.nostromo.social -wss://relay.sendstr.com -wss://nostr-dev.universalname.space -wss://relay.nostrcheck.me -wss://nostr.libertasprimordium.com -wss://nostr.kollider.xyz -wss://expensive-relay.fiatjaf.com -wss://nostr-sandbox.minds.io -wss://relay.nostrich.de -wss://nostr.gromeul.eu -wss://relay.nostr.wine -wss://nostr.screaminglife.io -wss://nostr-relay.derekross.me -wss://nostrica.nostrnotes.com -wss://paid.no.str.cr -wss://nostr.sethforprivacy.com -wss://nostr.dumpit.top -wss://nostr.herci.one -wss://cheery-paddock-rsakdrtc35c55n6yregn.wnext.app -wss://nostr.blockpower.capital -wss://nostr.nym.life -wss://nostr-verif.slothy.win -wss://fiatdenier.com -wss://nostr.bitcoin-21.org -wss://nostr.fluidtrack.in -wss://nostr.developer.li -wss://r.ayit.org -wss://relay.nostr.nu -wss://nostr.bostonbtc.com -wss://rly.social -wss://nostr.bridgey.dev -wss://relay.nostrprotocol.net -wss://nostr.mado.io -wss://nostr.einundzwanzig.space -wss://nostr2.actn.io -wss://nostr-relay.freedomnode.com -wss://nostr.pleb.network -wss://nostr.mouton.dev -wss://eelay.current.fyi -wss://nostr.notmyhostna.me -wss://nostr.pjv.me -wss://nostr.jatm.link -wss://nostr.fractalized.ovh -wss://nostr-relay.app.ikeji.ma -wss://relayer.ocha.one -wss://nostr.com.de -wss://nostr-2.afarazit.eu -wss://nostr.l00p.org -wss://nostr.drss.io -wss://relay.nostrify.io -wss://nostr.radixrat.com -wss://nostr-relay.bitcoin.ninja -wss://nostrsatva.net -wss://nostr.mustardnodes.com -wss://nostr01.vida.dev -wss://nostr.noones.com -wss://nostr.easify.de -wss://nostr3.actn.io -wss://moonbreeze.richardbondi.net -wss://nostr.naut.social -wss://private-nostr.v0l.io -wss://nostr.zaprite.io -wss://nostr.lightninglinks.xyz -wss://nostr.hackerman.pro -wss://nr.yay.so -wss://nostr.roundrockbitcoiners.com -wss://nostr.sovbit.host -wss://nostrelay.yeghro.site -wss://pow32.nostr.land -wss://nostr.1729.cloud -wss://nostr.rdfriedl.com -wss://nostr.h4x0r.host -wss://nostr.up.railway.app -wss://nostr.lnorb.com -wss://nostr.lordkno.ws -wss://relay.nostr.vision -wss://nostr-3.orba.ca -wss://satstacker.cloud -wss://freedom-relay.herokuapp.com -wss://nostr-relay.freeberty.net -wss://nostr.unknown.place -wss://nostr.delo.software -wss://relay.nostr.pro -wss://relay.minds.com -wss://nostr.ono.re -wss://relay.grunch.dev -wss://relay.cynsar.foundation -wss://relay.oldcity-bitcoiners.info -wss://relay.bitid.nz -wss://relay.nostr.xyz -wss://relay.futohq.com -wss://relay.farscapian.com -wss://astral.ninja -wss://relay.sovereign-stack.org -wss://nostr-2.zebedee.cloud -wss://nostr.nymsrelay.com -wss://relay.kronkltd.net -wss://relay.r3d.red -wss://universe.nostrich.landlangen -wss://nostr-dev.wellorder.net -wss://nostr.beta3.dev -wss://nostr.data.haus -wss://nostr.hugo.md -wss://relay-dev.cowdle.gg -wss://relay.dwadziesciajeden.pl -wss://tmp-relay.cesc.trade -wss://nostr.massmux.com -wss://relay.nostr.africa -wss://nostr1.tunnelsats.com -wss://nostr.f44.dev -wss://relay.n057r.club -wss://nostr-verif.slothy.com -wss://nostr.1f52b.xyz -wss://nostr.sebastix.dev -wss://nostr.lightning.contact -wss://nostr.rly.social -wss://noster.online -wss://relay.lexingtonbitcoin.org -wss://nostr.bitcoinbay.engineering -wss://nostr.howtobitcoin.shop -wss://blg.nostr.sx -wss://deschooling.us -wss://foolay.nostr.moe -wss://freespeech.casa -wss://nostr-01.bolt.observer -wss://nostr-01.dorafactory.org -wss://nostr-au.coinfundit.com -wss://nostr-eu.coinfundit.com -wss://nostr-relay.alekberg.net -wss://nostr-pub1.southflorida.ninja -wss://nostr-relay.gkbrk.com -wss://nostr-relay.pcdkd.fyi -wss://nostr-relay.schnitzel.world -wss://nostr-us.coinfundit.com -wss://nostr.21crypto.ch -wss://nostr.600.wtf -wss://nostr.8e23.net -wss://nostr.app.runonflux.io -wss://nostr.arguflow.gg -wss://nostr.bch.ninja -wss://nostr.chainofimmortals.net -wss://nostr.cizmar.net -wss://nostr.cheeserobot.org -wss://nostr.coollamer.com -wss://nostr.corebreach.com -wss://nostr.cro.social -wss://nostr.easydns.ca -wss://nostr.globals.fans -wss://nostr.handyjunky.com -wss://nostr.itas.li -wss://nostr.sectiontwo.org -wss://nostr.spleenrider.one -wss://nostr.thibautrey.fr -wss://nostr.uthark.com -wss://nostr.vulpem.com -wss://nostr.w3ird.tech -wss://nostr.whoop.ph -wss://nostr.yuv.al -wss://nostr01.opencult.com -wss://nostre.cc -wss://nostream.denizenid.com -wss://nostring.deno.dev -wss://pow.nostrati.com -wss://relay-pub.deschooling.us -wss://nostr.jiashanlu.synology.me -wss://nostr.klabo.blog -wss://relay.valireum.net -wss://nostr.fly.dev -wss://nostr.nordlysln.net -wss://nostr.zerofeerouting.com -wss://rsslay.nostr.net -wss://nostr-relay.nonce.academy -wss://nostr.rewardsbunny.com -wss://lv01.tater.ninja -wss://nostr-2.orba.ca -wss://nostr.orba.ca -wss://nostr.supremestack.xyz -wss://nostrrelay.com -wss://relay.nostr.au -wss://nostr.oooxxx.ml -wss://nostr.yael.at -wss://nostr-relay.trustbtc.org -wss://nostr.namek.link -wss://nostr-relay.wolfandcrow.tech -wss://nostr.satsophone.tk -wss://relay.dev.kronkltd.net -wss://nostr2.namek.link -wss://relay.21spirits.io -wss://relay.minds.io -wss://nostr.d11n.net -wss://nostr.tunnelsats.com -wss://nostr.leximaster.com -wss://mule.platanito.org -wss://nostr.robotechy.com -wss://relay.nostrmoto.xyz -wss://relay.boring.surf -wss://nostr.gruntwerk.org -wss://nostr.hyperlingo.com -wss://nostr.ethtozero.fr -wss://nostr.nodeofsven.com -wss://nostr.jimc.me -wss://nostr.utxo.lol -wss://relay.nyx.ma -wss://nostr.shmueli.org -wss://wizards.wormrobot.org -wss://nostr.sovbit.com -wss://nostr.datamagik.com -wss://relay.nostrid.com -wss://nostr1.starbackr.me -wss://relay.nostr.express -wss://nostr.formigator.eu -wss://nostr.xpersona.net -wss://nostr.digitalreformation.info -wss://nostr-relay.usebitcoin.space -wss://nostr-alpha.gruntwerk.org -wss://nostr-relay.australiaeast.cloudapp.azure.com -wss://nostr-relay.smoove.net -wss://nostr-relay.j3s7m4n.com -wss://nostr.demovement.net -wss://nostr.thesimplekid.com -wss://nostr.aozing.com -wss://nostr.blocs.fr -wss://no.str.watch -wss://btc.klendazu.com -wss://nostr.mrbits.it -wss://nostr.zenon.wtf -wss://no.contry.xyz -wss://nostream.gromeul.eu -wss://relay.nostr.ro -wss://nostr.ncsa.illinois.edu -wss://nostr.itssilvestre.com -wss://nostr.chaker.net -wss://knostr.neutrine.com -wss://nostr.pobblelabs.org -wss://nostr.simatime.com -wss://relay.nosphr.com -wss://student.chadpolytechnic.com -wss://nostr.localhost.re -wss://nostr.coinsamba.com.br -wss://deconomy-netser.ddns.net:2121 -wss://nostr.21m.fr -wss://zur.nostr.sx -wss://nostr-relay.texashedge.xyz -wss://spleenrider.herokuapp.com -wss://nostr.bitcoin.sex -wss://relay.nostrzoo.com -wss://nostr.blockchaincaffe.it -wss://nostr-bg01.ciph.rs -wss://knostr.neutrine.com:8880 -wss://nostr.ahaspharos.de -wss://nostr.argdx.net -wss://nostr.snblago.com -wss://merrcurr.up.railway.app -wss://nostr.bingtech.tk -wss://relay.nostr.wf -wss://relay.koreus.social -wss://nostr.randomdevelopment.biz -wss://relay.nostr.hu -wss://relay.nostr.lu -wss://relay.nostr.ae -wss://middling.myddns.me:8080 -wss://nostr.nikolaj.online -wss://relay.nostrology.org -wss://nostr.satoshi.fun -wss://nostream.kinchie.snowinning.com -wss://nostr.lapalomilla.mx -wss://relay.thes.ai -wss://rsr.uyky.net:30443 -wss://nostrafrica.pcdkd.fyi -wss://nostr.bitcoin-basel.ch -wss://relay.21baiwan.com -wss://nostr.ddns.net:8008 -wss://free-relay.nostrich.land -wss://nostr.lukeacl.com -wss://nostr.ddns.net -wss://nostr.rocket-tech.net -wss://nostr-1.afarazit.eu -wss://nostr.0nyx.eu -wss://nostr-mv.ashiroid.com -wss://lbrygen.xyz -wss://nostr.community.networks.deavmi.assigned.network -wss://nostr.ownscale.org -wss://relay1.gems.xyz -wss://nostr.soscary.net -wss://nostr.0xtr.dev -wss://damus.io -wss://relay.alien.blue -wss://nostr.btcmp.com -wss://relayer.fiatjaf.com -wss://relay.lacosanostr.com -wss://adult.18plus.social -wss://nostrrr.bublina.eu.org -wss://relay.stoner -wss://nostr.pwnshop.cloud -wss://nostr.directory -wss://nostr-relay-dev.wlvs.space -wss://member.cash -wss://relay.nyc1.vinux.app -wss://nostr-relay.digitamob.ro -wss://nor.st -wss://nostr.topeth.info -wss://nostr.rocketstyle.com.au -wss://relay.tnano.duckdns.org -wss://nostr.21l.st -wss://electra.nostr.land -wss://relay.codl.co -wss://nostr.koning-degraaf.nl -wss://relay.mrjohnsson.net -wss://nostr.thank.eu -wss://relay.stonez.me -wss://relay.nostr.distrl.net -wss://relay.valera.co -wss://api.semisol.dev -wss://nostr.lol -wss://relay.shitforce.one -wss://n-word.sharivegas.com -wss://lamp.wtf -wss://nostr.bitcoinpuertori.co -wss://nostr-01.bolt.oberver -wss://3d515c5277e9.ngrok.io -wss://nostr.xmrk.mooo.com -wss://alphapanda.pro -wss://relays.world -wss://universe.nostrich.landlangzh -wss://arnostr.permadao.io -wss://relay.chenxixian.cn -wss://universe.nostrich.landlangzhlangen -wss://v2r.chenxixian.cn -wss://nostr-relay-test.nokotaro.work -wss://universe.nostrich.landlangjalangen -wss://nostr.risa.zone -wss://relay.nosbin.com -wss://translate.argosopentech.com -wss://edennostr.land -wss://nostr.kawagarbo.xyz -wss://nostr.member.cash -wss://ch1.duno.com -wss://nostream-production-b80e.up.railway.app -wss://relay1.nostrich.cloud -wss://relay.t5y.ca -wss://nostr.zhongwen.world -wss://nostr.p2sh.co -wss://nostr.thomascdnns.com -wss://nostream.simon.snowinning.com -wss://relay.nostr.blockhenge.com -wss://nostr.buythisdip.com -wss://nostrua.com -wss://relay.bigred.social -wss://lingoh.dev -wss://nostr.poster.place -wss://nostr.geekgalaxy.com -wss://oarnx6xdrq5mygfdrbmzsvh3is3holefpz2x4qwbopwcicwd63gcivid.onion -wss://relay.nostropolis.xyz -wss://nostream-production-ba43.up.railway.app -wss://nostr.sabross.xyz -wss://relay.nvote.co -wss://nostrati.com -wss://cloudnull.land -wss://nostr.frennet.xyz -wss://nostr.wine.com -wss://nostr.sactiontwo.org -wss://nostr.liberty.fans -wss://nostr.primz.org -wss://btc-italia.online -wss://homenode.local:4848 -wss://nostr.frennet.xyzl -wss://relay.roosoft.com -wss://rasca.asnubes.art -wss://nostr.bitcoin.sexanewlycre -wss://nostr.barf.bz -wss://nostr.middling.mydns.jp -wss://relay.xuzmail.com -wss://no-str.wnhefei.cn:28443 -wss://quirky-bunch-isubghsvoi26fbbt3n7o.wnext.app -wss://nostr.fennel.org:7000 -wss://nostr.0ne.day -wss://nostr.vpn1.codingmerc.com -wss://nostr.jacany.com -wss://nostream.lucas.snowinning.com -wss://relay.beta.fogtype.com -wss://nostr.zue.news -wss://nostream.madbean.snowinning.com -wss://nostr2.rbel.co -wss://relay.1bps.io -wss://nostream-relay-nostr.831.pp.ua -wss://zee-relay.fly.dev -wss://nostrrelay.geforcy.com -wss://relay.nostr.jhot.me -wss://nostr.itredneck.com -wss://nostr.h3y6e.com -wss://relay.bitcoiner.social -wss://hos.lol -wss://iris.to -wss://nostr-pub.senisol.dev -wss://nostr-pub.wellirder.net -wss://bitcoinforthe.lol -wss://relav.nostr.info -wss://3e32-200-229-144-129.ngrok.io -wss://nostr-relay.hzrd149.com -wss://nostr-world.h3z.jp -wss://nostr.thesamecat.io -wss://nostr.compile-error.net -wss://relayable.org -wss://mostra.milou.lol -wss://nproxy.cc -wss://nostr.bitmatk.io -wss://coracle.social -wss://umbrel.local:4848 -wss://nostr.ownbtc.online -wss://wss.nostrgram.co:444 -wss://nostr.minimue81.selfhost.co -wss://relay.current.fy -wss://nostream.nostr.parts -wss://nostr.zebede.cloud -wss://nostrwhoop.ph -wss://relay.nostrified.org -wss://nproxy.zerologin.co -wss://nostr.pcdkd.fyi -wss://relay.kongerik.et -wss://nostr.eden.land -wss://nostr.retroware.run.place -wss://relay.humanumest.social -wss://bhagos.org -wss://hushvault.ie -wss://nostream-production-9458.up.railway.app -wss://nostr.nakamotosatoshi.cf -wss://globals.fans -wss://nostr.cruncher.com -wss://nostr.global.fan -wss://relay.nostr.snblago.com -wss://nostr.nokotaro.com -wss://ostr-1.afarazit.eu -wss://relay.zhix.in -wss://stats.nostr.band -wss://nostr.fine -wss://vxlw4rlg7go34ol43g4gxbvfu4txdzjauquvnbptzwjflezs3vik55id.onion -wss://big.fist.black -wss://universe.nostrich.landlangzhlangja -wss://relay.plebz.space -wss://nostrich.land -wss://relay.mynostr.fun -wss://swiss-enigma.ch -wss://nostr1.current.fyi -wss://relay.atlas.nostr.land -wss://nostr.band -wss://n.wingu.se -wss://nostr.jmdtx.com -wss://nostrproxy-1.f7z.io -wss://nostr.ch -wss://roundrockbitcoiners.com -wss://nostr.sept.ml -wss://srelay.roli.social -wss://nostr.monostr.com -wss://nostr.dojotunnel.online -wss://nostrica.dojotunnel.online -wss://nostr.shadownode.org -wss://thes.ai -wss://rsslay.wss -wss://nostr.vol.io -wss://nostrgram.co -wss://habla.news -wss://runningnostr.lol -wss://mostr.pub -wss://relay.example2.com -wss://profiles.f7z.io -wss://nostr.adpo.co -wss://jp-relay-nostr.invr.chat -wss://nostr.anchel.nl -wss://mutinywallet.com -wss://relay.nostrbr.online -wss://filter.eden.nostr.land -wss://relay.nostrdocs.com -wss://relay.nostr.lucentlabs.co -wss://n.xmr.se -wss://nostr.relayer.rs -wss://monad.jb55.com:8080 -wss://nostr.watch -wss://universe.nostrich.landlangenlangzhlangja -wss://nostr.asdf.mx -wss://ts.relays.world -wss://arc1.arcadelabs.co -wss://stealth.wine -wss://nostr.bg -wss://really.nostr.bg -wss://realy.nostr.bg -wss://relai.nostr.bg -wss://relay-verified.deschooling.us -wss://4.up.railway.app -wss://nostr-relay.aapi.me -wss://nostr-z9tc.onrender.com -wss://nostrich.site -wss://nostr.ginuerzh.xyz -wss://310b-200-229-144-129.ngrok.io -wss://nostr.aste.co -wss://black.nostrcity.club -wss://nostr.guru -wss://nostrica.com -wss://relay.leesalminen.com -wss://nostr.shroomslab.net -wss://meta-relay-beta.nostr.wirednet.jp -wss://nostr.reamde.dev -wss://nostr.africa -wss://powrelay.xyz -wss://rsslay.data.haus -wss://nostr.danvergara.com -wss://nostr.one.re -wss://dev2.hazilitt.fiatjaf.com -wss://nostr-2.zebdeee.cloud -wss://zerosequioso.com -wss://brb.io.relay -wss://relay.realsearch.cc -wss://nodestr.fmt.wiz.biz -wss://r.alphaama.com -wss://nostr.relay-wlvs.space -wss://relay.21spiritis.io -wss://nostr.pinkanki.org -wss://nostr.damus.io -wss://fin-nostr.seekdisruption.com -wss://relay.nostr.lu.ke -wss://nostr.rdfried.com -wss://nostr.trustbtc.org -wss://nostr.verymad.net -wss://relay.damus.info -wss://relays.nostrplebs.com -wss://nostr.688.org -wss://15171031.688.org -wss://dgi4mb7antpcmrx4rynm6xq52xzt5duvxa4iwucq4mszgpz6smrjajqd.onion -wss://mastodon.cloud -wss://nostr.ch3n2k.com -wss://nostr.forecastdao.com -wss://nostr.nostrelay.org -wss://nostr.robotesc.ro -wss://nostr.test.aesyc.io -wss://nostr.web3infra.xyz -wss://nostrrelay.maciejz.net -wss://nostrsxz4lbwe-nostr.functions.fnc.fr-par.scw.cloud -wss://nostrpurple.com -wss://rsslay.ch3n2k.com -wss://nos.qghs.in -wss://nostr.nordlysln.net:3241 -wss://nostr.net.in -wss://relay.rip -wss://universe.nostrich.landlangenlangja -wss://wmv-vm.local:4848 -wss://relay.austritch.net -wss://relay.oxtr.dev -wss://rwlay.bigred.social -wss://relay.lexongtonbitcoin.org -wss://knostr.neutrine -wss://nostr.online -wss://filter.stealth.wine -wss://nostream-production-5895.up.railway.app -wss://nostr.stereosteve.com -wss://relay01.apus.network -wss://test.nostr.0x50.tech -wss://nostr.0x50.tech -wss://nostr.256k1.dev -wss://nostr.malin.onl -wss://jqiwgflfw4dezjsy42frompmknrlcfazoiyngftgknj7yrmnhtobd7id.local -wss://b.ayit.org -wss://nostrelay.rajabi.ca -wss://off20chain.pub -wss://nostr.milou.land -wss://nostr.primedomain.fr -wss://nostr.theblockreward.com -wss://anon.computer -wss://relay.hamnet.io -wss://nostramsterdam.vpx.moe -wss://global-relay.cesc.trade -wss://btcpay.kukks.org -wss://relay.cent2sat.com -wss://nostr.mnethome.de -wss://nostream.sh4.red -wss://klockenga.social -wss://nostream.megadope.snowinning.com -wss://nostr.cvilleblockchain.org -wss://nostr.bitocial.xyz -wss://nostr.bitcoiner.socail -wss://nostr-relay.eniehack.net -wss://greenart7c3.dedyn.io -wss://nostr.data.naus -wss://8.tcp.ngrok.io:19607 -wss://relay.nostr24.com -wss://d463rbo7dgbfuxvvxpory2og2etl4gttfzmqcixdq7rpts47lpgolkyd.onion -wss://dublin.saoirse.dev -wss://www.weixin.com -wss://nostr-rs-relay.cryptoassetssubledger.com -wss://nostr.kojira.net -wss://nostr.fan -wss://nostr.pk -wss://getalpy.com -wss://billert.xyz -wss://circle-ay.info -wss://jawsh.xyz -wss://walletofsatoshi.com -wss://ogblock.xyz -wss://nodestrich.com -wss://kunigaku.gith -wss://nostr.21ideas.org -wss://133332.xyz -wss://asats.io -wss://nostrchack.me -wss://chalow.net -wss://cashu.me -wss://tsukemonogit.git -wss://h3y6e.com -wss://elder.nostr.land -wss://xmr.rocks -wss://nostr.build -wss://mofumemo.com -wss://kpherox.dev -wss://sb.nostr.band -wss://tyiu.xyz -wss://welkinhere.githu -wss://ocha.one -wss://in.tips -wss://vitorpamplona.co -wss://fiatjaf.com -wss://akiomik.github.io -wss://stacker.news -wss://nvk.org -wss://lordkno.ws -wss://nostr.indus -wss://lotdkno.ws -wss://nosutora.com -wss://milou.lol -wss://mostr.pu -wss://dergigi.com -wss://shirehodl.com -wss://cash.app -wss://h3z.jp -wss://murachue.cytes.net -wss://snowcait.gith -wss://jb55.com -wss://nodeless.io -wss://orange-crush.com -wss://nostr.com.au -wss://oooxxx.ml -wss://plebs.place -wss://ahr999.com -wss://penpenpng.github.io -wss://nostrpurple.co -wss://thank.eu -wss://weep.jp -wss://tigerville.no -wss://nostrcheck.me -wss://frenstr.com -wss://ln.tips -wss://ryumu.dev -wss://honeyroad.store -wss://harlembitcoin.com -wss://f7z.io -wss://relay.fan -wss://nisshiee.org -wss://www.lopp.net -wss://getalby.com -wss://heguro.com -wss://wil.bio -wss://b.tc -wss://nodedttich.com -wss://relay.taldra.in -wss://relat.nostrica.com -wss://nostr.boring.surf -wss://nostr.raitisoja.net -wss://nostr.astrox.app -wss://nostr.mjex.me -wss://slick.mjex.me -wss://nostr.hrmb.org -wss://relay.semaphore.life -wss://rss.nostr.band -wss://63ragcfwb5xhoe5gfflazfyrde3qjdo73cblhhmnbviizowdo2q5haid.onion:5051 -wss://nostrblip.app -wss://relay.vanderwarker.family -wss://relay-local.cowdle.gg -wss://relay.damus.com -wss://relay.reeve.cn -wss://relay.strfry.net -wss://relay.alxgsv.com -wss://nostrv0l.io -wss://relay.nostrula.com -wss://release.nostr.band -wss://nostr.k3tan.com -wss://nostr.bitcoin.social -wss://thesimplekid.space -wss://relay.ypcloud.com -wss://at.nostrwork.at -wss://nostream.dev.kronkltd.net -wss://nostr-relay.xbytez.io -wss://relay.nostr.io -wss://relay.nvote.co:443 -wss://relay.cryptoculture.com -wss://rjj6ejkihilniytxs56qrgtttgcfnnjvbii6vaas6jzppcmekd63ugad.local -wss://puravida.nostr.land.com -wss://bitcoinmaximalist.online -wss://wine.nostr -wss://nostr.messagepush.io -wss://nostrich.love -wss://relaynostrplebs.com -wss://hamstr.to -wss://yosupp.app -wss://snort.social -wss://relay.nostr.gt -wss://nost.ratchat.nl -wss://nostr.chrissmith.site -wss://i.relay.boats -wss://nostr.wineto -wss://nostr.eluc.ch -wss://nostrplebs.com -wss://nostr.tools.global.id -wss://nostr.rocketnode.space -wss://relay.roli.social -wss://bitcoin.nostr.com -wss://relay.badgr.space -wss://nostriches.club -wss://nostr-check.me -wss://nostrelay.nokotaro.com -wss://rbr.bio -wss://rly.bopln.com -wss://6amyhf3sjvgxe5qzbx4xn52pcnqresdmi7szxurp6umkvz6mthxjdcad.onion -wss://6amyhf3sjvgxe5qzbx4xn52pcnqresdmi7szxurp6umkvz6mthxjdcad.local -wss://20nos.lol -wss://test.theglobalpersian.com -wss://nostr.exposed -wss://nostr-pub.liujiale.me -wss://nostream.frank.snowinning.com -wss://nstrs.fly.dev -wss://eospark.com -wss://relay.nosterplebs.com -wss://nproxy.kristapsk.lv -wss://nostr.universalname.space -wss://relays.snort.social -wss://nostr.how -wss://kukks.org -wss://nostr.dutch.cryptonews -wss://relay.cryptojournaal.net -wss://relay.dutch.cryptonews -wss://nostr.cryptojournaal.net -wss://no-str.wnhefei.cn -wss://www.131.me -wss://rsr.uyky.net -wss://nostr-relay.ie9.org -wss://nostr.fmt.wis.biz -wss://nostr.exotr.dev -wss://nostr.merrcurr.com -wss://realy.damus.io -wss://nerostr.xmr.rocks -wss://rkdgwzgvcgrciemlnfsxqgyrv5whgpw44s6zycokmuchpq4ucflgjtqd.local -wss://nostr.simplex.icu -wss://ralay.damus.io -wss://relay.kronkitd.net -wss://nostr.info -wss://nostr.lnnodeinsight.com -wss://nostr.truckenbucks.com -wss://brt.io -wss://nostr.lingoh.dev -wss://relay.nor.st -wss://nostr-relay.inmarkets.com -wss://wiz.biz -wss://nostream.git -wss://nostr.dpbu.de -wss://wcl2meyp236fa3dmfzfyq6aacbdoixrlocb6zozjs6xklxizschj2did.local -wss://relay.nostr.co.jp -wss://relap.orzv.workers.dev -wss://nostr.bitcoiner.socia -wss://eden.nosrt.land -wss://strfry.cryptocartel.social -wss://nostr.citizenry.technology -wss://universe.nostrich.landlang -wss://nostr.inprivate.network -wss://relay.snort.test -wss://lpkue6jtz3pnp7zok4jwlct4n3mzuffsfrpagiffsuplyaswhdtmpoid.local -wss://nostr.rezhajulio.id -wss://lnbits.eldamar.icu -wss://nostr.freefrom.fi -wss://yael.at -wss://rain8128.github.io -wss://badges.page -wss://latam1-nostr.stealthy.co -wss://nostr.relay.se -wss://nfdn.testnet.dotalgo.io -wss://relav.nostr3.io -wss://ofchain.pub -wss://nostr.fmt.wiz.bi -wss://relay.gui.dog -wss://eden.nostry.land -wss://nostr.relay.limo -wss://relay.nostrichs.org -wss://20nostr.mom -wss://relay20damus.io -wss://nostr.sandwich.pro -wss://sq.qemura.xyz -wss://nostr-pub.seminol.dev -wss://nostr.eunundzwanzig.space -wss://nostr.bitcoinet.social -wss://nostr-relay.untether.me -wss://relay.nostr.pub -wss://nostr.100p.org -wss://7tdom3xuus7ekv423ul46w3j43zyixjj54yoe62bndpcrgii3adeppid.local -wss://nostr.gram -wss://nostr.videre.net -wss://nostr-2.zebedee.cloudwss -wss://nostr-pub.wellorder.netwss -wss://sonzai.net -wss://snort.fail -wss://ppavybjpqjft5slnpeovehbegomhwtvvxtesvwwdrfndz6qe2c5kf2ad.local -wss://nostr.land -wss://relay.nvote.com -wss://purevida.nostr.land -wss://nostr.bitcoiner.soical -wss://nostr.inosta.co -wss://nas.lol -wss://wss.nostr.milou -wss://nostr.cheesebot.org -wss://eyeswideshut.ath.cx -wss://test.relays.world -wss://relay.snort.com -wss://relay.nostrplebs.co -wss://nostr.kimi.im -wss://nostrum.com -wss://wss.nostr.wine -wss://relay.usenostr.org -wss://paladium.my:4848 -wss://umbrel.home.local:4848 -wss://nostr.metamadeenah.com -wss://lnbits.sdbtc.org -wss://relay.nostr.mutinywallet.com -wss://relay.theglobalpersian.com -wss://relay1.easymeta.app -wss://relay.nostris.online -wss://s1.wonder3.org -wss://eden.nostraland -wss://byc.klendazu.com -wss://ephemerelay.mostr.pub -wss://7si6co27cvaw5yjyx6asvxfmaw5ah2arywwgrem4y5svi5ntskoeb5id.onion -wss://nwmdev.com -wss://nostro.online -wss://nostr.massimux.com -wss://nostr.glate.ch -wss://nostr.openordex.org -wss://nostr.schorsch.fans -wss://nostr-relay-dev.nisshiee.org -wss://ibz.me -wss://alexandernostrplebs.com -wss://fishbanananostrplebs.com -wss://relay.nostrcheck.com -wss://nostr.roli.io -wss://nostr.net.za -wss://nostr.worldkey.io -wss://nostr-pub.welloorder.net -wss://nostr.actin.io -wss://nostrzebedee.cloud -wss://nostr.zclub.app -wss://nostr.13x.sh -wss://nostr.totient.xyz -wss://nostr1.federated.computer -wss://nostr.zhix.in -wss://nostr.vdstruis.com -wss://caro-relay.fiatjaf.com -wss://nostr.winewss -wss://notstro.wine -wss://nostr.btc-library.com -wss://nostr.phenomenon.space -wss://nostr.octr.dev -wss://nostr.impervious.live -wss://nostr.plebs.space -wss://iefan.tech -wss://w3ird.tech -wss://yunginter.net -wss://nostr.coach -wss://sleepy.cafe -wss://freespeechextremist.com -wss://nostr.uselessshit.com -wss://liberdon.com -wss://eveningzoo.club -wss://gleasonator.com -wss://mindly.social -wss://ottawa.place -wss://noagendasocial.com -wss://poa.st -wss://misskey.io -wss://misskey.cf -wss://nostr.bybieyang.com -wss://mastodon.online -wss://toad.social -wss://best-friends.chat -wss://universeodon.com -wss://mastodon.world -wss://mastodon.social -wss://nostr.vulpem -wss://nicecrew.digital -wss://front-end.social -wss://nostr-relay.wellorder.net -wss://relays.pro -wss://rsslay.sovbit.host -wss://seal.cafe -wss://social.6bq.de -wss://universe.nostrich.landlangjalangzh -wss://social.xenofem.me -wss://mi.hibi-tsumo.com -wss://mstdn.social -wss://mas.to -wss://relay.rebelbase.site -wss://pixelfed.social -wss://digitalcourage.social -wss://ruby.social -wss://cr8r.gg -wss://nijimiss.moe -wss://returtle.com -wss://lor.sh -wss://toot.community -wss://mstdn.jp -wss://relay.berserker.town -wss://nostr.rajabi.ca -wss://fosstodon.org -wss://misskey.takehi.to -wss://izj3isbk3pmade74ontdijodhehsytnw2iokdhh6k3flk4mq2pau6sid.onion -wss://mastodon.scot -wss://nostriches.org -wss://aus.social -wss://relay.nostr.amane.moe -wss://romancelandia.club -wss://stonez.me -wss://merrcurr.com -wss://nostr.ist -wss://onprem.wtf -wss://fedibird.com -wss://s2.wonder3.org -wss://freespech.casa -wss://disabled.social -wss://relay.house -wss://nostr-pub.welllorder.net -wss://social.teamb.space -wss://infosec.exchange -wss://blockedur.mom -wss://progressivecafe.social -wss://mstdn.ca -wss://c.im -wss://mefi.social -wss://basebitcoinplebs.place -wss://med-mastodon.com -wss://ohai.social -wss://defcon.social -wss://pxlmo.com -wss://zlocur7ctbds4qsdswb3qpkp6n2e2ywqne2fp2tdn4fqubpudfiyxwid.local -wss://thebag.social -wss://mstdn.party -wss://relay.serpae.xyz -wss://clew.lol -wss://qoto.org -wss://relay.exchange -wss://nostr.relayable.org -wss://mastodon.coffee -wss://kmy.blue -wss://home.social -wss://detmi.social -wss://brighteon.social -wss://nostr.nom -wss://theblower.au -wss://astral.nostr.land -wss://beige.party -wss://orangepill.dev -wss://redgreenblue.click -wss://nostr.fredix.xyz -wss://nostr.essydns.ca -wss://nostr.private.network -wss://nostr.member.cas -wss://nostr-rely.digitalmob.ro -wss://relay.nostr -wss://relay.current -wss://no-str.or -wss://pl.gamers.exposed -wss://studentchadpolytechnic.com -wss://scicomm.xyz -wss://relay.alien-sos.gov -wss://masto.es -wss://spinster.xyz -wss://parallels-parallels-virtual-platform.local:4848 -wss://relay.daums.io -wss://kolektiva.social -wss://mastodonapp.uk -wss://convo.casa -wss://sfba.social -wss://techhub.social -wss://leafposter.club -wss://nrw.social -wss://mastodon.uno -wss://handon.club -wss://social.vivaldi.net -wss://nostr.goller.net -wss://minazukey.uk -wss://mstdn.beer -wss://expressional.social -wss://paid.nostr.0x50.tech -wss://mstdn.nere9.help -wss://relay.nostr.ai -wss://relay.noswss -wss://relay.nwss -wss://furry.engineer -wss://uselessshit.co -wss://kosmos.social -wss://mynostr.io -wss://premis.one -wss://social.tchncs.de -wss://retro.pizza -wss://pearl-mount-showed-fishing.trycloudflare.com -wss://kpa4k6acxzjv2m2p72keftbpaymwpq2h67jqnin3d4y3djxyheuifoqd.onion -wss://gm7.social -wss://relays.nostr.info -wss://chitter.xyz -wss://hackers.town -wss://anarchism.space -wss://relay.nostrica -wss://halifaxsocial.ca -wss://asimon.org -wss://nostr.blipme.add -wss://troet.cafe -wss://octodon.social -wss://m.cmx.im -wss://filename-ambassador-distance-mountains.trycloudflare.com -wss://nostr.getgle.org -wss://relay.froth.zone -wss://novoa.nagoya -wss://relay.dispute.systems -wss://clubcyberia.co -wss://thechimp.zone -wss://coolsite.win -wss://puravida.nostra.land -wss://shelter.local:1111 -wss://det.social -wss://nostr-2.zebee.cloud -wss://chaosfem.tw -wss://nostr.v01.io -wss://social.kechpaja.com -wss://mastodon.green -wss://plebchain.nostr -wss://plebchain.nostr.land -wss://relay.onsats.org -wss://u5epuanp2fbie4phw6zekzna6zotvsffji4td4ee7iwgwdxlz4kwqqad.onion:5051 -wss://3ddc8bbee6db.ngrok.app -wss://b4968f09859e.ngrok.io -wss://genserver.social -wss://masto.ai -wss://hachyderm.io -wss://shitpost.cloud -wss://oldbytes.space -wss://mastodon.ie -wss://baraag.net -wss://nostr.dvdt.dev -wss://relay.com.de -wss://nostr.rbel.co -wss://bologna.one -wss://nostr-relay.app -wss://kagamisskey.com -wss://nostr-rs-relay.phamthanh.me -wss://nostr.bcmp.com -wss://blogstack.io -wss://rly.nostrkid.com -wss://mastodon.bida.im -wss://freeatlantis.com -wss://pieville.net -wss://climatejustice.rocks -wss://relay.mynostr.id -wss://relay.farscapian -wss://relay.blogstack.io -wss://orwell.fun -wss://misskey.04.si -wss://sushi.ski -wss://nostr.milou.lo -wss://pawoo.net -wss://tooter.social -wss://mastodon.sdf.org -wss://nostr.com -wss://misskey.design -wss://macaw.social -wss://relay.nostr.inforelay.nostr.band -wss://pub1.southflorida.ninja -wss://strawberry-pudding.net -wss://mastodon-japan.net -wss://nostream.0x50.tech -wss://multiplextr.coracle.social -wss://pleroma.skyshanty.xyz -wss://uxxq6b2enojvflhkrzsg4erakd5rrb7v2cql4m4pspj4xtqwyli47rid.local -wss://masto.deoan.org -wss://replay.damus.io -wss://melhorque.com.br -wss://nostr1.actn.io -wss://nostr.bolt.fun -wss://relay.vtbmoyu.com -wss://eosla.comrelay.zeh.app -wss://bikeshed.party -wss://nostr.xanny.family -wss://milker.cafe -wss://nostr.give.africa -wss://relay.got-relayed.com -wss://cum.salon -wss://puravid.nostr.land -wss://soc.punktrash.club -wss://seafoam.space -wss://h4.io -wss://nostr.swiss.enigma.ch -wss://toot.cafe -wss://sneed.social -wss://newsie.social -wss://indg.club -wss://xoxo.zone -wss://relay.nostr.bitcoiner.social -wss://relay.snort.band -wss://socel.net -wss://social.coop -wss://postpandemicparty.org -wss://merveilles.town -wss://search.nostr.wine -wss://search.nos.today -wss://mstdn-huahin.com -wss://eden.nostr.la -wss://nostrja-kari-nip50.heguro.com -wss://relay.gems.xyz -wss://plnetwork.xyz -wss://nostr.swiss-enigma.sh -wss://geofront.rocks -wss://toot.io -wss://indieweb.social -wss://mastodon.content.town -wss://awaymessage.club -wss://relay.intify.io -wss://mstdn.science -wss://maniakey.com -wss://otadon.com -wss://mstdn.guru -wss://misskey.noellabo.jp -wss://hessen.social -wss://mastodon.top -wss://ipv6.nostr.wirednet.jp -wss://relay.nostr-relay.org -wss://cum.camp -wss://nebbia.fail -wss://current.fyi -wss://bae.st -wss://lolison.network -wss://ioc.exchange -wss://bylines.social -wss://decayable.ink -wss://nostr.unitedserializer.com -wss://urbanists.social -wss://dark-elves.social -wss://writing.exchange -wss://nostr.rikmeijer.nl -wss://misskey.social -wss://ht.nixre.net -wss://mathstodon.xyz -wss://t7jvqwu35hneszx7fihsprbcpwonlcfnsjr4xtn6shqgwbv324w4gdid.local -wss://abla.news -wss://muenchen.social -wss://homeserver.drake-carp.ts.net:4848 -wss://snailedit.social -wss://mastodon.gamedev.place -wss://tech.lgbt -wss://mast.lat -wss://econtwitter.net -wss://veganism.social -wss://btclolap6mm4tl37huslk6j76enq7qxaj2kwq7w6cdr5ros56eletcqd.onion -wss://toot.cat -wss://nostr.zenon.info -wss://misskey.art -wss://nostr.hushvault.ie:4848 -wss://o6ga6lxnax2z7pgkkenifollohkrv55r36mdzdtofi7d5yyif2f4o5yd.onion:5051 -wss://20nostr-pub.wellorder.net -wss://ecoevo.social -wss://nostr.zerofiat.world -wss://nostr.atitlan.io -wss://detroitriotcity.com -wss://rollenspiel.social -wss://paquita.masto.host -wss://literatur.social -wss://dave.st.germa.in -wss://sunny.garden -wss://nostrpro.xyz -wss://relay.getalby.com -wss://misskey.systems -wss://mamot.fr -wss://social.anoxinon.de -wss://relay.nostr.social -wss://mindmachine.org -wss://microblog.club -wss://relay.utxo.com -wss://universe.nostrich.landlangth -wss://social.teci.world -wss://social.freetalklive.com -wss://mk.absturztau.be -wss://social.ornella.xyz -wss://left-tusk.com -wss://bofh.social -wss://a11y.social -wss://shroomslab.net -wss://relay2.nostr.vet -wss://mugicha.club -wss://laserbeak.local:4848 -wss://annihilation.social -wss://humble.cafe -wss://nostr.relay.damus.io -wss://nostr.milol.lol -wss://nostr.blimpme.app -wss://noc.social -wss://wetdry.world -wss://nostr.taxi -wss://nostr.21-bitcoin.org -wss://relay.llevotu-bitcoiners.info -wss://pl.kitsunemimi.club -wss://djsumdog.com -wss://citadel.local:4848 -wss://federate.blogpocket.com -wss://chaos.social -wss://mastodon.me.uk -wss://oxtr.dev -wss://misskey.cloud -wss://varishangout.net -wss://lacosanostr.com -wss://willem.currycash.net:4848 -wss://lightniningrelay.com -wss://queer.party -wss://lightning.relay.com -wss://mastodon.nl -wss://purplepag.es -wss://cupoftea.social -wss://bitcoiner.socialwss -wss://relay.current.io -wss://sigmoid.social -wss://wandering.shop -wss://braydmedia.de -wss://quey.la -wss://nostr.zebeedee.cloud -wss://nostr-2.zebeedee.cloud -wss://artsio.com -wss://nostr1676031941328.app.runonflux.io -wss://arsip.ddns.net -wss://relay.nostrcitadel.org -wss://relay.nostr-citadel.org -wss://nostr-citadel.org -wss://blastr.f7z.io -wss://nostr.myowndamnnode.com -wss://snowdin.town -wss://nostr.zxcvbn.space -wss://relay.notmandatory.org -wss://bitcoin.social -wss://friendsofdesoto.social -wss://zirk.us -wss://digipres.club -wss://nostr-test.elastos.io -wss://nostr.onsat.org -wss://damus.relay.io -wss://beefyboys.win -wss://nostrija-kari.heguro.com -wss://froth.zone -wss://ns.penseer.com -wss://relay.nostrical.com -wss://nostr.hoshizora.ch -wss://kunigaku.github.io -wss://nostr.shino3.net -wss://umbrel.tailbb128.ts.net:4848 -wss://tailbb128.ts.net:4848 -wss://fedi.twoshortplanks.com -wss://filter.nostr.winebroadcasttrue -wss://nostr.doufu-tech.com -wss://relay.hodl.haus -wss://pgh.social -wss://coeditor-congested.fractalnetworks.co -wss://mastodon.podaboutli.st -wss://frenfiverse.net -wss://nostr.f4255529.fun -wss://nostream.megadope.snowinning -wss://kiritan.work -wss://forever21.lol -wss://zitron.net -wss://mastodon-swiss.org -wss://relay1.current.fyi -wss://nostr.plebs.com -wss://multiplextr.corocal.social -wss://nostr.zedebee.cloud -wss://nostr.mutinywallet.comisntbanned.addthatonesotheycangetnotesrelayedtotherestofthenetworkfrominsideofchina -wss://mastodon.mit.edu -wss://relaynostrati.com -wss://relaynostr.band -wss://relaynostr.info -wss://relaychenxixian.cn -wss://relaysnort.social -wss://relaynostr.com.au -wss://nostr-tbd.website -wss://relay.hoshizora.ch -wss://social.balsillie.net -wss://strangeobject.space -wss://relay.nvote20.co -wss://fla.red -wss://urusai.social -wss://chad.polytechnic.com -wss://robo358.com -wss://conxole.io -wss://relay.ohbe.me -wss://nostr.bitcoiner.com -wss://www.mutinywallet.com -wss://x.9600.link:10070 -wss://a11y.info -wss://homelab.host -wss://k65qz57zx4sw24fow2bvchjgmpjqljj4cp3oa7dtpbbprjifsdotggid.local -wss://relay.vtuber.directory -wss://x.9600.link:8000 -wss://nostr.plebs.win -wss://renkontu.com -wss://nost.massmux.com -wss://submarin.online -wss://social.librem.one -wss://relai.kongerik.et -wss://mstdn.io -wss://astral.swiss-enigma.ch -wss://stereophonic.space -wss://relayer.pleb.social -wss://mastodon.nz -wss://nostr.nostr.de -wss://nostr.thezap.club -wss://proxy.shroomslab.net -wss://pfr24mrpxowclhm4y6adu36kbo3erx7gskzyfqqhnhfpdkdmylpfgkyd.local -wss://nostr.hodl.haus -wss://vtdon.com -wss://relay.nost.band -wss://fnxwipsg3lfzij64lvjgmutvkkpd7eo2mr2khxkofyywf3vsvbk73jad.onion -wss://fnxwipsg3lfzij64lvjgmutvkkpd7eo2mr2khxkofyywf3vsvbk73jad.local -wss://md.hugo.nostr -wss://boks.moe -wss://truthsocial.co.in -wss://mastodon.xyz -wss://relay.universalname.space -wss://relay-nostr.wirednet.jp -wss://fedi.pawlicker.com -wss://favcalc.com -wss://rot13maxi.com -wss://awayuki.net -wss://mazinkhoury.com -wss://g0v.social -wss://shpposter.club -wss://a.lufimianet.jp -wss://pura20vida.nostr.land -wss://sotalive.net -wss://sendsats.lol -wss://vida.page -wss://wagvwfrdrikrqzp7h3b5lwl6btyuttu7mqpeji35ljzq36ovzgjhsfqd.onion -wss://uec2cmjauzufrtlq6wq6l2ujfncdvo3suezz423gsvz5xvhehm2mcgid.onion -wss://mastodon-belgium.be -wss://rejecttheframe.xyz -wss://nogood.store -wss://getaiby.com -wss://plebchain.club -wss://nostrverified.com -wss://x.9600.link -wss://n0p0.shroomslab.net -wss://orangemakura.xyz -wss://jamw.net -wss://7ab7qqbj2dw3pjnkoskgsfn4ikqc7orwnkpmcfjmeobw63kf4zgykjid.local -wss://snabelen.no -wss://floss.social -wss://mastoot.fr -wss://kmc-nostr.amiunderwater.com -wss://hodl.camp -wss://xmr.usenostr.com -wss://nostr-pub.wellorn.net -wss://gudako.net -wss://ryona.agency -wss://kafeneio.social -wss://massmux.com -wss://freezepeach.online -wss://nostream-production-f83d.up.railway.app -wss://taobox.pub -wss://mastodon.radio -wss://einundzwanzig.relay.com -wss://journa.host -wss://social.here.blue -wss://toot.wales -wss://staging.nostr.com.se -wss://pleroma.soykaf.com -wss://berserker.town -wss://zapforart.site -wss://forall.social -wss://nostrja-kari.heguro.comyee -wss://higheredweb.social -wss://social.gnuhacker.org -wss://the.hodl.haus -wss://ng4jk6yiqgfczo4wyxszuj7w6jok3fptehu533o3mlzs3vph3dvjfdid.onion -wss://nostr.mtpx.ovh -wss://relay.coollamer.com -wss://glasgow.social -wss://fedi.absturztau.be -wss://nostr.frostr.xyz -wss://relay.runningnostr.lol -wss://relay2.vtuber.directory -wss://nerdculture.de -wss://nostr-relay.untethr.meoperator -wss://nostr.verif-slothy.win -wss://nostr.pub -wss://social.process-one.net -wss://invillage-outvillage.com -wss://otofu.uk -wss://universe.nostrich.landlangenlangpt -wss://mastodon.iriseden.eu -wss://frighteningdeafeningagent.nailuogg.repl.co -wss://obo.sh -wss://relay.hodlhaus.net -wss://rdrama.cc -wss://nostr.packetlostandfound.us -wss://test.relay -wss://mastodonbooks.net -wss://southflorida.ninja -wss://blob.cat -wss://tooting.ch -wss://relay.pineapple.pizza -wss://relay.nostr.directory -wss://relar.nostr.bg -wss://reisen.church -wss://relay.honk.pub -wss://nostr.rsfriedl.com -wss://relay.nort.social -wss://1611.social -wss://the.hodl.house -wss://relayable.com -wss://fedi.syspxl.xyz -wss://nostrpub.yeghro.site -wss://skr5bbrgzfnideglw4cs2iw6au2jm2b7gupocxbmkv5qopo6rcitmiqd.local -wss://a2mi.social -wss://status.relayable.org -wss://toot.blue -wss://btcqspp5dl4rlgl5pomcyv3odfeki7a5zrmjoekyu5vsoqz5bth4e7yd.onion -wss://bitcoinr6de5lkvx4tpwdmzrdfdpla5sya2afwpcabjup2xpi5dulbad.onion -wss://7tdom3xuus7ekv423ul46w3j43zyixjj54yoe62bndpcrgii3adeppid.onion -wss://dnze4ekho2kuiejwatjw5omeprtmdaum2ukok52roiu5rztii3rp2aid.onion -wss://53snncs7vegargpaardbxjnii2oan3xpmbeaf6czwoqa2axz5mvbsjid.onion -wss://fnqdhz3df33da6wxg7jskvumd5rjn3nknln6ecun7uwwysc7vkwkjgid.onion -wss://relay.thefockinfury.wtf -wss://silliness.observer -wss://freak.university -wss://piaille.fr -wss://nostr.weking.tk -wss://f.reun.de -wss://radixrat.com -wss://hostux.social -wss://chat.freenode.net -wss://no.str..cr -wss://nostr.inoata.cc -wss://kappa.seijin.jp -wss://floyds.io -wss://mast.dragon-fly.club -wss://zbd.ai -wss://misc.name -wss://pdx.social -wss://0w0.is -wss://d6jvu2tev2rblkuzgu4ydw2413jizr53j26ut47hxpykvtusbvekhiid.onion -wss://d6jvu2tcv2rblkuzgu4ydw24l3jizr53j26ut47hxpykvtusbvekhiid.onion -wss://drcassone.social -wss://mastodon.energy -wss://rayci.st -wss://ischool.social -wss://nostr.halfway2forever.com -wss://relay.nostr.rocks -wss://nostr.stoner.com -wss://2nodez.com -wss://rs.nostr-x.com -wss://schleuss.online -wss://thrashzone.org -wss://relay.nostrgraph.com -wss://relay.2nodez.com -wss://occult-zuki.com -wss://mastodon.lithium03.info -wss://bigbadpc.local:4848 -wss://social.gr0k.net -wss://nostr.wirednet.jp -wss://relay.plebster.com -wss://bitcoin.nostr -wss://mastodon.im -wss://brb..io -wss://nostr.sandwhich.farm -wss://relay.nos.lol -wss://beta.nostr.v0l.io -wss://eden.nostr.space -wss://powerlay.xyz -wss://rap.social -wss://relay.mutinywallet.com -wss://rogue.earth -wss://600.wtf -wss://klabo.blog -wss://petrikajander.com -wss://tgkzmdd.help -wss://nostr.red -wss://brb.lol -wss://jz2l2bf6f6wssdqwkg7ogthkc5i3ymyiwkaz3tbhff6ro3h3zqddekyd.onion -wss://mstdn.mini4wd-engineer.com -wss://nostr.exposd -wss://nostr.a-ef.org -wss://computerfairi.es -wss://social.targaryen.house -wss://o3o.ca -wss://walkah.social -wss://cosocial.ca -wss://filter.nostr.band -wss://relais.nostrview.com -wss://gigaohm.bio -wss://dobbs.town -wss://bark.lgbt -wss://mastodon.gal -wss://snug.moe -wss://genomic.social -wss://relay.orangepilldev.com -wss://social.sdf.org -wss://social.camph.net -wss://mstdn.poyo.me -wss://nein.lol -wss://nostr.i00.org -wss://kemono.ink -wss://mu.zaitcev.nu -wss://libera.site -wss://ca.hibi-tsumo.com -wss://social.bund.de -wss://xscape.top -wss://social.lol -wss://birds.town -wss://arnostr.com -wss://nostr.33co.de -wss://relay.nostr.lighting -wss://metadata-contacts-relays.pages.dev -wss://webs.node9.org -wss://pleroma.elementality.org -wss://suya.place -wss://livellosegreto.it -wss://peoplemaking.games -wss://nattois.life -wss://typo.social -wss://neutrine.com -wss://ragner-relay.com -wss://wss.nostr.uselessshit.co -wss://wss.nostrue.com -wss://relay.nvote.co:433 -wss://disobey.net -wss://rneetup.com -wss://arnostr.com:8433 -wss://relay.uxto.one -wss://relay.hackerman.pro -wss://thisis.mylegendary.quest -wss://poliversity.it -wss://sats.lnaddy.com -wss://rs2.abaiba.top -wss://rs1.abaiba.top -wss://rs2.abaiba.top.abaiba.top -wss://social.matarillo.com -wss://nostr01.counterclockwise.io -wss://backup.local:4848 -wss://touhou.vodka -wss://mi-wo.site -wss://nostr.f7z.io -wss://alive.bar -wss://strfry.nostr-x.com -wss://mastodontti.fi -wss://nostr.wellorder.net -wss://y.9600.link:8000 -wss://ephemrelay.mostr.pub -wss://byc-italia.online -wss://fissionator.com -wss://stranger.social -wss://eupolicy.social -wss://nostr-desktop.local:4848 -wss://aoir.social -wss://mstdn.plus -wss://nostrproxy.io:3333 -wss://mastodon.hams.social -wss://jorts.horse -wss://metalhead.club -wss://dice.camp -wss://mstdn.y-zu.org -wss://loffchain.pub -wss://mastodon.llarian.net -wss://nostr-relay2.thefockinfury.wtf -wss://2g2jzcfgq5lcrceuq23lmya2drm3ku5qmqimr3bvu3amol55vidctrad.onion -wss://mastodon.au -wss://bgme.me -wss://nostr.badran.xyz -wss://nostr.coincreek.com -wss://nostream-test.up.railway.app -wss://relay.blackthunder.click -wss://relay.grorp.com -wss://atomicpoet.org -wss://iddqd.social -wss://gusto.masto.host -wss://lifehack.social -wss://blorbo.social -wss://freecumextremist.com -wss://13bells.com -wss://rsslay.nostr.netrelay -wss://relay.zerosequioso.com -wss://nauka-relay.herokuapp.com -wss://nostr.nofdeofsven.com -wss://out.of.milk -wss://cawfee.club -wss://r.relay.fan -wss://alo.ottonove891.cf -wss://keinoha.tailnet-0240.ts.net -wss://nostr.paralelnipolis.cz -wss://tuiter.rocks -wss://elizur.me -wss://nostr-dev.newstr.io -wss://discuss.systems -wss://blahaj.zone -wss://mastodon.art -wss://makersocial.online -wss://gamepad.club -wss://nostr.flameofsoul.ru -wss://dmv.community -wss://relay.nostr.com -wss://nostr-desktop.saiga-shark.ts.net:4848 -wss://nostrfoxden.ddns.net:4848 -wss://soc.umrath.net -wss://ravenation.club -wss://oisaur.com -wss://nostr-relay.net -wss://social.mikutter.hachune.net -wss://lewacki.space -wss://fediscience.org -wss://todon.eu -wss://nuccy-nuc7i5bnk.local:4848 -wss://games.gamertron.net:4848 -wss://fiedlerfamily.net -wss://postnstuffds.lol -wss://nostr.planetary.social -wss://worldkey.io -wss://hcommons.social -wss://gymp7qquljs47xbbvs47hkptnyyzegy2jkst26mkjxaciifqffjatqid.onion -wss://rs3.abaiba.top -wss://sudo-nostr.com -wss://satgag.site -wss://nostr.lnbitcoin.cz -wss://relay20nostrplebs.com -wss://2pbkpndvpeebljfvjew6auq63lndzszqnntct5aqfmazslerzxe75kad.onion -wss://dragonchat.org -wss://welcome.nostr.wine -wss://relay.nostr.land -wss://social.linux.pizza -wss://dnppj4kopczovvzvpzmihv2iwe5wt3gbrxjnltjc2zdjpttrdz4owpad.onion -wss://potofu.me -wss://nostrbr.online -wss://mastorol.es -wss://notebook.taild34d0.ts.net -wss://t.aqn.jp -wss://nostr.mutinywallet -wss://wcone.nostr.wine -wss://norden.social -wss://eostagram.com -wss://shigusegubu.club -wss://toot.jkiviluoto.fi -wss://kiwifarms.cc -wss://swiss-talk.net -wss://v532btfg2fb4za2g476a7w23pgpkllc7uq274wqtktwjogt5ynb3ukqd.local -wss://mstdn.maud.io -wss://arc1.arcadelabs.com -wss://nostr.jp -wss://relay-jp.nostr.wirrdnet.jp -wss://climatejustice.social -wss://witter.cz -wss://mastodon.pnpde.social -wss://ttrpg-hangout.social -wss://beehaw.org -wss://thecanadian.social -wss://nostr.fbxl.net -wss://relay.sandwich.farm -wss://nostr.olwe.link -wss://botsin.space -wss://zeroes.ca -wss://photog.social -wss://paid.nostr.lc -wss://free.nostr.lc -wss://test.nostr.lc -wss://gzanlkgurj7zd3psqms3da4vrw4imurnyyzaycfuiiug7elqow7xlayd.onion:5051 -wss://masto.nu -wss://mastodon.uy -wss://bit.relay.center -wss://offchain.relay.center -wss://damus.relay.center -wss://wine.relay.center -wss://eden.relay.center -wss://moth.social -wss://nostr.masmux.com -wss://chrome.pl -wss://mastodon.ktachibana.party -wss://ak.kawen.space -wss://mementomori.social -wss://relay.s3x.social -wss://lnbits.michaelantonfischer.com -wss://yof23ggqmert72c5wcl5qglphapy3o2xjdedtkbrn2dt5rbae2s7f6qd.onion -wss://relay.snort.socail -wss://post.lurk.org -wss://yiff.life -wss://q3zaylwjjhq77yzx34lbydz26szzjljberwetkjgxgsapcekrpjzsmqd.onion -wss://lnbits.b1tco1n.org -wss://welcome.nostr.relay -wss://sound-money-relay.denizenid.com -wss://carnivore-diet-relay.denizenid.com -wss://africa.nostr.joburg -wss://nostr.jolt.run -wss://nostr.chainbits.co.uk -wss://ithurtswhenip.ee -wss://nostr.cloudversia.com -wss://relay1.east.us.nostr.btron.io -wss://ca.orangepill.dev -wss://pdx.land -wss://linh.social -wss://okla.social -wss://androiddev.social -wss://spore.social -wss://mastodo.fi -wss://kabedon.space -wss://nost.inosta.cc -wss://relay2cdamus.io -wss://nostr.openhoofd.nl -wss://dragonscave.space -wss://genart.social -wss://dewp.space -wss://layer8.space -wss://qou7zzll2mxx2ehl73n6pptmhizl5b3entowljlin3sqhcvltxdtlmad.onion:5051 -wss://nostr.wines -wss://relay.snort.relay.ryzizub.com -wss://pixelfed.de -wss://nostr.holyscapegoat.com -wss://nostr.einunzwanzig.space -wss://nostr.hifish.org -wss://colearn.social -wss://topspicy.social -wss://mastodon.neat.computer -wss://relay.nostr.hach.re -wss://nostr.dakukitsune.ca -wss://7ab7qqbj2dw3pjnkoskgsfn4ikqc7orwnkpmcfjmeobw63kf4zgykjid.onion -wss://esq.social -wss://famichiki.jp -wss://tribe.net -wss://masto.nobigtech.es -wss://umbrel-nuc.local:4848 -wss://mastodon.cocoasamurai.social -wss://debian.taildd32b.ts.net:4848 -wss://vlt.ge -wss://relay.johnnyasantos.com -wss://snort.relay.center -wss://nb.relay.center -wss://waag.social -wss://concentrical.com -wss://stat.rocks -wss://oransns.com -wss://relay-jpp.nostr.wirednet.jp -wss://oc.todon.fr -wss://jundow.gitlab.io -wss://neurodifferent.me -wss://jazztodon.com -wss://nostrich.friendship -wss://indieauthors.social -wss://werunbtc.com -wss://frogtalk.lol -wss://pop-os.local:4848 -wss://fediver.de -wss://d6qxo55dhms6revgrmbindvb5ejd3gw5hrji7ylkm6khghii3hjs3uyd.onion -wss://eldritch.cafe -wss://karlsruhe-social.de -wss://social.yl.ms -wss://nostr.mycloudhouse.duckdns.org -wss://nostr.otc.sh -wss://nya.social -wss://relay2.nostrchat.io -wss://relay1.nostrchat.io -wss://nostrja-world-relays-test.heguro.com -wss://ndk-relay.local -wss://reespeech.casa -wss://pleroma.atyh.cc -wss://lawfedi.blue -wss://akkoma.jasminetea.uk -wss://peeledoffmy.skin -wss://plush.city -wss://astrodon.social -wss://samenet.social -wss://toot.bike -wss://mi.yukioke.com -wss://social.growyourown.services -wss://mastodon.nu -wss://lou.lt -wss://functional.cafe -wss://relaydamus.io -wss://coma.social -wss://social.fbxl.net -wss://biplus.social -wss://toots.matapacos.dog -wss://psychoet.ml:3250 -wss://danserver.equipment -wss://nostr.lacrypta.com.ar -wss://wonkodon.com -wss://nostr.seankibler.com -wss://autistics.life -wss://cambrian.social -wss://rvqkqr5kl3dvvxyn67rfowcnvoflx4zby5tjbysavym4ycckti4dbjyd.onion -wss://swiss.nostr.lc -wss://snac.saifulh.online -wss://loma.ml -wss://nostr.privoxy.io -wss://366.koyomi.online -wss://mastodon.stormy178.com -wss://podcastindex.social -wss://bitcoiner.socia -wss://fedi.ml -wss://replayable.org -wss://nostr.schroomslab.net -wss://nostr.global.fans -wss://relay.weedstr.net -wss://vocalodon.net -wss://relay.nostr.bandadd -wss://nostr.oxtr.devadd -wss://jarvis.taild68e2.ts.net:4848 -wss://t7jvqwu35hneszx7fihsprbcpwonlcfnsjr4xtn6shqgwbv324w4gdid.onion -wss://stonez.local:4848 -wss://notrustverify.ch -wss://woof.group -wss://mastodon.floe.earth -wss://lnbits.plebtag.com -wss://kpop.social -wss://relay.wavlake.com -wss://mastodon.sharma.io -wss://travelpandas.fr -wss://alphapanda.prowss -wss://relay.saes.io -wss://barelysocial.org -wss://masto.komintern.work -wss://norcal.social -wss://nostr.zbd.gg -wss://mk.outv.im -wss://mstdn.mx -wss://col.social -wss://nostr.freedom.fi -wss://filter.nostr.winebroadcasttrueglobalall -wss://eden.nostr.landv -wss://me.dm -wss://emacs.ch -wss://winonostr.wine -wss://infoplebstr.com -wss://mas.towss -wss://gruene.social -wss://relay.freeplace.nl -wss://itis.to -wss://bsky.social -wss://lnbits.thefockinfury.wtf -wss://5xxkt7zvmh4zdsjw64lgvchjdlrrgw4w2huujiiud35qms6gnkn5azad.onion -wss://eientei.org -wss://artisan.chat -wss://nustr.mom -wss://relay.nostrhraph.net -wss://shitposter.club -wss://nostril.cam -wss://nostr.spaceshell.xyz -wss://relay.wtr.app -wss://tdd.social -wss://d3meec25b53kegrnjmtmtyynikbkmuxf4jqgtk3sonjs6e62hpaezyqd.onion -wss://tkz.one -wss://freerelay.xyz -wss://nfdn.betanet.dotalgo.io -wss://mitra.social -wss://hablanews.io -wss://calle.wtf -wss://voskey.icalo.net -wss://nostr.sloyhy.win -wss://framapiaf.org -wss://nostrnodeofsven.com -wss://ciberlandia.pt -wss://gnusocial.net -wss://relay.s3x.socia -wss://paste.2nodez.com -wss://union.place -wss://bofh.socia -wss://sovbit.dev -wss://lnbits.btc-payserver.eu -wss://osna.social -wss://im-in.space -wss://junxingwang.org -wss://relay.nostr.wirednet.jpcheck -wss://libranet.de -wss://fedisnap.com -wss://woodpecker.social -wss://gensokyo.town -wss://social.imirhil.fr -wss://links.potsda.mn -wss://law-and-politics.online -wss://nostrich.bar -wss://tweesecake.social -wss://nostr.kisiel.net.pl -wss://relay.kisiel.net.pl -wss://calckey.social -wss://nostr.cercatrowa.me -wss://meganekeesu.tokyo -wss://lndiscs.duckdns.org -wss://omochi.xyz -wss://wue.social -wss://nostr.libreleaf.com -wss://rusnak.io -wss://rsslay-production.up.railway.app -wss://nostr.yuhr.org -wss://bod4ojj37fneith2setv3qjbii563wesbjqgdipdz4ag6voic2xk5iad.onion -wss://fediverse.blog -wss://pouet.chapril.org -wss://baq5ufl2rnczpalnoqabxwpjm3kvhzduwgvptxzx7yq37oqdbgf65syd.local -wss://baq5ufl2rnczpalnoqabxwpjm3kvhzduwgvptxzx7yq37oqdbgf65syd.onion -wss://cryptodon.lol -wss://nostr.debancariser.com -wss://mizunashi.hostdon.ne.jp -wss://relay.deezy.io -wss://bbq.snoot.com -wss://historians.social -wss://mi.mashiro.site -wss://mastodonpost.social -wss://nostrpub.welliorder.net -wss://social.cologne -wss://metapixl.com -wss://wandzeitung.xyz -wss://lightninhrelay.com -wss://techopolis.social -wss://lnbits.btcpins.com -wss://purplenostrich.com -wss://onewilshire.la -wss://sself.co -wss://anygemini13.blogs.sapo.pt -wss://federated.press -wss://metadata.nostr.com -wss://nostr.hodl.ar -wss://goreslut.xyz -wss://mastodon.com.tr -wss://climatejustice.global -wss://brotka.st -wss://sueden.social -wss://mstdn.fr -wss://abid.cc -wss://lnbits.fuckedbitcoin.com -wss://meow.social -wss://nostr.rehab -wss://mstdn.media -wss://nodeo1.nostress.cc -wss://nostrmassmux.com -wss://sauropods.win -wss://civilians.social -wss://pnw.zone -wss://zebeedee.cloud -wss://nostr.walletofsatishi.com -wss://aufovmqaxj5nhqmtorhgpogdjxefhkff25cbyyjt2sub3vwg6b6rplid.onion -wss://forfuture.social -wss://76f67qcwxsxpz7cfozlzunota2ejqznpldc5pnqtyq233hjpjzrmlfid.local -wss://toot.garden -wss://umbrel.tail9dfb.ts.net:4848 -wss://mastodon.chasem.dev -wss://misskey.pm -wss://merovingian.club -wss://chirp.enworld.org -wss://paid.no.str.ce -wss://masto.bike -wss://masto.1146.nohost.me -wss://eosla.comno-str.orgrelay.zeh.appno-str.orgrelay.zeh.app -wss://nixnet.social -wss://pay.zapit.live -wss://respublicae.eu -wss://nekomiya.net -wss://mastodon.internet-czas-dzialac.pl -wss://plushies.social -wss://lnb.openchain.fr -wss://mastodon.lol -wss://social.rebellion.global -wss://ruhr.social -wss://mi.farland.world -wss://pkutalk.com -wss://systemli.social -wss://nostr.minimue81.selfhost.com -wss://mastodon.la -wss://everything.happens.horse -wss://pagan.plus -wss://clacks.link -wss://u-tokyo.social -wss://fediverse.projectftm.com -wss://mastodon.bachgau.social -wss://social.kabi.tk -wss://ty3zdjkwlxo4zah6tgdoolznjcbvkhxpcvjyqe2buxeg23hbeyvr3rad.local -wss://pleroma.wakuwakup.net -wss://social.horrorhub.club -wss://nostr.nightowlstudios.ca -wss://mastodon.ml -wss://relay.orangepillapp.com -wss://misskey.sup39.dev -wss://mfmf.club -wss://pokemon.mastportal.info -wss://gohan-oisii.net -wss://aipi.social -wss://nostr.semisol.com -wss://oslo.town -wss://relay.layer.systems -wss://naharia.net -wss://social.elbespace.de -wss://linuxrocks.online -wss://b81m3pf94ridtry53g8ufyyrjtjaoxgbyjbs5k8qrqkr1whocxiy.loki:8080 -wss://lvl01.tater.ninja -wss://kinky.business -wss://relay.bitblockboom.com -wss://fedi.omada.cafe -wss://social.secret-wg.org -wss://celebrity.social -wss://weirdo.network -wss://mastodon.design -wss://berlin.social -wss://misskey.yukineko.me -wss://mindmachine.688.org -wss://sackheads.social -wss://a.farook.org -wss://social.ridetrans.it -wss://nostr-2.crypticthreadz.com -wss://test.itas.li -wss://fashionsocial.host -wss://ordinary.cafe -wss://social.arinbasu.online -wss://nostr.crypticthreadz.com -wss://relay.zebedee.cloud -wss://nostr.pub.wellorder.net -wss://09d4-5-161-189-144.ngrok-free.app -wss://andalucia.social -wss://udongein.xyz -wss://squeet.me -wss://mastodon.org.uk -wss://guild.pmdcollab.org -wss://relay.fi -wss://black.nostrscity.club -wss://relay.darker.to -wss://denostr.paiya.app -wss://noste.lu.ke -wss://wss.node01.nostress.cc -wss://theverge.space -wss://swiss.social -wss://relay.webstr.org -wss://nostr.shsbt.xyz -wss://relay.nostr.mom -wss://bitcoinmaximlaists.online -wss://relay.openhoofd.nl -wss://toot.aquilenet.fr -wss://toot.ale.gd -wss://relay.devstr.org -wss://lounge.town -wss://amala.schwartzwelt.xyz -wss://planetasieve.com.br -wss://alcrypt.ru:20911 -wss://webzero.grin.plus:8080 -wss://pylons.lightlns.com:28556 -wss://etourneau.fr:28343 -wss://sentie.relay.rts.network -wss://hermes.boarstudios.com -wss://non-central.pw -wss://nostrum.casa -wss://press.coop -wss://neovibe.app -wss://mstdn.starnix.network -wss://nostr.ameristraliagov.com -wss://cryptodon.chat -wss://umbrell.local:4848 -wss://mastodon.codingfield.com -wss://fe.disroot.org -wss://national.catposting.agency -wss://mastodon.pinewoodroad.net -wss://podcasts.social -wss://20nostr.semisol.dev -wss://nostr.oxtr.net -wss://mstdn.o-nature-culture.net -wss://dearcoati6.lnbits.com -wss://die-partei.social -wss://donotban.com -wss://creative.ai -wss://metaskey.net -wss://spacey.space -wss://node01.nostreess.cc -wss://node01.nostress.co -wss://niscii.xyz -wss://3gkpphcfwb6w5iq6axnmlbvr7pz2t37uy4ofocyijzttzrbz4jy43fid.onion -wss://3gkpphcfwb6w5iq6axnmlbvr7pz2t37uy4ofocyijzttzrbz4jy43fid.local -wss://sportsbots.xyz -wss://videos.lukesmith.xyz -wss://nostr.btcfreedom.ca -wss://gardenstate.social -wss://bg-btc.local:4848 -wss://0fa53e299287.ngrok.app -wss://bird.makeup -wss://nlayer.lbdev.fun -wss://relay.queiroz.vip -wss://mstdn.business -wss://osage.moe -wss://botrelay.com -wss://filter.wine -wss://gingadon.com -wss://noncentral.pw -wss://honi.club -wss://xn--baw-joa.social -wss://nostr.semisol.devwss -wss://relay.iris.to -wss://mynostrrelay.deno.dev -wss://social.heise.de -wss://vavursybkbgfyow7nnst5jnqsj2xyteusf3zeerbjdizq6y7h25v4syd.onion:5051 -wss://vavursybkbgfyow7nnst5jnqsj2xyteusf3zeerbjdizq6y7h25v4syd.onion:5050 -wss://relao.nostr.bg -wss://phpc.social -wss://mastodon.kylerank.in -wss://gameliberty.club -wss://rot.gives -wss://www.nostrweb.xyz -wss://ligma.pro -wss://mastodon.grin.hu -wss://geeknews.chat -wss://devdilettante.com -wss://relay.nosr-latam.link -wss://nosr.bitcoiner.social -wss://raru.re -wss://create-key.net -wss://lgbtqia.space -wss://fluffy.family -wss://mas.town -wss://bird.froth.zone -wss://akkoma.cryptoschizo.club -wss://relay.ramus.io -wss://40two.site -wss://relay.40two.site -wss://vis.social -wss://mk.paritybit.ca -wss://nyan.network -wss://ln.weedstr.net -wss://wikis.world -wss://social.fringe.com -wss://umbraxenu.no-ip.biz -wss://heads.social -wss://tsqdakwo4dh5ej3llsi52ftxfbialteu3jm4cmvxaksl3psbyeoyxxqd.onion -wss://jameliris.to -wss://mastodon.thirring.org -wss://miniwa.moe -wss://welcom.nostr.wine -wss://nostr.vulpem.comwss -wss://relay.semisol.dev -wss://kitsunes.club -wss://songbird.cloud -wss://nostr.wyssblitz.org -wss://libera.tokyo -wss://trpger.us -wss://comam.es -wss://nostr-pub.semisol.devaddittoyourrel -wss://social.dev-wiki.de -wss://ostfrie.se -wss://darmstadt.social -wss://nostr.cx.ms -wss://alentours.cc -wss://nostr.kleofash.eu -wss://gib.social -wss://test23.hifish.org -wss://rapemeat.solutions -wss://filter.stealth.winebroadcasttrue -wss://nostr.montre -wss://grumble.social -wss://nostr.roli.social -wss://primarycare.app -wss://hodlr.rocks -wss://superlinks.me -wss://mastodon.lawprofs.org -wss://tictoc.social -wss://nostest.dojotunnel.online -wss://kafka.icu -wss://nostr.lanparty.one -wss://filter.nostr.wineglobaltrue -wss://social.b10m.net -wss://worm.pink -wss://nostr-test.cx.ms -wss://nostrverifired.com -wss://relay.nostrss.re -wss://eliitin-some.fi -wss://dju.social -wss://jeremy.hu -wss://stream.criminallycute.fi -wss://nostr.0x50.dev -wss://n.s.nyc -wss://n.8.s.nyc -wss://relay.rocks -wss://relay.fiatjaf.com -wss://n-lan.s.nyc -wss://sciences.social -wss://nostr.swiss-enigma.com -wss://quietplace.xyz -wss://universe.nostrich.landlangenlangzh -wss://5280.city -wss://feddit.de -wss://base.lc -wss://social.medusmedia.com -wss://umha4zl6xk62a4dous6e7tq4qlmt462hlzs2su33en6qrvtvs3hkjgid.onion -wss://etorneau.fr:28343 -wss://foggyminds.com -wss://neurodiversity-in.au -wss://nostr.hendrixson.net -wss://ramen-fsm.eu.org -wss://www.nostrical.com -wss://feedbeat.me -wss://relay.bsky.social -wss://babka.social -wss://mastodontech.de -wss://commiespace.duckdns.org -wss://openbiblio.social -wss://karkatdyinginagluetrap.com -wss://relay.fundr.vanderwarker.family -wss://hannover.town -wss://nostr.relay.info -wss://mastodon.tetaneutral.net -wss://cache2.primal.net -wss://nostr.petrkr.net -wss://ifwo.eu -wss://mastodong.lol -wss://venera.social -wss://wallets.fyoumoneypod.com -wss://nostr.relay-nokotaro.com -wss://social.exozy.me -wss://gqgjp2bun4opme6mepz3rrgprkw4xatb6h5ogayorqwi6sajsxcp5sad.local -wss://branle.netlify.app -wss://rsslay.fiat.jaf -wss://chrislace.damus.io -wss://relay.leafbodhi.com -wss://social.opendesktop.org -wss://relay.nostr.watch -wss://gearlandia.haus -wss://freiburg.social -wss://atlas.nostro.land -wss://nostr.io -wss://patrizio.tn.al -wss://chaintools.io -wss://kn.icu -wss://relaywithme.eu -wss://la-autopilot-this-end-up.dvm.email -wss://coinfinity.co -wss://assemblag.es -wss://qou7zzll2mxx2ehl73n6pptmhizl5b3entowljlin3sqhcvltxdtlmad.onion -wss://indigenouscreatives.social -wss://bookwyrm.social -wss://kokoro.shugetsu.space -wss://eden.nost.land -wss://misskey.gothloli.club -wss://sersleepy.com -wss://rsslay.nos.pink -wss://pettingzoo.co -wss://witches.live -wss://7craxnzfi42touzi23etut5qjzqro27sqcuottxj7opntcin4fstruad.onion -wss://mastodonsweden.se -wss://mv2k.com -wss://nostr.tchaicap.space -wss://relay.whoop.ph -wss://bitcoiner.nostr.social -wss://kinkyelephant.com -wss://www.superstork.org -wss://mastodon.iftas.org -wss://lea.pet -wss://zug.network -wss://homeserver.local:4848 -wss://frontrange.co -wss://sciencemastodon.com -wss://montereybay.social -wss://social.securecryptomining.com -wss://rssrelay.nostr.moe -wss://nostr.org -wss://nostr.tw -wss://nostr.hk -wss://wxw.moe -wss://mastodonmusic.social -wss://puntarella.party -wss://oyasumi.space -wss://drumstodon.net -wss://social.wikimedia.de -wss://iyasaretai.pw -wss://social.coletivos.org -wss://nostream.localtest.me -wss://ubuntu201.local:4848 -wss://masto.pt -wss://taiwan.riley-tech.net -wss://freeradical.zone -wss://blastrf7z.xyz -wss://onemorestop.photo -wss://frikiverse.zone -wss://toot.bldrweb.org -wss://electroverse.tech -wss://mstdn.games -wss://relay1.nostr.unitedfop.com -wss://gratefuldread.masto.host -wss://mk.gabe.rocks -wss://widerweb.org -wss://mastodon.eternalaugust.com -wss://lay.southeastasia.cloudapp.azure.com:445 -wss://me.ns.ci -wss://gnostr.th -wss://fritter.cn -wss://nex.cn -wss://nex.tw -wss://fritter.jp -wss://fritter.tw -wss://gnostr.cn -wss://mstdn.dk -wss://akkoma.simulacrum-emporium.eu -wss://dalliance.social -wss://toot.re -wss://avatastic.uk -wss://blastr20f7z.xyz -wss://the.voiceover.bar -wss://porcodon.net -wss://nostr.dncn.xyz -wss://nostr.dnxn.xyz -wss://relay.xmr.rocks -wss://d6egak3woofrixu26gr3utb5qezhkktavsuwlrfqaauu55lmpudxudqd.onion:5051 -wss://social.wuebbsy.com -wss://4kgwkcfzea2xhefsquktyxqyjf3rsxa7oo7hxbcs6k3xdpxznknydsqd.local -wss://4kgwkcfzea2xhefsquktyxqyjf3rsxa7oo7hxbcs6k3xdpxznknydsqd.onion -wss://tooters.org -wss://nostre.wine -wss://relay.xplive.local -wss://relay2.xplive.local -wss://4v5umvicfs6a7d3aiy67uu2ibttiuanl2cehfmv5qaorbojousbgkdad.onion -wss://pipou.academy -wss://opjk6jxrcyicuwhe62tqy6zwx776u7rfi6cqo6iodurjvege7piz5wqd.local -wss://mythology.social -wss://nostr.millou.lol -wss://ryogrid.net:7777 -wss://nost.debancariser.com -wss://fault.stsecurity.moe -wss://jan-optiplex-5040.local:4848 -wss://relay-jp.wirednet.jp -wss://mastodon.kitchen -wss://relay.mnethome.de -wss://verkehrswende.social -wss://nostr.gleeze.com -wss://dair-community.social -wss://shota.house -wss://kavlak.uk -wss://social.inex.rocks -wss://4v5umvicfs6a7d3aiy67uu2ibttiuanl2cehfmv5qaorbojousbgkdad.local -wss://startrekshitposting.com -wss://nostrfmar.ddns.net -wss://4yqp7gzuf15zfc3hpwhz3j5p2uarvdsnf75ovpdiqvyjdsmku771jfid.onion -wss://nostrgraph.net -wss://idolheaven.org -wss://mstdn.kemono-friends.info -wss://mastodon.bawue.social -wss://social.pmj.rocks -wss://ursal.zone -wss://nstr.milou.lol -wss://poweredbygay.social -wss://gochisou.photo -wss://lnb3.openchain.fr -wss://nostr.filmweb.pl -wss://nostr-word.h3z.jp -wss://blastr.f7z.xyzanotherinstanceofblastr -wss://shakedown.social -wss://nostr.bubu.hair -wss://hyper-nostr.inosta.cc -wss://ieji.de -wss://wawmartme.com -wss://nostr.kungfu-g.rip -wss://bozgor.org -wss://todon.nl -wss://nostpy.lol -wss://ostatus.taiyolab.com -wss://polsum.rocks -wss://freespeech.group -wss://im.allmendenetz.de -wss://shitpost.poridge.club -wss://twingyeo.kr -wss://social.platypush.tech -wss://rsslay-production-bc22.up.railway.app -wss://lonely.damus.io -wss://kirche.social -wss://cubalibre.social -wss://relay.txinito.xyz -wss://realy.orangepill.dev -wss://3zi.ru -wss://plebstr.com -wss://social.seattle.wa.us -wss://social.bim.land -wss://cubhub.social -wss://relay.nostr-x.com -wss://hispagatos.space -wss://node101.nostress.cc -wss://lsbt.me -wss://jgqaglhautb4k6e6i2g34jakxiemqp6z4wynlirltuukgkft2xuglmqd.onion -wss://nostdemo.dojotunnel.online -wss://f.cz \ No newline at end of file From 92f07c56150ea03e88b94cc46abccaf96d74ae75 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 7 Jan 2026 14:33:56 -0500 Subject: [PATCH 109/137] Remove deprecated field --- .../quartz/nip01Core/metadata/UserMetadata.kt | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/UserMetadata.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/UserMetadata.kt index bfbeb2436..2499c8379 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/UserMetadata.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/UserMetadata.kt @@ -30,9 +30,6 @@ import kotlinx.serialization.Serializable class UserMetadata { var name: String? = null - @Deprecated("Use name instead", replaceWith = ReplaceWith("name")) - var username: String? = null - @SerialName("display_name") var displayName: String? = null var picture: String? = null @@ -56,16 +53,16 @@ class UserMetadata { @kotlin.jvm.Transient var tags: ImmutableListOfLists? = null - fun anyName(): String? = displayName ?: name ?: username + fun anyName(): String? = displayName ?: name fun anyNameStartsWith(prefix: String): Boolean = - listOfNotNull(name, username, displayName, nip05, lud06, lud16).any { + listOfNotNull(name, displayName, nip05, lud06, lud16).any { it.contains(prefix, true) } fun lnAddress(): String? = lud16 ?: lud06 - fun bestName(): String? = displayName ?: name ?: username + fun bestName(): String? = displayName ?: name fun nip05(): String? = nip05 @@ -78,7 +75,6 @@ class UserMetadata { if (nip05?.isNotEmpty() == true) nip05 = nip05?.trim() if (displayName?.isNotEmpty() == true) displayName = displayName?.trim() if (name?.isNotEmpty() == true) name = name?.trim() - if (username?.isNotEmpty() == true) username = username?.trim() if (lud06?.isNotEmpty() == true) lud06 = lud06?.trim() if (lud16?.isNotEmpty() == true) lud16 = lud16?.trim() if (pronouns?.isNotEmpty() == true) pronouns = pronouns?.trim() @@ -91,7 +87,6 @@ class UserMetadata { if (nip05?.isBlank() == true) nip05 = null if (displayName?.isBlank() == true) displayName = null if (name?.isBlank() == true) name = null - if (username?.isBlank() == true) username = null if (lud06?.isBlank() == true) lud06 = null if (lud16?.isBlank() == true) lud16 = null From 139e9fd3efacec7081f58b03e46bf0113ff7aa39 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 7 Jan 2026 14:52:40 -0500 Subject: [PATCH 110/137] Re-adds name to the profile edit page. --- .../nip01UserMetadata/UserMetadataState.kt | 5 +- .../ui/actions/NewUserMetadataScreen.kt | 27 +++++++++-- .../ui/actions/NewUserMetadataViewModel.kt | 48 +++++++++---------- amethyst/src/main/res/values/strings.xml | 2 + 4 files changed, 52 insertions(+), 30 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt index d64ec40bd..503eb1c4c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt @@ -50,6 +50,7 @@ class UserMetadataState( suspend fun sendNewUserMetadata( name: String? = null, + displayName: String? = null, picture: String? = null, banner: String? = null, website: String? = null, @@ -69,7 +70,7 @@ class UserMetadataState( MetadataEvent.updateFromPast( latest = latest, name = name, - displayName = name, + displayName = displayName, picture = picture, banner = banner, website = website, @@ -85,7 +86,7 @@ class UserMetadataState( } else { MetadataEvent.createNew( name = name, - displayName = name, + displayName = displayName, picture = picture, banner = banner, website = website, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt index 0fa2fd845..14c841ad2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt @@ -70,11 +70,12 @@ fun NewUserMetadataScreen( SavingTopBar( titleRes = R.string.profile, onCancel = { - postViewModel.clear() nav.popBack() }, onPost = { - postViewModel.create() + accountViewModel.launchSigner { + postViewModel.create() + } nav.popBack() }, ) @@ -95,7 +96,27 @@ fun NewUserMetadataScreen( modifier = Modifier.padding(10.dp).verticalScroll(rememberScrollState()), ) { OutlinedTextField( - label = { Text(text = stringRes(R.string.profile_name)) }, + label = { Text(text = stringRes(R.string.profile_name_with_explainer)) }, + modifier = Modifier.fillMaxWidth(), + value = postViewModel.name.value, + onValueChange = { postViewModel.name.value = it }, + placeholder = { + Text( + text = stringRes(R.string.my_name), + color = MaterialTheme.colorScheme.placeholderText, + ) + }, + keyboardOptions = + KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.Sentences, + ), + singleLine = true, + ) + + Spacer(modifier = Modifier.height(10.dp)) + + OutlinedTextField( + label = { Text(text = stringRes(R.string.display_name)) }, modifier = Modifier.fillMaxWidth(), value = postViewModel.displayName.value, onValueChange = { postViewModel.displayName.value = it }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt index 139bf9587..32dddea67 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt @@ -47,7 +47,7 @@ class NewUserMetadataViewModel : ViewModel() { private lateinit var accountViewModel: AccountViewModel private lateinit var account: Account - // val userName = mutableStateOf("") + val name = mutableStateOf("") val displayName = mutableStateOf("") val about = mutableStateOf("") @@ -74,8 +74,8 @@ class NewUserMetadataViewModel : ViewModel() { fun load() { account.userProfile().let { - // userName.value = it.bestUsername() ?: "" - displayName.value = it.info?.bestName() ?: "" + name.value = it.info?.name ?: "" + displayName.value = it.info?.displayName ?: "" about.value = it.info?.about ?: "" picture.value = it.info?.picture ?: "" banner.value = it.info?.banner ?: "" @@ -100,33 +100,31 @@ class NewUserMetadataViewModel : ViewModel() { } } - fun create() { - // Tries to not delete any existing attribute that we do not work with. - accountViewModel.launchSigner { - val metadata = - account.userMetadata.sendNewUserMetadata( - name = displayName.value, - picture = picture.value, - banner = banner.value, - website = website.value, - pronouns = pronouns.value, - about = about.value, - nip05 = nip05.value, - lnAddress = lnAddress.value, - lnURL = lnURL.value, - twitter = twitter.value, - mastodon = mastodon.value, - github = github.value, - ) + suspend fun create() { + val metadata = + account.userMetadata.sendNewUserMetadata( + name = name.value, + displayName = displayName.value, + picture = picture.value, + banner = banner.value, + website = website.value, + pronouns = pronouns.value, + about = about.value, + nip05 = nip05.value, + lnAddress = lnAddress.value, + lnURL = lnURL.value, + twitter = twitter.value, + mastodon = mastodon.value, + github = github.value, + ) - account.sendLiterallyEverywhere(metadata) + account.sendLiterallyEverywhere(metadata) - clear() - } + clear() } fun clear() { - // userName.value = "" + name.value = "" displayName.value = "" about.value = "" picture.value = "" diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 212407e46..d033e2736 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -147,6 +147,8 @@ Add a User Add a Relay Name + Name (for @tagging) + My @tag name Display Name My display name Ostrich McAwesome From 6b8036eac47f22c1d3bdab0efe3bd140b375e459 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 7 Jan 2026 15:33:16 -0500 Subject: [PATCH 111/137] Forcing notify relay to be either the NIP-65 or DM relays --- .../notifyCommand/compose/DisplayNotifyMessages.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/compose/DisplayNotifyMessages.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/compose/DisplayNotifyMessages.kt index dbd28b2dd..c455d77e0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/compose/DisplayNotifyMessages.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/compose/DisplayNotifyMessages.kt @@ -47,7 +47,10 @@ fun DisplayNotifyMessages( val flow = remember(accountViewModel) { requests.transientPaymentRequests.map { - it.filter { it.relayUrl in accountViewModel.account.trustedRelays.flow.value } + it.filter { notifyMsg -> + notifyMsg.relayUrl in accountViewModel.account.dmRelayList.flow.value || + notifyMsg.relayUrl in accountViewModel.account.nip65RelayList.allFlowNoDefaults.value + } } } From b5a9b346c62b121170c8735132b4e6f82fa100a5 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Wed, 7 Jan 2026 20:35:11 +0000 Subject: [PATCH 112/137] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-fr-rFR/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/amethyst/src/main/res/values-fr-rFR/strings.xml b/amethyst/src/main/res/values-fr-rFR/strings.xml index a863d2a1d..c680de793 100644 --- a/amethyst/src/main/res/values-fr-rFR/strings.xml +++ b/amethyst/src/main/res/values-fr-rFR/strings.xml @@ -15,6 +15,7 @@ Impossible de déchiffrer le message Image de groupe Contenu choquant + Annonce du serveur Spam Le nombre d\'événements spam provenant de ce relais Falsification d\'identité From cba4d254241eb70383c875b53f3ef485492a9ed0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 7 Jan 2026 16:16:01 -0500 Subject: [PATCH 113/137] Moves follows and publish to their own packages --- .../commons/{actions => model/nip02FollowList}/FollowAction.kt | 2 +- .../commons/{actions => model/nip10TextNotes}/PublishAction.kt | 2 +- .../com/vitorpamplona/amethyst/commons/state/FollowState.kt | 2 +- .../com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt | 2 +- .../com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/{actions => model/nip02FollowList}/FollowAction.kt (98%) rename commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/{actions => model/nip10TextNotes}/PublishAction.kt (98%) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/FollowAction.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip02FollowList/FollowAction.kt similarity index 98% rename from commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/FollowAction.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip02FollowList/FollowAction.kt index 1be5372b5..84243a592 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/FollowAction.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip02FollowList/FollowAction.kt @@ -18,7 +18,7 @@ * 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.actions +package com.vitorpamplona.amethyst.commons.model.nip02FollowList import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/PublishAction.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip10TextNotes/PublishAction.kt similarity index 98% rename from commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/PublishAction.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip10TextNotes/PublishAction.kt index 37814d8a4..8ab3ce094 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/PublishAction.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip10TextNotes/PublishAction.kt @@ -18,7 +18,7 @@ * 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.actions +package com.vitorpamplona.amethyst.commons.model.nip10TextNotes import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/state/FollowState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/state/FollowState.kt index 9996f94ac..218e1454b 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/state/FollowState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/state/FollowState.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.commons.state import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.commons.actions.FollowAction +import com.vitorpamplona.amethyst.commons.model.nip02FollowList.FollowAction import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt index a532439fa..aca7e6664 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt @@ -44,7 +44,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import com.vitorpamplona.amethyst.commons.account.AccountState -import com.vitorpamplona.amethyst.commons.actions.PublishAction +import com.vitorpamplona.amethyst.commons.model.nip10TextNotes.PublishAction import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt index c3dc70722..475fe55b1 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt @@ -57,7 +57,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.account.AccountState -import com.vitorpamplona.amethyst.commons.actions.FollowAction +import com.vitorpamplona.amethyst.commons.model.nip02FollowList.FollowAction import com.vitorpamplona.amethyst.commons.state.EventCollectionState import com.vitorpamplona.amethyst.commons.state.FollowState import com.vitorpamplona.amethyst.commons.subscriptions.createContactListSubscription From 882cf6ee59ee868a0a182c26ca4ed09cec3863dc Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 7 Jan 2026 16:34:15 -0500 Subject: [PATCH 114/137] Moving OpenGraph parser to commons --- .../amethyst/service/previews/HtmlParser.kt | 49 +++++----------- .../amethyst/service/previews/UrlPreview.kt | 2 + .../amethyst/ui/components/UrlPreviewCard.kt | 2 +- .../amethyst/ui/components/UrlPreviewState.kt | 2 +- .../markdown/RenderContentAsMarkdown.kt | 2 +- .../commons/preview/HtmlCharsetParser.kt | 56 +++++++++++++++++++ .../commons/preview}/OpenGraphParser.kt | 4 +- .../amethyst/commons/preview}/UrlInfoItem.kt | 4 +- 8 files changed, 77 insertions(+), 44 deletions(-) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/HtmlCharsetParser.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview}/OpenGraphParser.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview}/UrlInfoItem.kt (93%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/HtmlParser.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/HtmlParser.kt index e76b66440..3cc0b10e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/HtmlParser.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/HtmlParser.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.service.previews +import com.vitorpamplona.amethyst.commons.preview.HtmlCharsetParser import com.vitorpamplona.amethyst.commons.preview.MetaTag import com.vitorpamplona.amethyst.commons.preview.MetaTagsParser import kotlinx.coroutines.Dispatchers @@ -31,10 +32,6 @@ import java.nio.charset.Charset class HtmlParser { companion object { - val ATTRIBUTE_VALUE_CHARSET = "charset" - val ATTRIBUTE_VALUE_HTTP_EQUIV = "http-equiv" - val CONTENT = "content" - // taken from okhttp private val UNICODE_BOMS = Options.of( @@ -49,25 +46,30 @@ class HtmlParser { // UTF-32LE "ffff0000".decodeHex(), ) - - private val RE_CONTENT_TYPE_CHARSET = Regex("""charset=([^;]+)""") } suspend fun parseHtml( source: BufferedSource, type: Charset?, + ): Sequence = + parseHtml( + source.readByteArray(), + type ?: source.readBomAsCharset(), + ) + + suspend fun parseHtml( + bodyBytes: ByteArray, + type: Charset?, ): Sequence = withContext(Dispatchers.IO) { // sniff charset from Content-Type header or BOM - val sniffedCharset = type ?: source.readBomAsCharset() - if (sniffedCharset != null) { - val content = source.readByteArray().toString(sniffedCharset) + if (type != null) { + val content = bodyBytes.toString(type) return@withContext MetaTagsParser.parse(content) } // if sniffing was failed, detect charset from content - val bodyBytes = source.readByteArray() - val charset = detectCharset(bodyBytes) + val charset = HtmlCharsetParser.detectCharset(bodyBytes) val content = bodyBytes.toString(charset) return@withContext MetaTagsParser.parse(content) } @@ -82,29 +84,4 @@ class HtmlParser { -1 -> null else -> throw AssertionError() } - - private fun detectCharset(bodyBytes: ByteArray): Charset { - // try to detect charset from meta tags parsed from first 1024 bytes of body - val firstPart = String(bodyBytes, 0, 1024, Charset.forName("utf-8")) - val metaTags = MetaTagsParser.parse(firstPart) - metaTags.forEach { meta -> - val charsetAttr = meta.attr(ATTRIBUTE_VALUE_CHARSET) - if (charsetAttr.isNotEmpty()) { - runCatching { Charset.forName(charsetAttr) }.getOrNull()?.let { - return it - } - } - if (meta.attr(ATTRIBUTE_VALUE_HTTP_EQUIV).lowercase() == "content-type") { - RE_CONTENT_TYPE_CHARSET - .find(meta.attr(CONTENT)) - ?.let { - runCatching { Charset.forName(it.groupValues[1]) }.getOrNull() - }?.let { - return it - } - } - } - // defaults to UTF-8 - return Charset.forName("utf-8") - } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreview.kt index c7ea3eed4..7eb4e9f75 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreview.kt @@ -20,6 +20,8 @@ */ package com.vitorpamplona.amethyst.service.previews +import com.vitorpamplona.amethyst.commons.preview.OpenGraphParser +import com.vitorpamplona.amethyst.commons.preview.UrlInfoItem import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewCard.kt index a006bd5ab..49a144591 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewCard.kt @@ -40,7 +40,7 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextOverflow import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.service.previews.UrlInfoItem +import com.vitorpamplona.amethyst.commons.preview.UrlInfoItem import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.MaxWidthWithHorzPadding diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewState.kt index f8784de78..56608fb9f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewState.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.components import androidx.compose.runtime.Immutable -import com.vitorpamplona.amethyst.service.previews.UrlInfoItem +import com.vitorpamplona.amethyst.commons.preview.UrlInfoItem @Immutable sealed class UrlPreviewState { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt index 223431553..9f20f4687 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt @@ -39,9 +39,9 @@ import com.halilibo.richtext.commonmark.CommonMarkdownParseOptions import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser import com.halilibo.richtext.markdown.BasicMarkdown import com.halilibo.richtext.ui.material3.RichText +import com.vitorpamplona.amethyst.commons.preview.UrlInfoItem import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.UrlCachedPreviewer -import com.vitorpamplona.amethyst.service.previews.UrlInfoItem import com.vitorpamplona.amethyst.ui.components.UrlPreviewState import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/HtmlCharsetParser.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/HtmlCharsetParser.kt new file mode 100644 index 000000000..84f16502c --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/HtmlCharsetParser.kt @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.preview + +import java.nio.charset.Charset + +object HtmlCharsetParser { + val ATTRIBUTE_VALUE_CHARSET = "charset" + val ATTRIBUTE_VALUE_HTTP_EQUIV = "http-equiv" + val CONTENT = "content" + + private val RE_CONTENT_TYPE_CHARSET = Regex("""charset=([^;]+)""") + + fun detectCharset(bodyBytes: ByteArray): Charset { + // try to detect charset from meta tags parsed from first 1024 bytes of body + val firstPart = String(bodyBytes, 0, 1024, Charset.forName("utf-8")) + val metaTags = MetaTagsParser.parse(firstPart) + metaTags.forEach { meta -> + val charsetAttr = meta.attr(ATTRIBUTE_VALUE_CHARSET) + if (charsetAttr.isNotEmpty()) { + runCatching { Charset.forName(charsetAttr) }.getOrNull()?.let { + return it + } + } + if (meta.attr(ATTRIBUTE_VALUE_HTTP_EQUIV).lowercase() == "content-type") { + RE_CONTENT_TYPE_CHARSET + .find(meta.attr(CONTENT)) + ?.let { + runCatching { Charset.forName(it.groupValues[1]) }.getOrNull() + }?.let { + return it + } + } + } + // defaults to UTF-8 + return Charset.forName("utf-8") + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/OpenGraphParser.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/OpenGraphParser.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/OpenGraphParser.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/OpenGraphParser.kt index cec53b311..6543b3d08 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/OpenGraphParser.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/OpenGraphParser.kt @@ -18,9 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.service.previews - -import com.vitorpamplona.amethyst.commons.preview.MetaTag +package com.vitorpamplona.amethyst.commons.preview class OpenGraphParser { class Result( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlInfoItem.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlInfoItem.kt similarity index 93% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlInfoItem.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlInfoItem.kt index e2f483482..7b28bda05 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlInfoItem.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlInfoItem.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.service.previews +package com.vitorpamplona.amethyst.commons.preview import androidx.compose.runtime.Immutable import java.net.URL @@ -31,7 +31,7 @@ class UrlInfoItem( val image: String = "", val mimeType: String, ) { - val verifiedUrl = kotlin.runCatching { URL(url) }.getOrNull() + val verifiedUrl = runCatching { URL(url) }.getOrNull() val imageUrlFullPath = if (image.startsWith("/")) { URL(verifiedUrl, image).toString() From d5ba3ffc0451b092ea8036eb2e02254b73c52533 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 7 Jan 2026 16:38:51 -0500 Subject: [PATCH 115/137] Simplifies NIP-05 processing --- .../amethyst/service/Nip05NostrAddressVerifier.kt | 9 +-------- .../amethyst/ui/screen/AccountStateViewModel.kt | 2 +- .../amethyst/ui/screen/loggedIn/AccountViewModel.kt | 2 +- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt index 35fb7b90d..5ad6597e8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt @@ -28,15 +28,13 @@ import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.coroutines.executeAsync -class Nip05NostrAddressVerifier { +object Nip05NostrAddressVerifier { suspend fun fetchNip05Json( nip05: String, okHttpClient: (String) -> OkHttpClient, onSuccess: suspend (String) -> Unit, onError: (String) -> Unit, ) = withContext(Dispatchers.IO) { - checkNotInMainThread() - val url = Nip05().assembleUrl(nip05) if (url == null) { @@ -73,15 +71,10 @@ class Nip05NostrAddressVerifier { onSuccess: suspend (String) -> Unit, onError: (String) -> Unit, ) { - // check fails on tests - checkNotInMainThread() - fetchNip05Json( nip05, okHttpClient, onSuccess = { - checkNotInMainThread() - Nip05().parseHexKeyFor(nip05, it.lowercase()).fold( onSuccess = { hexKey -> if (hexKey == null) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index c96851b95..c969c26e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -214,7 +214,7 @@ class AccountStateViewModel : ViewModel() { loginSync(newKey, transientAccount, loginWithExternalSigner, packageName, onError) } } else if (EMAIL_PATTERN.matcher(key).matches()) { - Nip05NostrAddressVerifier().verifyNip05( + Nip05NostrAddressVerifier.verifyNip05( key, okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(false) }, onSuccess = { publicKey -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 006e2cf70..9c983e475 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -941,7 +941,7 @@ class AccountViewModel( val nip05 = userMetadata.nip05?.ifBlank { null } ?: return viewModelScope.launch(Dispatchers.IO) { - Nip05NostrAddressVerifier() + Nip05NostrAddressVerifier .verifyNip05( nip05, okHttpClient = httpClientBuilder::okHttpClientForNip05, From dd259bab333c113458d12b48e5bcd069feca296a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 8 Jan 2026 10:00:42 -0500 Subject: [PATCH 116/137] Separates currentFilters from lastSeenFilters to correctly close subscriptions with relays when ending them on NostrClient. --- .../relay/client/pool/PoolRequests.kt | 201 +++++++++++------- .../relay/client/reqs/IRequestListener.kt | 2 + .../client/reqs/RelayActiveRequestStates.kt | 2 +- .../client/reqs/RequestSubscriptionState.kt | 36 +++- .../subscriptions/SubscriptionController.kt | 2 +- 5 files changed, 153 insertions(+), 90 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolRequests.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolRequests.kt index 4847a747c..ac66e0731 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolRequests.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolRequests.kt @@ -36,21 +36,39 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.utils.cache.LargeCache import kotlinx.coroutines.flow.MutableStateFlow +/** + * Manages relay subscriptions for the entire pool in a way that only + * sends new states to the relay if they have changed. + * + * This code also awaits a subscription to come to EOSE since many relays + * have through switching subs while they are processing the past. + */ class PoolRequests { /** - * Desired subs and listeners are changed immediately when the local code requests + * Desired subs and listeners + * + * These are changed immediately when the local code requests and should map + * to what the app whants to do. */ private val desiredSubs = LargeCache>>() private val desiredSubListeners = LargeCache() val desiredRelays = MutableStateFlow(setOf()) /** - * relay states are kept and only removed after everything is processed. + * Relay states map what we think the relay is doing + * + * This is important to link responses with which filter was a sub replying to and + * to figure out if we need to update the relay if a REQ has changed. */ private val relayState = LargeCache>() fun subState(subId: String): RequestSubscriptionState = relayState.getOrCreate(subId) { RequestSubscriptionState() } + /** + * This is called when a sub is added or removed from this class and + * should update the desired relay list to get the pool to connect + * to new ones or disconnect to old ones if they are not needed anymore + */ private fun updateRelays() { val myRelays = mutableSetOf() desiredSubs.forEach { sub, perRelayFilters -> @@ -62,19 +80,23 @@ class PoolRequests { } } + /** + * Returns all the active filters for a relay, per subscription + */ fun activeFiltersFor(url: NormalizedRelayUrl): Map> { val myRelays = mutableMapOf>() desiredSubs.forEach { sub, perRelayFilters -> val filters = perRelayFilters[url] if (filters != null) { - myRelays.put(sub, filters) + myRelays[sub] = filters } } return myRelays } /** - * Adds a new filter to the pool, and returns the relays that need to be updated. + * Adds a new subscription to the pool, and returns which relays + * MIGHT need to be updated. */ fun addOrUpdate( subId: String, @@ -98,7 +120,8 @@ class PoolRequests { } /** - * Removes the sub from the pool, and returns the relays that need to be updated. + * Removes the sub from the pool, and returns which relays + * MIGHT need to be updated. */ fun remove(subId: String): Set = if (desiredSubs.containsKey(subId)) { @@ -108,8 +131,6 @@ class PoolRequests { desiredSubs.remove(subId) // update listener desiredSubListeners.remove(subId) - // remove states - relayState.remove(subId) // update relays for pool updateRelays() // return all affected relays @@ -123,12 +144,102 @@ class PoolRequests { // -------------------------- // State management functions // -------------------------- + + /** + * When a connecting, updates the state of all subs + */ fun onConnecting(url: NormalizedRelayUrl) { + // Change states to connecting. relayState.forEach { subId, state -> state.connecting(url) } } + /** + * When a new command is sent to this relay, updates the state + */ + fun onSent( + relay: NormalizedRelayUrl, + cmd: Command, + ) { + when (cmd) { + is ReqCmd -> { + subState(cmd.subId).onOpenReq(relay, cmd.filters) + desiredSubListeners.get(cmd.subId)?.onStartReq( + relay = relay.url, + forFilters = cmd.filters, + ) + } + is CloseCmd -> { + subState(cmd.subId).onCloseReq(relay) + desiredSubListeners.get(cmd.subId)?.onCloseReq( + relay = relay.url, + ) + } + } + } + + /** + * When a new message is received by the relay, updates the sub + */ + fun onIncomingMessage( + relay: IRelayClient, + msg: Message, + ) { + when (msg) { + is EventMessage -> { + val state = relayState.get(msg.subId) + state?.onNewEvent(relay.url) + desiredSubListeners.get(msg.subId)?.onEvent( + event = msg.event, + isLive = state?.currentState(relay.url) == ReqSubStatus.LIVE, + relay = relay.url, + forFilters = state?.lastKnownFilterStates(relay.url), + ) + } + is EoseMessage -> { + val state = relayState.get(msg.subId) + state?.onEose(relay.url) + desiredSubListeners.get(msg.subId)?.onEose( + relay = relay.url, + forFilters = state?.lastKnownFilterStates(relay.url), + ) + + // send a newer version when done + sendToRelayIfChanged(msg.subId, relay.url) { cmd -> + relay.sendOrConnectAndSync(cmd) + } + } + is ClosedMessage -> { + val state = relayState.get(msg.subId) + state?.onClosed(relay.url) + + desiredSubListeners.get(msg.subId)?.onClosed( + message = msg.message, + relay = relay.url, + forFilters = state?.lastKnownFilterStates(relay.url), + ) + + // send a newer version when done + sendToRelayIfChanged(msg.subId, relay.url) { cmd -> + // don't send a close if just closed + if (cmd !is CloseCmd) { + relay.sendOrConnectAndSync(cmd) + } + } + } + } + } + + /** + * When the relay disconnects + */ + fun onDisconnected(url: NormalizedRelayUrl) { + relayState.forEach { subId, state -> + state.disconnected(url) + } + } + fun syncState( relay: NormalizedRelayUrl, sync: (Command) -> Unit, @@ -143,77 +254,6 @@ class PoolRequests { } } - fun onSent( - relay: NormalizedRelayUrl, - cmd: Command, - ) { - when (cmd) { - is ReqCmd -> { - subState(cmd.subId).onOpenReq(relay, cmd.filters) - desiredSubListeners.get(cmd.subId)?.onStartReq( - relay = relay.url, - forFilters = cmd.filters, - ) - } - is CloseCmd -> subState(cmd.subId).onCloseReq(relay) - } - } - - fun onIncomingMessage( - relay: IRelayClient, - msg: Message, - ) { - when (msg) { - is EventMessage -> { - val state = relayState.get(msg.subId) - state?.onNewEvent(relay.url) - desiredSubListeners.get(msg.subId)?.onEvent( - event = msg.event, - isLive = state?.currentState(relay.url) == ReqSubStatus.LIVE, - relay = relay.url, - forFilters = state?.currentFilters(relay.url), - ) - } - is EoseMessage -> { - val state = relayState.get(msg.subId) - state?.onEose(relay.url) - desiredSubListeners.get(msg.subId)?.onEose( - relay = relay.url, - forFilters = state?.currentFilters(relay.url), - ) - - // send a newer version when done - sendToRelayIfChanged(msg.subId, relay.url) { cmd -> - relay.sendOrConnectAndSync(cmd) - } - } - is ClosedMessage -> { - val state = relayState.get(msg.subId) - state?.onClosed(relay.url) - - desiredSubListeners.get(msg.subId)?.onClosed( - message = msg.message, - relay = relay.url, - forFilters = state?.currentFilters(relay.url), - ) - - // send a newer version when done - sendToRelayIfChanged(msg.subId, relay.url) { cmd -> - // don't send a close if just closed - if (cmd !is CloseCmd) { - relay.sendOrConnectAndSync(cmd) - } - } - } - } - } - - fun onDisconnected(url: NormalizedRelayUrl) { - relayState.forEach { subId, state -> - state.disconnected(url) - } - } - /** * If cannot connect, closes subs */ @@ -225,7 +265,7 @@ class PoolRequests { desiredSubListeners.get(subId)?.onCannotConnect( message = errorMessage, relay = url, - forFilters = state.currentFilters(url), + forFilters = state.lastKnownFilterStates(url), ) } } @@ -258,7 +298,8 @@ class PoolRequests { relay: NormalizedRelayUrl, sync: (Command) -> Unit, ) { - val oldFilters = relayState.get(subId)?.currentFilters(relay) + val state = relayState.get(subId) + val oldFilters = state?.currentFilters(relay) val newFilters = desiredSubs.get(subId)?.get(relay) sendToRelayIfChanged(subId, oldFilters, newFilters, sync) } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/IRequestListener.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/IRequestListener.kt index 57f114c3e..64d321fc3 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/IRequestListener.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/IRequestListener.kt @@ -53,4 +53,6 @@ interface IRequestListener { relay: String, forFilters: List, ) {} + + fun onCloseReq(relay: String) {} } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/RelayActiveRequestStates.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/RelayActiveRequestStates.kt index b27dc1efd..7ddb3a2db 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/RelayActiveRequestStates.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/RelayActiveRequestStates.kt @@ -43,7 +43,7 @@ class RelayActiveRequestStates( private val clientListener = object : IRelayClientListener { override fun onConnecting(relay: IRelayClient) { - subStates.put(relay.url, RequestSubscriptionState()) + subStates[relay.url] = RequestSubscriptionState() } override fun onIncomingMessage( diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/RequestSubscriptionState.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/RequestSubscriptionState.kt index f2c7b253b..390265219 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/RequestSubscriptionState.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/RequestSubscriptionState.kt @@ -22,6 +22,10 @@ package com.vitorpamplona.quartz.nip01Core.relay.client.reqs import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +/** + * Manages the State of Subscriptions by logging states as the + * subscription progresses. + */ class RequestSubscriptionState { // Logs the state of each channel to: // 1. inform when an event is received as live @@ -32,36 +36,53 @@ class RequestSubscriptionState { private val subStates = mutableMapOf() private val filterStates = mutableMapOf>() + /** + * This cache is used to make sure we know what the relay was processing + * before a close or disconnect so that if new events still arrive + * we can link them with the appropriate filters. + */ + private val lastKnownFilterStates = mutableMapOf>() + fun currentFilters() = filterStates fun currentFilters(reference: T) = filterStates[reference] + fun lastKnownFilterStates(reference: T) = lastKnownFilterStates[reference] + fun currentState(reference: T) = subStates[reference] fun onNewEvent(reference: T) { if (subStates[reference] == ReqSubStatus.SENT) { - subStates.put(reference, ReqSubStatus.QUERYING_PAST) + subStates[reference] = ReqSubStatus.QUERYING_PAST } } fun onEose(reference: T) { - subStates.put(reference, ReqSubStatus.LIVE) + subStates[reference] = ReqSubStatus.LIVE } fun onClosed(reference: T) { - subStates.put(reference, ReqSubStatus.CLOSED) + subStates[reference] = ReqSubStatus.CLOSED + // Closed messages are usually relays refusing to process a REQ + // This message keeps the state of filterStates intact to + // avoid sending the same filter, and getting immediately closed, + // over and over again. + + // filterStates.remove(reference) } fun onOpenReq( reference: T, filters: List, ) { - subStates.put(reference, ReqSubStatus.SENT) - filterStates.put(reference, filters) + subStates[reference] = ReqSubStatus.SENT + filterStates[reference] = filters + lastKnownFilterStates[reference] = filters } fun onCloseReq(reference: T) { - subStates.put(reference, ReqSubStatus.CLOSED) + subStates[reference] = ReqSubStatus.CLOSED + filterStates.remove(reference) } fun connecting(reference: T) { @@ -70,8 +91,7 @@ class RequestSubscriptionState { } fun disconnected(reference: T) { - // Can't remove filterStates because disconnections happen - // before processing all the events in the channel queue. subStates.remove(reference) + filterStates.remove(reference) } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/subscriptions/SubscriptionController.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/subscriptions/SubscriptionController.kt index c87bb5158..58930f47f 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/subscriptions/SubscriptionController.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/subscriptions/SubscriptionController.kt @@ -59,9 +59,9 @@ class SubscriptionController( fun dismissSubscription(subId: String) = getSub(subId)?.let { dismissSubscription(it) } fun dismissSubscription(subscription: Subscription) { - client.close(subscription.id) subscription.reset() subscriptions.remove(subscription.id) + client.close(subscription.id) } fun updateRelays() { From a9728910596552364f88b7336187f246495f85d1 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Thu, 8 Jan 2026 15:03:34 +0000 Subject: [PATCH 117/137] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-fr-rFR/strings.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/amethyst/src/main/res/values-fr-rFR/strings.xml b/amethyst/src/main/res/values-fr-rFR/strings.xml index c680de793..bbdb00c1e 100644 --- a/amethyst/src/main/res/values-fr-rFR/strings.xml +++ b/amethyst/src/main/res/values-fr-rFR/strings.xml @@ -16,12 +16,14 @@ Image de groupe Contenu choquant Annonce du serveur + Message en double Spam Le nombre d\'événements spam provenant de ce relais Falsification d\'identité Comportement illégal Autre Harcèlement + Violence inconnu Icône de relais Auteur inconnu @@ -101,10 +103,12 @@ Ajouter au message "Erreur d'analyse de l'aperçu pour %1$s : %2$s" "Aperçu de l'image pour %1$s" + Article Nouveau canal Nom de canal Mon groupe spectaculaire Url de l\'image + Url de l\'image (Optionnel) Description Description introuvable "à propos…" @@ -119,6 +123,7 @@ Adresse du relais Publications Octets + Erreur Erreurs Le nombre d\'erreurs de connexion dans cette session Vos notifications @@ -156,7 +161,15 @@ Enregistrer un message Enregistrer un message Cliquez et maintenez pour enregistrer un message + Ré-enregistrer + Enregistrer + Enregistrement %1$s Chargement… + Erreur d\'envoi + Échec de l\'envoi du message vocal + Erreur lors de l\'envoi + NIP-95 n\'est pas encore supporté pour les messages vocaux + Échec de l\'envoi de la voix : %1$s L\'utilisateur n\'a pas configuré d\'adresse Lightning pour recevoir des sats "répondre ici" Copie l\'ID de note dans le presse-papiers pour le partage sur Nostr @@ -350,6 +363,7 @@ Bloquer Division manuelle de zaps Favoris + Signets par défaut Brouillons Favoris Privés Favoris Publics From f2bda2e5ecd91481670dda72fe43b36b9a1a82e0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 8 Jan 2026 15:23:11 -0500 Subject: [PATCH 118/137] No need to set items directly. This happens later. --- .../amethyst/service/playback/playerPool/MediaSessionPool.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaSessionPool.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaSessionPool.kt index 3ec0bd726..e5db2d6e0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaSessionPool.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaSessionPool.kt @@ -197,8 +197,6 @@ class MediaSessionPool( controller: MediaSession.ControllerInfo, mediaItems: List, ): ListenableFuture> { - mediaSession.player.setMediaItems(mediaItems) - // set up return call when clicking on the Notification bar mediaItems.firstOrNull()?.mediaMetadata?.extras?.getString("callbackUri")?.let { mediaSession.setSessionActivity( From bb019181209083d633b241e97c80cbd1e70e0b73 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 8 Jan 2026 16:21:30 -0500 Subject: [PATCH 119/137] v1.05.0 --- CHANGELOG.md | 130 ++++++++++++++++++++++++++++++++++++++++ README.md | 10 ++-- amethyst/build.gradle | 6 +- quartz/build.gradle.kts | 2 +- 4 files changed, 139 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b54d77de..4c7cfd601 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,133 @@ + +# [Release v1.05.0: Bookmark Lists and WoT Scores](https://github.com/vitorpamplona/amethyst/releases/tag/v1.05.0) - 2025-01-08 + +#Amethyst v1.05.0: Bookmark Lists, Voice Notes, and WoT Scores + +This release introduces Bookmark List management, a complete overhaul of Voice Notes/YakBaks, +and the debut of Web of Trust (WoT) scores for a safer social experience. + +This version adds support for creating, managing, deleting, and viewing multiple bookmark lists, +which include both public and private members. You will find an improved "Bookmarks" menu option in +the sidebar and extra bookmark options in the context menu of each post, allowing you to add posts +directly to one or more individual lists. + +The Voice Notes UI has been redesigned to allow recording directly within the new Post Screen and a +dedicated Voice Reply screen. Users can record a new voice message, preview it with waveform +visualization, re-record if needed, select a media server, and post the reply. You now have full control. + +Amethyst now supports Trusted Assertions. By connecting to a WoT provider, you can see trust scores +and verified follower counts directly on user pictures. This helps filter signal from noise, identifying +reputable accounts to follow, which DMs to open, and which notifications to prioritize. To activate +this, you will need to find a provider capable of computing these scores. While providers are +currently limited and resource-constrained, we hope more will bring their own algorithms to Nostr over time. + +Quartz received a significantly improved database engine capable of sub-microsecond queries using Android's +default SQLite database. The engine is optimized for mobile environments, using as little memory as +possible to avoid impacting other apps. + +In the background, we have begun building Amethyst Desktop. While much work remains, the goal is a +standalone, mouse-first application that moves away from mobile-centric UI layouts. + +New Features +- Trusted Assertions: Added support for trust scores displayed on user profile pictures +- WoT Followers: Displays verified follower counts in user profiles +- Bookmark Lists: Full support for custom lists by @npub1a3tx8wcrt789skl6gg7rqwj4wey0j53eesr4z6asd4h4jwrd62jq0wkq4k +- Relay Information: New UI with expanded NIP-11 feature support +- Voice Notes & Replies: Redesigned experience by @npub1e2yuky03caw4ke3zy68lg0fz3r4gkt94hx4fjmlelacyljgyk79svn3eef +- Profile Banner: New default banner by @npub1tx5ccpregnm9afq0xaj42hh93xl4qd3lfa7u74v5cdvyhwcnlanqplhd8g +- Native Links: Intercept njump, yakihonne, primal, iris.to, zap.stream, and shosho.live to open directly on Amethyst by @npub1lu4l4wh7482u8vq0u63g2yuj8yqjdq8ne06r432n92rnd22lspcq0cxa32 + +Improvements: +- New in-memory graph-based cache scheme; moved reports and WoT scores to this new system +- Disabled top bar reappearance to prevent feed shifting when navigating between pages +- Lenient Kotlin Serialization to prevent crashes from malformed JSON; +- Removed expired addressable events from cache +- Moves reports from the old caching system to the new Graph-based one. +- Reverted to a 500-post load limit for Profile screens to handle high-reply accounts +- Moved the QR Code screen from a Dialog to a full Route. +- Re-adds name as a tagging name to the profile edit page. + +Performance: +- Faster event id checker by serializing, sha256 hashing, and ID comparison without creating any intermediary buffers. +- Faster event JSON parsers by avoiding new variables and thus garbage collection calls +- Faster tag array Deserializer +- Manages the pool state without having to loop through relays, saving some milliseconds of processing. +- Adds a cache system for WoT scores +- Improved Compose stability for video UI + +BugFixes: +- Fixes JSON serialization of UTF-8 Emoji surrogates for compatibility with standard Nostr implementations +- Improves error message on zap configuration errors with detailed NWC URI by @npub1e2yuky03caw4ke3zy68lg0fz3r4gkt94hx4fjmlelacyljgyk79svn3eef +- Centers QR dialog content and reduce excessive top spacing by @npub1qqqqqqz7nhdqz3uuwmzlflxt46lyu7zkuqhcapddhgz66c4ddynswreecw +- Closes subscriptions when ending them on NostrClient instead of waiting for them to finish +- Requires a relay to be an outbox/inbox relay to be able to NOTIFY a user of a payment +- Improves the speed of parsing of invalid kinds inside an address string +- Fixes count not working for LIMIT queries in the DB +- Fixes icon bug with incorrect resource id by @npub1e2yuky03caw4ke3zy68lg0fz3r4gkt94hx4fjmlelacyljgyk79svn3eef +- Fixes missing updates to the feed when the top list is not yet available locally +- Fixes List of supported NIPs as Integers on NIP-11 by @npub1e2yuky03caw4ke3zy68lg0fz3r4gkt94hx4fjmlelacyljgyk79svn3eef +- Fixes ConcurrentExceptions on event outboxes + +Desktop: +- Base Compose Multiplatform Desktop App with posts and global/following feeds by @npub12cfje6nl2nuxplcqfvhg7ljt89fmpj0n0fd24zxsukja5qm9wmtqd7y76c + +Web: +- New website by @npub18ams6ewn5aj2n3wt2qawzglx9mr4nzksxhvrdc4gzrecw7n5tvjqctp424 + +Quartz: +- Adds support for Trust Provider lists and Contact Cards for NIP-85 +- Early support for Payment targets as per [NIP-A3](https://github.com/nostr-protocol/nips/pull/2119) by @npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5 +- Initial support for NIP 46 by @npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5 +- Adds support for fast MurMur hash 3 64 bits +- Adds a nextLong secure random method +- Removing the generalist approach of ptag-mentions +- Removes deprecated fields in UserMetadata +- Removes compose bom from Quartz to avoid unnecessary dependencies. +- Removes datetime dependencies from Quartz +- Adds dependency on coroutines directly (instead of through compose runtime) +- Removes old secp256 target dependencies +- Adds Default scope for NostrClient and Relay Authenticator + +Quartz-Event Store: +- Moves from text tags to probabilistic 64-bit MurMur Hash3 integers for performance +- Moves from range index queries to kind,pubkey queries by default. +- Adds simpler SQL queries for specific simple Nostr filters +- Expose SQL query plans, vacuum, and analyse to lib users +- Implements AND Tag queries from [NIP-91](https://github.com/nostr-protocol/nips/pull/1365) +- Implements GiftWrap deletions by p-Tag with deletions and vanish requests +- Offers several indexing strategy options to users. +- Adds several test cases that verify not only the SQL but also the indexes used +- Exposes raw queries that return columns for relays that might not need the tag array +- Forces the use of the index on Addressables and Replaceables on triggers +- Fixes duplicated events being returned from the DB +- Fixes unused Or condition in the SQL builder +- Refine the structure of the module classes for the DB +- Removes the Statement cache since statements are not thread safe +- Creating interfaces for multiple EventStores + +Code Quality: +- Updates kotlin, compose, multiplatform, activity, serialization, media3, mockk, secp256, tor, androidxCamera, stdlib +- Adds a compose stability plugin to allow traces in debug +- Updates to the latest Zapstore config +- Updates quarts instructions in the ReadMe. + +Updated translations: +- Czech, German, Swedish, and Portuguese by @npub1e2yuky03caw4ke3zy68lg0fz3r4gkt94hx4fjmlelacyljgyk79svn3eef +- Polish by @npub16gjyljum0ksrrm28zzvejydgxwfm7xse98zwc4hlgq8epxeuggushqwyrm +- Hungarian by @npub1ww8kjxz2akn82qptdpl7glywnchhkx3x04hez3d3rye397turrhssenvtp @npub1dnvslq0vvrs8d603suykc4harv94yglcxwna9sl2xu8grt2afm3qgfh0tp +- Hindi by @npub1ww6huwu3xye6r05n3qkjeq62wds5pq0jswhl7uc59lchc0n0ns4sdtw5e6 +- Slovenian by @npub1qqqqqqz7nhdqz3uuwmzlflxt46lyu7zkuqhcapddhgz66c4ddynswreecw +- Spanish by @npub1luhyzgce7qtcs6r6v00ryjxza8av8u4dzh3avg0zks38tjktnmxspxq903 +- Latvian by @npub1l60stxkwwmkts76kv02vdjppka9uy6y3paztck7paau7g64l687saaw6av +- Dutch by @npub1w4la29u3zv09r6crx5u8yxax0ffxgekzdm2egzjkjckef7xc83fs0ftxcd +- French by @npub106efcyntxc5qwl3w8krrhyt626m59ya2nk9f40px5s968u5xdwhsjsr8fz and Alexis Magzalci +- Chinese by @npub1gd8e0xfkylc7v8c5a6hkpj4gelwwcy99jt90lqjseqjj2t253s2s6ch58h + + +# [Release v1.04.2: Fix for Google Play](https://github.com/vitorpamplona/amethyst/releases/tag/v1.04.2) - 2025-11-15 + +Quick release for Google. + # [Release v1.04.1: Bugfixes](https://github.com/vitorpamplona/amethyst/releases/tag/v1.04.1) - 2025-11-15 diff --git a/README.md b/README.md index 54297b800..2762617d6 100644 --- a/README.md +++ b/README.md @@ -259,16 +259,16 @@ repositories { Add the following line to your `commonMain` dependencies: ```gradle -implementation('com.vitorpamplona.quartz:quartz:') +implementation('com.vitorpamplona.quartz:quartz:1:05.0') ``` Variations to each platform are also available: ```gradle -implementation('com.vitorpamplona.quartz:quartz-android:') -implementation('com.vitorpamplona.quartz:quartz-jvm:') -implementation('com.vitorpamplona.quartz:quartz-iosarm64:') -implementation('com.vitorpamplona.quartz:quartz-iossimulatorarm64:') +implementation('com.vitorpamplona.quartz:quartz-android:1:05.0') +implementation('com.vitorpamplona.quartz:quartz-jvm:1:05.0') +implementation('com.vitorpamplona.quartz:quartz-iosarm64:1:05.0') +implementation('com.vitorpamplona.quartz:quartz-iossimulatorarm64:1:05.0') ``` Check versions on [MavenCentral](https://central.sonatype.com/search?q=com.vitorpamplona.quartz) diff --git a/amethyst/build.gradle b/amethyst/build.gradle index 744720060..e35d3cb44 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -45,9 +45,9 @@ android { applicationId = "com.vitorpamplona.amethyst" minSdk = libs.versions.android.minSdk.get().toInteger() targetSdk = libs.versions.android.targetSdk.get().toInteger() - versionCode = 430 - versionName = generateVersionName("1.04.2") - buildConfigField "String", "RELEASE_NOTES_ID", "\"3a03c75d85aaf6b181d3b232d064c4d4feea5c73f0bea2bd91ed61b8da7cd6a6\"" + versionCode = 431 + versionName = generateVersionName("1.05.0") + buildConfigField "String", "RELEASE_NOTES_ID", "\"b457a20195ffcf501389fcb708f0ef73f4ee263e3bba63f1b893a896129e4c79\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index a9f1d39e6..b942e49fe 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -264,7 +264,7 @@ mavenPublishing { coordinates( groupId = "com.vitorpamplona.quartz", artifactId = "quartz", - version = "1.04.2" + version = "1.05.0" ) // Configure publishing to Maven Central From 466ae37b856f23dc4efbddcb8e6427734ba762bc Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 8 Jan 2026 17:30:46 -0500 Subject: [PATCH 120/137] Fixes bug of mixing DMs between the logged in users. --- .../com/vitorpamplona/quartz/nip17Dm/base/BaseDMGroupEvent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip17Dm/base/BaseDMGroupEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip17Dm/base/BaseDMGroupEvent.kt index 0e2d3da7c..aa892937a 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip17Dm/base/BaseDMGroupEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip17Dm/base/BaseDMGroupEvent.kt @@ -69,7 +69,7 @@ open class BaseDMGroupEvent( return result } - override fun isIncluded(user: HexKey) = pubKey == this.pubKey || tags.any(PTag::isTagged, pubKey) + override fun isIncluded(user: HexKey) = user == this.pubKey || tags.any(PTag::isTagged, user) override fun groupMembers() = recipientsPubKey().plus(pubKey).toSet() From dca9a1da4731e084ec739f92b6d86e9eb392ece6 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 8 Jan 2026 18:29:00 -0500 Subject: [PATCH 121/137] Clicking on Drafts now edits the post. --- .../vitorpamplona/amethyst/ui/note/NoteCompose.kt | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 6d152f6ca..f9db3f422 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -65,6 +65,7 @@ import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.layouts.GenericRepostLayout import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeEditDraftTo import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.DisplayZapSplits import com.vitorpamplona.amethyst.ui.note.elements.BoostedMark @@ -225,6 +226,7 @@ import com.vitorpamplona.quartz.nip90Dvms.NIP90StatusEvent import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @Composable @@ -499,7 +501,16 @@ fun ClickableNote( } else { baseNote } - routeFor(redirectToNote, accountViewModel.account)?.let { nav.nav(it) } + + nav.nav { + if (redirectToNote.event is DraftWrapEvent) { + with(Dispatchers.IO) { + routeEditDraftTo(redirectToNote, accountViewModel.account) + } + } else { + routeFor(redirectToNote, accountViewModel.account) + } + } }, onLongClick = showPopup, ).background(backgroundColor.value) From 54c1605de7d5bc83f62a13364b8efea53173447e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 8 Jan 2026 19:01:01 -0500 Subject: [PATCH 122/137] v1.05.1 --- CHANGELOG.md | 6 ++++++ amethyst/build.gradle | 4 ++-- quartz/build.gradle.kts | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c7cfd601..e8fa1598e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ + +# [Release v1.05.1: BugFixes](https://github.com/vitorpamplona/amethyst/releases/tag/v1.05.0) - 2025-01-08 + +- Fixed mixed DMs between logged in users. +- Fixed draft screen click to edit post. + # [Release v1.05.0: Bookmark Lists and WoT Scores](https://github.com/vitorpamplona/amethyst/releases/tag/v1.05.0) - 2025-01-08 diff --git a/amethyst/build.gradle b/amethyst/build.gradle index e35d3cb44..ee42fb543 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -45,8 +45,8 @@ android { applicationId = "com.vitorpamplona.amethyst" minSdk = libs.versions.android.minSdk.get().toInteger() targetSdk = libs.versions.android.targetSdk.get().toInteger() - versionCode = 431 - versionName = generateVersionName("1.05.0") + versionCode = 432 + versionName = generateVersionName("1.05.1") buildConfigField "String", "RELEASE_NOTES_ID", "\"b457a20195ffcf501389fcb708f0ef73f4ee263e3bba63f1b893a896129e4c79\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index b942e49fe..e279c3576 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -264,7 +264,7 @@ mavenPublishing { coordinates( groupId = "com.vitorpamplona.quartz", artifactId = "quartz", - version = "1.05.0" + version = "1.05.1" ) // Configure publishing to Maven Central From e874e302e06d14191d6ccf047b5e09f2e82dbc58 Mon Sep 17 00:00:00 2001 From: davotoula Date: Fri, 9 Jan 2026 11:04:34 +0100 Subject: [PATCH 123/137] update cz, pt, de, sv --- .../src/main/res/values-cs-rCZ/strings.xml | 49 +++++++++++++++++-- .../src/main/res/values-de-rDE/strings.xml | 47 +++++++++++++++++- .../src/main/res/values-pt-rBR/strings.xml | 49 +++++++++++++++++-- .../src/main/res/values-sv-rSE/strings.xml | 49 +++++++++++++++++-- 4 files changed, 183 insertions(+), 11 deletions(-) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index c32931f87..0cae372a8 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -655,10 +655,8 @@ Předplatné Filtry Délka ID předplatného - Minimální předpona - Maximální počet značek události + Min. předpona Délka obsahu - Minimální PoW Ověření Platba Cashu Token @@ -1184,4 +1182,49 @@ Broadcast sady doporučení Smazat seznam Smazat sadu doporučení + Jméno (pro @tagování) + Moje @tag jméno + Oznámení relé + Duplicitní příspěvek + Chyba + Servisní klíč + Spuštěno %1$s + Spuštěno %1$s (%2$s) + Podporované GRASPy + Vstup + Předplatné + Publikování + Platby %1$s + Cílové publikum + Zásady & odkazy + Poplatky & platby + Témata + Všechny země + Všechny jazyky + Zásady ochrany soukromí + Podmínky & ujednání + N/A + Max. značek události + Max. délka obsahu + Zahazuje starší než + Přijímá až + %1$s v budoucnu + před %1$s + %1$s nul + %1$s bitů + Uchovávání událostí + Velikost obsahu + Připojení + Řízení přístupu + Min. obtížnost PoW + Vyžaduje se autentizace + Vyžaduje se platba + Max. délka zprávy + Max. odběrů + Max. filtrů na odběr + Max. limit (vracené události) + Výchozí limit (vracené události) + Max. délka SubID + Výchozí záložky + Vaše výchozí záložky, které mnoho klientů podporuje diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index 2073348b2..dc8aebca6 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -661,9 +661,7 @@ anz der Bedingungen ist erforderlich Filter Länge der Abonnement-ID Mindestpräfix - Maximale Anzahl von Ereignis-Tags Inhaltslänge - Mindest-PoW Authentifizierung Zahlung Cashu-Token @@ -1189,4 +1187,49 @@ anz der Bedingungen ist erforderlich Empfehlungspaket veröffentlichen Liste löschen Empfehlungspaket löschen + Name (zum @Taggen) + Mein @tag-Name + Relay-Hinweis + Doppelter Beitrag + Fehler + Service-Schlüssel + %1$s wird ausgeführt + %1$s wird ausgeführt (%2$s) + Unterstützte GRASPs + Zutritt + Abonnement + Veröffentlichung + Zahlungen %1$s + Zielgruppe + Richtlinien & Links + Gebühren & Zahlungen + Themen + Alle Länder + Alle Sprachen + Datenschutzerklärung + Allgemeine Geschäftsbedingungen + N/A + Max. Event-Tags + Max. Inhaltslänge + Verwirft älter als + Akzeptiert bis zu + %1$s in der Zukunft + vor %1$s + %1$s Nullen + %1$s Bits + Ereignis-Aufbewahrung + Inhaltsgröße + Verbindung + Zugriffskontrolle + Min. PoW-Schwierigkeit + Authentifizierung erforderlich + Zahlung erforderlich + Max. Nachrichtenlänge + Max. Abonnements + Max. Filter pro Abo + Max. Limit (zurückgegebene Ereignisse) + Standardlimit (zurückgegebene Ereignisse) + Max. SubID-Länge + Standard-Lesezeichen + Deine Standard-Lesezeichen, die viele Clients unterstützen diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 6cbc364be..05c69d1a0 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -655,10 +655,7 @@ Assinaturas Filtros Comprimento do ID da assinatura - Prefixo mínimo - Máximo de tags de evento Tamanho do conteúdo - PoW mínimo Autenticação Pagamento Token Cashu @@ -1184,4 +1181,50 @@ Transmitir pacote de recomendações Excluir lista Excluir pacote de recomendações + Nome (para @marcar) + Meu nome de @tag + Aviso do relay + Post duplicado + Erro + Chave de serviço + Executando %1$s + Executando %1$s (%2$s) + GRASPs compatíveis + Entrada + Assinatura + Publicação + Pagamentos %1$s + Público-alvo + Políticas & links + Taxas & pagamentos + Tópicos + Todos os países + Todos os idiomas + Política de privacidade + Termos & condições + N/A + Prefixo mín. + Máx. tags de evento + Máx. tamanho do conteúdo + Descarta mais antigos que + Aceita até + %1$s no futuro + há %1$s + %1$s zeros + %1$s bits + Retenção de eventos + Tamanho do conteúdo + Conectividade + Controle de acesso + Dificuldade mínima de PoW + Autenticação obrigatória + Pagamento obrigatório + Máx. tamanho da mensagem + Máx. inscrições + Máx. filtros por inscrição + Limite máx. (eventos retornados) + Limite padrão (eventos retornados) + Comprimento máx. do SubID + Marcadores padrão + Seus marcadores padrão que muitos clientes suportam diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 6939e8824..f4646c226 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -654,10 +654,7 @@ Prenumerationer Filter Längd på prenumerations-ID - Minsta prefix - Maximalt antal händelse-taggar Innehållslängd - Minsta PoW Autentisering Betalning Cashu Token @@ -1183,4 +1180,50 @@ Sänd rekommendationspaket Ta bort lista Ta bort rekommendationspaket + Namn (för @taggning) + Mitt @tag-namn + Relay-meddelande + Duplicerat inlägg + Fel + Service-nyckel + Kör %1$s + Kör %1$s (%2$s) + Stödda GRASPs + Inträde + Prenumeration + Publicering + Betalningar %1$s + Målgrupp + Policyer & länkar + Avgifter & betalningar + Ämnen + Alla länder + Alla språk + Integritetspolicy + Villkor & bestämmelser + N/A + Min. prefix + Max. eventtaggar + Max. innehållslängd + Kastar bort äldre än + Accepterar upp till + %1$s i framtiden + för %1$s sedan + %1$s nollor + %1$s bitar + Lagringstid för händelser + Innehållsstorlek + Anslutning + Åtkomstkontroll + Min. PoW-svårighet + Autentisering krävs + Betalning krävs + Max. meddelandelängd + Max. prenumerationer + Max. filter per prenumeration + Maxgräns (returnerade händelser) + Standardgräns (returnerade händelser) + Max. SubID-längd + Standardbokmärken + Dina standardbokmärken som många klienter stödjer From c7b408f79d7b25c46513502d52d5408a535e483d Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 9 Jan 2026 11:29:27 +0000 Subject: [PATCH 124/137] New Crowdin translations by GitHub Action --- .../src/main/res/values-cs-rCZ/strings.xml | 89 +++++++++--------- .../src/main/res/values-de-rDE/strings.xml | 89 +++++++++--------- .../src/main/res/values-hi-rIN/strings.xml | 46 +++++++++- .../src/main/res/values-pt-rBR/strings.xml | 89 +++++++++--------- .../src/main/res/values-sv-rSE/strings.xml | 91 +++++++++---------- .../src/main/res/values-zh-rCN/strings.xml | 43 +++++++++ 6 files changed, 264 insertions(+), 183 deletions(-) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 0cae372a8..8ff4d900a 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -15,6 +15,8 @@ Nepodařilo se dešifrovat zprávu Obrázek skupiny Explicitní obsah + Oznámení relé + Duplicitní příspěvek Nevyžádaná pošta Počet spamových událostí z tohoto relé Zneužívání identity @@ -121,6 +123,7 @@ Adresa relé Příspěvky Byty + Chyba Chyby Počet chyb připojení v této relaci Domovský kanál @@ -132,6 +135,8 @@ Přidat uživatele Přidat přeposílání Jméno + Jméno (pro @tagování) + Moje @tag jméno Zobrazované jméno Moje zobrazované jméno Ostrich McAwesome @@ -358,6 +363,8 @@ Blokovat Manuální zap rozdělení Záložky + Výchozí záložky + Vaše výchozí záložky, které mnoho klientů podporuje Koncepty Soukromé záložky Veřejné záložky @@ -639,26 +646,63 @@ Množství bajtů, které bylo přijato z tohoto relé, včetně filtrů a událostí Při pokusu o získání informací z Relay se vyskytla chyba z %1$s Vlastník + Servisní klíč + Spuštěno %1$s + Spuštěno %1$s (%2$s) Verze Programové vybavení Kontakt Podporované NIPs + Podporované GRASPy Vstupné + Vstup + Předplatné + Publikování + Platby %1$s URL plateb + Cílové publikum + Zásady & odkazy + Poplatky & platby Omezení Země Jazyky Tagy + Témata + Všechny země + Všechny jazyky Politika příspěvků + Zásady ochrany soukromí + Podmínky & ujednání Chyby a upozornění z tohoto relé Délka zprávy Předplatné Filtry Délka ID předplatného Min. předpona + Max. značek události Délka obsahu + Max. délka obsahu + Zahazuje starší než + Přijímá až + %1$s v budoucnu + před %1$s + %1$s nul + %1$s bitů + Uchovávání událostí + Velikost obsahu + Připojení + Řízení přístupu + Min. obtížnost PoW Ověření + Vyžaduje se autentizace Platba + Vyžaduje se platba + Max. délka zprávy + Max. odběrů + Max. filtrů na odběr + Max. limit (vracené události) + Výchozí limit (vracené události) + Max. délka SubID Cashu Token Vyměnit Poslat do Zap peněženky @@ -1182,49 +1226,4 @@ Broadcast sady doporučení Smazat seznam Smazat sadu doporučení - Jméno (pro @tagování) - Moje @tag jméno - Oznámení relé - Duplicitní příspěvek - Chyba - Servisní klíč - Spuštěno %1$s - Spuštěno %1$s (%2$s) - Podporované GRASPy - Vstup - Předplatné - Publikování - Platby %1$s - Cílové publikum - Zásady & odkazy - Poplatky & platby - Témata - Všechny země - Všechny jazyky - Zásady ochrany soukromí - Podmínky & ujednání - N/A - Max. značek události - Max. délka obsahu - Zahazuje starší než - Přijímá až - %1$s v budoucnu - před %1$s - %1$s nul - %1$s bitů - Uchovávání událostí - Velikost obsahu - Připojení - Řízení přístupu - Min. obtížnost PoW - Vyžaduje se autentizace - Vyžaduje se platba - Max. délka zprávy - Max. odběrů - Max. filtrů na odběr - Max. limit (vracené události) - Výchozí limit (vracené události) - Max. délka SubID - Výchozí záložky - Vaše výchozí záložky, které mnoho klientů podporuje diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index dc8aebca6..f31726918 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -15,6 +15,8 @@ Nachricht konnte nicht entschlüsselt werden Gruppenbild Anstößiger Inhalt + Relay-Hinweis + Doppelter Beitrag Spam Anzahl der Spam-Ereignisse von diesem Relais Vortäuschung @@ -121,6 +123,7 @@ Relay-Adresse Beiträge Bytes + Fehler Fehler Anzahl der Verbindungsfehler in dieser Sitzung Startseite @@ -132,6 +135,8 @@ Benutzer hinzufügen Relay hinzufügen Name + Name (zum @Taggen) + Mein @tag-Name Anzeigename Mein Anzeigename Ostrich McAwesome @@ -364,6 +369,8 @@ anz der Bedingungen ist erforderlich Blockieren Manuelle Zap-Splits Lesezeichen + Standard-Lesezeichen + Deine Standard-Lesezeichen, die viele Clients unterstützen Entwürfe Private Lesezeichen Öffentliche Lesezeichen @@ -644,26 +651,63 @@ anz der Bedingungen ist erforderlich Die Menge in Bytes, die von diesem Relais empfangen wurde, einschließlich Filter und Ereignisse Ein Fehler ist beim Abrufen von Relay-Informationen von %1$s aufgetreten Inhaber + Service-Schlüssel + %1$s wird ausgeführt + %1$s wird ausgeführt (%2$s) Version Programme Kontakt Unterstützte NIPs + Unterstützte GRASPs Eintrittsgebühren + Zutritt + Abonnement + Veröffentlichung + Zahlungen %1$s Zahlungs-URL + Zielgruppe + Richtlinien & Links + Gebühren & Zahlungen Einschränkungen Länder Sprachen Tags + Themen + Alle Länder + Alle Sprachen Veröffentlichungsrichtlinie + Datenschutzerklärung + Allgemeine Geschäftsbedingungen Fehler und Hinweise von diesem Relais Nachrichtenlänge Abonnements Filter Länge der Abonnement-ID Mindestpräfix + Max. Event-Tags Inhaltslänge + Max. Inhaltslänge + Verwirft älter als + Akzeptiert bis zu + %1$s in der Zukunft + vor %1$s + %1$s Nullen + %1$s Bits + Ereignis-Aufbewahrung + Inhaltsgröße + Verbindung + Zugriffskontrolle + Min. PoW-Schwierigkeit Authentifizierung + Authentifizierung erforderlich Zahlung + Zahlung erforderlich + Max. Nachrichtenlänge + Max. Abonnements + Max. Filter pro Abo + Max. Limit (zurückgegebene Ereignisse) + Standardlimit (zurückgegebene Ereignisse) + Max. SubID-Länge Cashu-Token Einlösen An Zap Wallet senden @@ -1187,49 +1231,4 @@ anz der Bedingungen ist erforderlich Empfehlungspaket veröffentlichen Liste löschen Empfehlungspaket löschen - Name (zum @Taggen) - Mein @tag-Name - Relay-Hinweis - Doppelter Beitrag - Fehler - Service-Schlüssel - %1$s wird ausgeführt - %1$s wird ausgeführt (%2$s) - Unterstützte GRASPs - Zutritt - Abonnement - Veröffentlichung - Zahlungen %1$s - Zielgruppe - Richtlinien & Links - Gebühren & Zahlungen - Themen - Alle Länder - Alle Sprachen - Datenschutzerklärung - Allgemeine Geschäftsbedingungen - N/A - Max. Event-Tags - Max. Inhaltslänge - Verwirft älter als - Akzeptiert bis zu - %1$s in der Zukunft - vor %1$s - %1$s Nullen - %1$s Bits - Ereignis-Aufbewahrung - Inhaltsgröße - Verbindung - Zugriffskontrolle - Min. PoW-Schwierigkeit - Authentifizierung erforderlich - Zahlung erforderlich - Max. Nachrichtenlänge - Max. Abonnements - Max. Filter pro Abo - Max. Limit (zurückgegebene Ereignisse) - Standardlimit (zurückgegebene Ereignisse) - Max. SubID-Länge - Standard-Lesezeichen - Deine Standard-Lesezeichen, die viele Clients unterstützen diff --git a/amethyst/src/main/res/values-hi-rIN/strings.xml b/amethyst/src/main/res/values-hi-rIN/strings.xml index 9b9946c55..8fff23237 100644 --- a/amethyst/src/main/res/values-hi-rIN/strings.xml +++ b/amethyst/src/main/res/values-hi-rIN/strings.xml @@ -15,6 +15,8 @@ सन्देश का अरहस्यीकरण असफल समूह चित्र अभद्र विषयवस्तु + पुनःप्रसारक सूचना + दोहराया गया पत्र कचरालेख इस पुनःप्रसारक से कचरालेख घटनाओं की संख्या पररूपण @@ -121,6 +123,7 @@ पुनःप्रसारक पता प्रकाशित पत्र अष्टक + अपक्रम अपक्रम संयोजन अपक्रमों की संख्या इस सत्र में मुख्य सूचनावली @@ -132,6 +135,8 @@ प्रयोक्ता जोडें पुनःप्रसारक जोडें नाम + नाम (@सूचक के लिए) + मेरा @सूचक नाम प्रदर्शन नाम मेरा प्रदर्शन नाम उष्ट्रपक्षी मक्बढिया @@ -158,6 +163,7 @@ ऐक संदेश का अभिलेखन करें ऐक संदेश का अभिलेखन करें टाँकें तथा दबाए रखें संदेश का अभिलेखन करने के लिए + पुनरभिलेखन ध्वन्यभिलेखन ध्वन्यभिलेखन %1$s आरोहण चल रहा है… @@ -359,6 +365,8 @@ बाधित करें मनुष्यकृत ज्साप विभाजन स्मर्तव्य + मूलविकल्प स्मर्त्तव्य सूची + आपका मूलविकल्प स्मर्त्तव्य सूची जिसका अवलम्बन अनेक ग्राहक करते हैं पाण्डुलिपियाँ निजी स्मर्तव्य सार्वजनिक स्मर्तव्य @@ -642,17 +650,34 @@ अष्टकों में मात्रा जो इस पुनःप्रसारक से प्राप्त हुआ था छलनियाँ तथा घटनाएँ समेत %1$s से पुनःप्रसारक जानकारी प्राप्त करने के प्रयास में अपक्रम हुआ अधिपति + सेवा कुंजी + %1$s चलाया जा रहा है + %1$s (%2$s) चलाया जा रहा है संस्करण क्रमक सम्पर्क अवलम्बन किए हुए निप॰ सूची + अवलम्बित पकड प्रवेश शुल्क + प्रवेशन + ग्राहकता + प्रकाशन + भुगतान %1$s भुगतान जालपता + लक्ष्य दर्शक + नीतियाँ तथा जालयोजक + शुल्क तथा भुगतान परिसीमाएँ देश भाषाएँ विषयसूचक + विषय सूची + सभी देश + सभी भाषाएँ पत्र प्रकाशन नीति + गोपनीयता नीति + नियम तथा अवस्था + अनुपलब्ध अपक्रम तथा सूचनाएँ इस पुनःप्रसारक से संदेश लम्बाई ग्राहकताएँ @@ -661,9 +686,28 @@ न्यूनतम उपसर्ग अधिकतम घटना विषयसूचक विषयवस्तु लम्बाई + अधिकतम विषयवस्तु लम्बाई + इससे पुराना हटाता है + इस तक स्वीकारता है + %1$s भविष्य में + %1$s पूर्व + %1$s शून्यांक + %1$s द्वयंक + घटना रखाई + विषयवस्तु आकार + संयोजकता + अभिगमन नियन्त्रण न्यूनतम श्रमप्रमाण प्रमाणीकरण + प्रमाणीकरण आवश्यक भुगतान + भुगतान आवश्यक + अधिकतम संदेश लम्बाई + अधिकतम ग्राहकताएँ + अधिकतम छलनियाँ प्रति ग्राहकता + अधिकतम सीमा (घटनाएँ प्रतिफलित) + मूलविकल्प सीमा (घटनाएँ प्रतिफलित) + अधिकतम ग्राहकताविभेदक लम्बाई काशयु राशिखण्ड चुकाएँ ज्साप धनकोष को भेजें @@ -685,7 +729,7 @@ ये प्रयोक्ता सूचियाँ हैं जो आप अन्य लोगों के लिए अनुशंसित करते हैं। केवल सार्वजनिक प्रयोक्ता अनुमत। पठितव्य सूचनावली विधियाँ - पण्यक्षेत्र + व्यापारक्षेत्र तत्क्षणप्रसार समुदाय चर्चाएँ diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 05c69d1a0..d3d01e993 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -15,6 +15,8 @@ Não foi possível descriptografar a mensagem Imagem do grupo Conteúdo explícito + Aviso do relay + Post duplicado Spam O número de eventos de spam vindo deste relé Representação @@ -121,6 +123,7 @@ Endereço do Relay Postagens Bytes + Erro Erros O número de erros de conexão nesta sessão Feed principal @@ -132,6 +135,8 @@ Adicionar um usuário Adicionar um Relay Nome + Nome (para @marcar) + Meu nome de @tag Nome de Exibição Meu nome de exibição McAwesome Ostrich @@ -358,6 +363,8 @@ Bloquear Divisão manual de Zap Itens Salvos + Marcadores padrão + Seus marcadores padrão que muitos clientes suportam Rascunhos Itens Salvos Privados Itens Salvos Públicos @@ -639,25 +646,61 @@ A quantidade em bytes que foi recebida deste relé, incluindo filtros e eventos Ocorreu um erro ao tentar obter informações do relay de %1$s Proprietário + Chave de serviço + Executando %1$s + Executando %1$s (%2$s) Versão Programa Contato NIPs Suportados + GRASPs compatíveis Taxas de admissão + Entrada + Assinatura + Publicação + Pagamentos %1$s URL de pagamentos + Público-alvo + Políticas & links + Taxas & pagamentos Limitações Países Línguas Cerquilhas + Tópicos + Todos os países + Todos os idiomas Política de postagem + Política de privacidade + Termos & condições Erros e Avisos deste Relé Tamanho da mensagem Assinaturas Filtros Comprimento do ID da assinatura + Prefixo mín. + Máx. tags de evento Tamanho do conteúdo + Máx. tamanho do conteúdo + Descarta mais antigos que + Aceita até + %1$s no futuro + há %1$s + Retenção de eventos + Tamanho do conteúdo + Conectividade + Controle de acesso + Dificuldade mínima de PoW Autenticação + Autenticação obrigatória Pagamento + Pagamento obrigatório + Máx. tamanho da mensagem + Máx. inscrições + Máx. filtros por inscrição + Limite máx. (eventos retornados) + Limite padrão (eventos retornados) + Comprimento máx. do SubID Token Cashu Resgatar Enviar para Zap Wallet @@ -1181,50 +1224,4 @@ Transmitir pacote de recomendações Excluir lista Excluir pacote de recomendações - Nome (para @marcar) - Meu nome de @tag - Aviso do relay - Post duplicado - Erro - Chave de serviço - Executando %1$s - Executando %1$s (%2$s) - GRASPs compatíveis - Entrada - Assinatura - Publicação - Pagamentos %1$s - Público-alvo - Políticas & links - Taxas & pagamentos - Tópicos - Todos os países - Todos os idiomas - Política de privacidade - Termos & condições - N/A - Prefixo mín. - Máx. tags de evento - Máx. tamanho do conteúdo - Descarta mais antigos que - Aceita até - %1$s no futuro - há %1$s - %1$s zeros - %1$s bits - Retenção de eventos - Tamanho do conteúdo - Conectividade - Controle de acesso - Dificuldade mínima de PoW - Autenticação obrigatória - Pagamento obrigatório - Máx. tamanho da mensagem - Máx. inscrições - Máx. filtros por inscrição - Limite máx. (eventos retornados) - Limite padrão (eventos retornados) - Comprimento máx. do SubID - Marcadores padrão - Seus marcadores padrão que muitos clientes suportam diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index f4646c226..623a0eadb 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -15,6 +15,8 @@ Kunde inte dekryptera meddelandet Grupp bild Explicit Innehåll + Relay-meddelande + Duplicerat inlägg Spam Antal skräpposthändelser från detta relä Imitation @@ -121,6 +123,7 @@ Relä Adress Inlägg Bytes + Fel Fel Antal anslutningsfel under denna session Hem Flöde @@ -132,6 +135,8 @@ Lägg till en användare Lägg till Relä Namn + Namn (för @taggning) + Mitt @tag-namn Visningsnamn Mitt visningsnamn Struts McAwesome @@ -358,6 +363,8 @@ Blockera Manuell Zap delning Bokmärken + Standardbokmärken + Dina standardbokmärken som många klienter stödjer Utkast Privata Bokmärken Publika Bokmärken @@ -638,25 +645,63 @@ Mängden data i byte som mottogs från detta relä, inklusive filter och händelser Ett fel inträffade vid försök att hämta information från Relay %1$s Ägare + Service-nyckel + Kör %1$s + Kör %1$s (%2$s) Version Programvara Kontakt Stödda NIPs + Stödda GRASPs Entréavgifter + Inträde + Prenumeration + Publicering + Betalningar %1$s Betalnings-URL + Målgrupp + Policyer & länkar + Avgifter & betalningar Begränsningar Länder Språk Taggar + Ämnen + Alla länder + Alla språk Publiceringspolicy + Integritetspolicy + Villkor & bestämmelser Fel och meddelanden från detta relä Meddelandelängd Prenumerationer Filter Längd på prenumerations-ID + Min. prefix + Max. eventtaggar Innehållslängd + Max. innehållslängd + Kastar bort äldre än + Accepterar upp till + %1$s i framtiden + för %1$s sedan + %1$s nollor + %1$s bitar + Lagringstid för händelser + Innehållsstorlek + Anslutning + Åtkomstkontroll + Min. PoW-svårighet Autentisering + Autentisering krävs Betalning + Betalning krävs + Max. meddelandelängd + Max. prenumerationer + Max. filter per prenumeration + Maxgräns (returnerade händelser) + Standardgräns (returnerade händelser) + Max. SubID-längd Cashu Token Inlösen Skicka till Zap Plånbok @@ -1180,50 +1225,4 @@ Sänd rekommendationspaket Ta bort lista Ta bort rekommendationspaket - Namn (för @taggning) - Mitt @tag-namn - Relay-meddelande - Duplicerat inlägg - Fel - Service-nyckel - Kör %1$s - Kör %1$s (%2$s) - Stödda GRASPs - Inträde - Prenumeration - Publicering - Betalningar %1$s - Målgrupp - Policyer & länkar - Avgifter & betalningar - Ämnen - Alla länder - Alla språk - Integritetspolicy - Villkor & bestämmelser - N/A - Min. prefix - Max. eventtaggar - Max. innehållslängd - Kastar bort äldre än - Accepterar upp till - %1$s i framtiden - för %1$s sedan - %1$s nollor - %1$s bitar - Lagringstid för händelser - Innehållsstorlek - Anslutning - Åtkomstkontroll - Min. PoW-svårighet - Autentisering krävs - Betalning krävs - Max. meddelandelängd - Max. prenumerationer - Max. filter per prenumeration - Maxgräns (returnerade händelser) - Standardgräns (returnerade händelser) - Max. SubID-längd - Standardbokmärken - Dina standardbokmärken som många klienter stödjer diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index 714143826..6cbc2e7d5 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -15,6 +15,8 @@ 无法解密消息 群聊图片 明确内容 + 中继通知 + 重复的贴文 垃圾信息 来自该中继的垃圾事件数量 冒充 @@ -121,6 +123,7 @@ 中继器地址 帖文 字节 + 错误 错误 该会话中产生的连接错误数量 主页 @@ -132,6 +135,8 @@ 添加用户 添加中继器 名称 + 名称 (用于 @ 标记时显示) + 我的 @ 名称 显示名称 我的显示名称 Nostr 太酷了 @@ -360,6 +365,8 @@ 仅阻止 手动 Zap 拆分 书签 + 默认书签 + 许多客户端支持默认书签 草稿 私人书签 公开书签 @@ -643,17 +650,34 @@ 从该中继接收事件和过滤响应所使用的数据量 尝试从 %1$s 获取中继器信息时出错 机主 + 服务密钥 + 正在运行 %1$s + 正在运行 %1$s (%2$s) 版本 软件 联络 支持的 NIPs + 支持 Grasps 接纳费 + 接纳 + 订阅 + 发布 + 付款 %1$s 付款网址 + 目标受众 + 策略 & 链接 + 费用 & 付款 限制 国家 语言 标签 + 话题 + 所有国家和地区 + 所有语言 发布政策 + 隐私政策 + 使用条款 + N/A 该中继的错误和通知 消息长度 订阅 @@ -662,9 +686,28 @@ 最少前缀 最多事件标签 内容长度 + 最大内容长度 + 丢弃旧于 + 最多接受 + 未来 %1$s + %1$s 之前 + %1$s 整 + %1$s 位 + 事件保留 + 内容大小 + 连通性 + 访问控制 最低工作量证明 认证 + 需要认证 支付 + 需要付费 + 最大消息长度 + 最大订阅连接数 + 最多订阅过滤器 + 最大数量(返回事件) + 默认限制(返回事件) + 最大 SubID 长度 Cashu 代币 兑换 发送到打闪钱包 From 1c26ccecc0173b755d24512ec7d7efe8b3019416 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 10 Jan 2026 11:15:03 -0500 Subject: [PATCH 125/137] Adds support for NipTexts on Amethyst --- .../amethyst/model/LocalCache.kt | 47 +++++ .../subassemblies/SearchPostsByText.kt | 2 + .../amethyst/ui/note/NoteCompose.kt | 3 + .../amethyst/ui/note/elements/ForkInfo.kt | 4 +- .../amethyst/ui/note/types/Nip.kt | 196 ++++++++++++++++++ .../amethyst/ui/screen/TopNavFilterState.kt | 3 + .../dal/FollowPackFeedNewThreadFeedFilter.kt | 4 +- .../datasource/FilterPostsByHashtags.kt | 2 + .../nip01Core/FilterHomePostsByHashtags.kt | 2 + .../nip65Follows/FilterHomePostsByAuthors.kt | 2 + .../FilterHomePostsFromCommunities.kt | 3 + .../dal/NotificationFeedFilter.kt | 2 + .../datasource/FilterUserProfilePosts.kt | 2 + .../mutual/dal/UserProfileMutualFeedFilter.kt | 2 + .../dal/UserProfileNewThreadFeedFilter.kt | 2 + .../loggedIn/threadview/ThreadFeedView.kt | 23 ++ amethyst/src/main/res/values/strings.xml | 1 + .../experimental/forks/IForkableEvent.kt | 4 + .../experimental/nipsOnNostr/NipTextEvent.kt | 151 ++++++++++++++ .../nipsOnNostr/TagArrayBuilderExt.kt | 29 +++ .../experimental/nipsOnNostr/tags/ForkTag.kt | 145 +++++++++++++ .../tags/TitleTag.kt} | 29 +-- .../quartz/nip10Notes/BaseThreadedEvent.kt | 3 +- .../quartz/nip54Wiki/TagArrayBuilderExt.kt | 13 ++ .../quartz/nip54Wiki/WikiNoteEvent.kt | 4 +- .../quartz/utils/EventFactory.kt | 2 + 26 files changed, 659 insertions(+), 21 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Nip.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/IForkableEvent.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/NipTextEvent.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/TagArrayBuilderExt.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/tags/ForkTag.kt rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/{forks/BaseThreadedEventExt.kt => nipsOnNostr/tags/TitleTag.kt} (57%) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/TagArrayBuilderExt.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 1b452063f..0bad1548b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -46,6 +46,7 @@ import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStory import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.nns.NNSEvent import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent @@ -665,6 +666,51 @@ object LocalCache : ILocalCache { wasVerified: Boolean, ) = consumeRegularEvent(event, relay, wasVerified) + fun consume( + event: NipTextEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val version = getOrCreateNote(event.id) + val note = getOrCreateAddressableNote(event.address()) + val author = getOrCreateUser(event.pubKey) + + val isVerified = + if (version.event == null && (wasVerified || justVerify(event))) { + version.loadEvent(event, author, emptyList()) + version.moveAllReferencesTo(note) + true + } else { + wasVerified + } + + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event?.id == event.id) return wasVerified + + if (antiSpam.isSpam(event, relay)) { + return false + } + + if (isVerified || justVerify(event)) { + val replyTo = computeReplyTo(event) + + if (event.createdAt > (note.createdAt() ?: 0L)) { + note.loadEvent(event, author, replyTo) + + refreshNewNoteObservers(note) + + return true + } + } + + return false + } + fun consume( event: LongTextNoteEvent, relay: NormalizedRelayUrl?, @@ -2852,6 +2898,7 @@ object LocalCache : ILocalCache { is MetadataEvent -> consume(event, relay, wasVerified) is MuteListEvent -> consume(event, relay, wasVerified) is NNSEvent -> consume(event, relay, wasVerified) + is NipTextEvent -> consume(event, relay, wasVerified) is OtsEvent -> consume(event, relay, wasVerified) is PictureEvent -> consume(event, relay, wasVerified) is PrivateDmEvent -> consume(event, relay, wasVerified) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPostsByText.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPostsByText.kt index dd1cf3e62..3c69f4fad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPostsByText.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPostsByText.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.nns.NNSEvent import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent @@ -81,6 +82,7 @@ val SearchPostsByTextKinds3 = InteractiveStoryPrologueEvent.KIND, InteractiveStorySceneEvent.KIND, FollowListEvent.KIND, + NipTextEvent.KIND, ) fun searchPostsByText( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index f9db3f422..d707d87fc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -118,6 +118,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityEvent import com.vitorpamplona.amethyst.ui.note.types.RenderLongFormContent import com.vitorpamplona.amethyst.ui.note.types.RenderNIP90ContentDiscoveryResponse import com.vitorpamplona.amethyst.ui.note.types.RenderNIP90Status +import com.vitorpamplona.amethyst.ui.note.types.RenderNipContent import com.vitorpamplona.amethyst.ui.note.types.RenderPinListEvent import com.vitorpamplona.amethyst.ui.note.types.RenderPoll import com.vitorpamplona.amethyst.ui.note.types.RenderPostApproval @@ -167,6 +168,7 @@ import com.vitorpamplona.quartz.experimental.forks.isAFork import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.tags.geohash.geoHashOrScope @@ -727,6 +729,7 @@ private fun RenderNoteRow( is ReportEvent -> RenderReport(baseNote, quotesLeft, backgroundColor, accountViewModel, nav) is LongTextNoteEvent -> RenderLongFormContent(baseNote, accountViewModel, nav) is WikiNoteEvent -> RenderWikiContent(baseNote, accountViewModel, nav) + is NipTextEvent -> RenderNipContent(baseNote, accountViewModel, nav) is BadgeAwardEvent -> RenderBadgeAward(baseNote, backgroundColor, accountViewModel, nav) is FhirResourceEvent -> RenderFhirResource(baseNote, accountViewModel, nav) is PeopleListEvent -> DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt index b8fa57650..6ad3d4bad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt @@ -83,7 +83,7 @@ fun ForkInformationRowLightColor( nav: INav, ) { val noteState by observeNote(originalVersion, accountViewModel) - val note = noteState?.note ?: return + val note = noteState.note val author = note.author ?: return val route = remember(note) { routeFor(note, accountViewModel.account) } @@ -129,7 +129,7 @@ fun ForkInformationRow( nav: INav, ) { val noteState by observeNote(originalVersion, accountViewModel) - val note = noteState?.note ?: return + val note = noteState.note val route = remember(note) { routeFor(note, accountViewModel.account) } if (route != null) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Nip.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Nip.kt new file mode 100644 index 000000000..557e26c46 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Nip.kt @@ -0,0 +1,196 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.note.types + +import android.R.attr.label +import android.R.attr.maxLines +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +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.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.ui.theme.QuoteBorder +import com.vitorpamplona.amethyst.ui.theme.Size5dp +import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn +import com.vitorpamplona.amethyst.ui.theme.subtleBorder +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent +import com.vitorpamplona.quartz.nip01Core.tags.kinds.kinds + +@Composable +fun RenderNipContent( + note: Note, + accountViewModel: AccountViewModel, + nav: INav, +) { + val noteEvent = note.event as? NipTextEvent ?: return + + NipNoteHeader(noteEvent, note, accountViewModel, nav) +} + +@Composable +private fun NipNoteHeader( + noteEvent: NipTextEvent, + note: Note, + accountViewModel: AccountViewModel, + nav: INav, +) { + val title = remember(noteEvent) { noteEvent.title() } + val kinds = remember(noteEvent) { noteEvent.kinds() } + + Column( + modifier = + Modifier + .padding(top = Size5dp) + .clip(shape = QuoteBorder) + .border( + 1.dp, + MaterialTheme.colorScheme.subtleBorder, + QuoteBorder, + ), + verticalArrangement = SpacedBy5dp, + ) { + title?.let { + Text( + text = it, + style = MaterialTheme.typography.titleMedium, + modifier = + Modifier + .fillMaxWidth() + .padding(start = 10.dp, end = 10.dp, top = 10.dp), + ) + } + Text( + text = remember(noteEvent) { noteEvent.summary() ?: noteEvent.content }, + style = MaterialTheme.typography.bodySmall, + modifier = + Modifier + .fillMaxWidth() + .padding(start = 10.dp, end = 10.dp, bottom = 10.dp), + color = Color.Gray, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + if (kinds.isNotEmpty()) { + FlowRow( + modifier = + Modifier + .fillMaxWidth() + .padding(start = 10.dp, end = 10.dp, bottom = 10.dp), + horizontalArrangement = SpacedBy5dp, + verticalArrangement = SpacedBy5dp, + itemVerticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.kinds), + ) + kinds.forEach { + NoPaddingSuggestionChip( + label = it.toString(), + ) + } + } + } + } +} + +@Preview +@Composable +fun NipNoteHeaderPreview() { + val event = + NipTextEvent( + id = "eb2b05394ff0014bb6a79c2eacfd1c80696821592f4dbf86c950c4bf16614aa0", + pubKey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", + createdAt = 1767978398, + content = "Trusted Translations\n--------------------\n\n`draft` `optional`\n\nThis NIP allows anyone to post a translation for any event on a `kind:76`.\n\n```js\n{\n \"kind\": 76,\n \"tags\": [\n [\"e\", \"\u003coriginal_event_id\u003e\", \"\u003crelay\u003e\"]\n [\"k\", \"\u003coriginal_event_kind\u003e\"]\n [\"l\", \"\u003ctranslated_to_language_country\u003e\"], // ISO 639-1: \"en\", \"es\", ...\n [\"l\", \"\u003ctranslated_to_language_code\u003e\"], // ISO 639-1: \"en-us\", \"en-br\"\n [\"s\", \"title\", \"translated title tag\"]\n [\"s\", \"summary\", \"translated summary tag\"]\n ],\n \"content\": \"this is a translated version of the original content\",\n // ...other fields\n}\n```\n\n`e` tag points to the event being translated, `k` tag points to the kind of that event.\n\n`l` tags define the language this was translated to in lowercase codes as defined by ISO 639-1. \n\n`s` tags are the translations for tags in the original event. \n\n`.content` contains the translation of the original `.content`\n\nClients SHOULD request translations by `e` and `l` tags spoken by their user. For every tag being rendered, Clients SHOULD look for their translated versions.\n\nProviders SHOULD use the event id in the filter to know which events need translations.\n\nProviders MAY store their translations behind a paid relay with NIP-42 auth.\n\n## Declaring Translation Providers\n\nKind `10041` lists the user's authorized translation providers. Each `p` tag is followed by the `pubkey` of the service publishing kind 76s, and the relay translations can be found. Users can specify these publicly or privately by JSON-stringifying and encrypting the tag list in the `.content` using NIP-44. \n\n```js\n{\n \"kind\": 10041,\n \"tags\": [\n [\"p\", \"4fd5e210530e4f6b2cb083795834bfe5108324f1ed9f00ab73b9e8fcfe5f12fe\", \"wss://translations.nostr.com\"],\n [\"l\", \"\u003ctranslated_to_language_country\u003e\"], // ISO 639-1: \"en\", \"es\", ...\n [\"l\", \"\u003ctranslated_to_language_code\u003e\"], // ISO 639-1: \"en-us\", \"en-br\"\n //...\n}\n```\n\n`l` tags in this event are the languages the user understands and wants translations to.\n\nProviders SHOULD create the `10041` event and post to the user's outbox relay.", + sig = "7fa9f1d49c41c7bfbdad6d089d1c6685777c86de8fbda7662a630888658839f0444cb1398385f759461c09191b287fa222eec0ffe22b3fa8cf740f71aab9dc21", + tags = + arrayOf( + arrayOf("d", "trusted-translations"), + arrayOf("title", "Trusted Translations"), + arrayOf("k", "1011"), + arrayOf("k", "10041"), + arrayOf("k", "1011"), + arrayOf("k", "10041"), + arrayOf("k", "1011"), + arrayOf("k", "10041"), + arrayOf("k", "1011"), + arrayOf("k", "10041"), + arrayOf("client", "nostrhub.io"), + ), + ) + + LocalCache.justConsume(event, null, true) + val note = LocalCache.getOrCreateNote(event.id) + + ThemeComparisonColumn( + toPreview = { + NipNoteHeader( + noteEvent = event, + note = note, + accountViewModel = mockAccountViewModel(), + nav = EmptyNav(), + ) + }, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun NoPaddingSuggestionChip( + label: String, + modifier: Modifier = Modifier, +) { + Surface( + shape = MaterialTheme.shapes.extraSmall, // Use a small shape for chip look + color = MaterialTheme.colorScheme.secondaryContainer, // Default chip color + modifier = modifier, + ) { + Text( + text = label, + // Apply desired internal padding to the Text itself + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSecondaryContainer, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt index 6cf7aea7d..9ee0b0632 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt @@ -37,6 +37,7 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent @@ -392,6 +393,7 @@ val DEFAULT_FEED_KINDS = LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND, WikiNoteEvent.KIND, + NipTextEvent.KIND, InteractiveStoryPrologueEvent.KIND, ) @@ -406,6 +408,7 @@ val DEFAULT_COMMUNITY_FEEDS = AudioTrackEvent.KIND, PinListEvent.KIND, WikiNoteEvent.KIND, + NipTextEvent.KIND, CommunityPostApprovalEvent.KIND, InteractiveStoryPrologueEvent.KIND, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt index ac8b756a2..ca20492aa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt @@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent @@ -49,12 +50,13 @@ class FollowPackFeedNewThreadFeedFilter( val followPackNote: AddressableNote, val account: Account, ) : AdditiveFeedFilter() { - companion object Companion { + companion object { val ADDRESSABLE_KINDS = listOf( AudioTrackEvent.KIND, InteractiveStoryPrologueEvent.KIND, WikiNoteEvent.KIND, + NipTextEvent.KIND, ClassifiedsEvent.KIND, LongTextNoteEvent.KIND, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterPostsByHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterPostsByHashtags.kt index 5b0c37659..307162570 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterPostsByHashtags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterPostsByHashtags.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip22Commen import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter @@ -58,6 +59,7 @@ val PostsByHashtagKinds2 = InteractiveStorySceneEvent.KIND, AudioTrackEvent.KIND, AudioHeaderEvent.KIND, + NipTextEvent.KIND, ) fun filterPostsByHashtags( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt index 8e9eddd2b..b7c3ef37c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip22Comments.filterHomePostsByScopes import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -52,6 +53,7 @@ val HomePostsBuHashtagsKinds = InteractiveStoryPrologueEvent.KIND, CommentEvent.KIND, WikiNoteEvent.KIND, + NipTextEvent.KIND, VoiceEvent.KIND, VoiceReplyEvent.KIND, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt index 46bebbc44..5ac4955e9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthors import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter @@ -53,6 +54,7 @@ val HomePostsNewThreadKinds = LongTextNoteEvent.KIND, HighlightEvent.KIND, WikiNoteEvent.KIND, + NipTextEvent.KIND, PollNoteEvent.KIND, InteractiveStoryPrologueEvent.KIND, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt index a914febf5..b96effdd4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip72Commu import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -41,6 +42,7 @@ val HomePostsFromCommunityKinds = ClassifiedsEvent.KIND, HighlightEvent.KIND, WikiNoteEvent.KIND, + NipTextEvent.KIND, CommunityPostApprovalEvent.KIND, CommentEvent.KIND, InteractiveStoryPrologueEvent.KIND, @@ -53,6 +55,7 @@ val HomePostsFromCommunityKindsStr = ClassifiedsEvent.KIND.toString(), HighlightEvent.KIND.toString(), WikiNoteEvent.KIND.toString(), + NipTextEvent.KIND.toString(), CommunityPostApprovalEvent.KIND.toString(), CommentEvent.KIND.toString(), InteractiveStoryPrologueEvent.KIND.toString(), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt index fac9b7661..d07a6f284 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt @@ -31,6 +31,7 @@ import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.forks.forkFromVersion import com.vitorpamplona.quartz.experimental.forks.isForkFromAddressWithPubkey +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser @@ -77,6 +78,7 @@ class NotificationFeedFilter( listOf( AudioTrackEvent.KIND, WikiNoteEvent.KIND, + NipTextEvent.KIND, ClassifiedsEvent.KIND, LongTextNoteEvent.KIND, CalendarTimeSlotEvent.KIND, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt index dbbd86c0e..991ed0f80 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter @@ -58,6 +59,7 @@ val UserProfilePostKinds2 = listOf( TorrentEvent.KIND, TorrentCommentEvent.KIND, + NipTextEvent.KIND, InteractiveStoryPrologueEvent.KIND, CommentEvent.KIND, VoiceReplyEvent.KIND, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt index 22a90a03e..dbb4981bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt @@ -30,6 +30,7 @@ import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent @@ -76,6 +77,7 @@ class UserProfileMutualFeedFilter( it.event is GenericRepostEvent || it.event is LongTextNoteEvent || it.event is WikiNoteEvent || + it.event is NipTextEvent || it.event is PollNoteEvent || it.event is HighlightEvent || it.event is InteractiveStoryPrologueEvent || diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt index 3b4b1b727..4aadd0277 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt @@ -30,6 +30,7 @@ import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent @@ -77,6 +78,7 @@ class UserProfileNewThreadFeedFilter( it.event is GenericRepostEvent || it.event is LongTextNoteEvent || it.event is WikiNoteEvent || + it.event is NipTextEvent || it.event is PollNoteEvent || it.event is HighlightEvent || it.event is InteractiveStoryPrologueEvent || diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index e3f5068b9..b48551417 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -191,6 +191,7 @@ import com.vitorpamplona.quartz.experimental.forks.forkFromAddress import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.core.Event @@ -545,6 +546,7 @@ private fun FullBleedNoteCompose( is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote, accountViewModel) is LongTextNoteEvent -> RenderLongFormHeaderForThread(noteEvent, baseNote, accountViewModel) is WikiNoteEvent -> RenderWikiHeaderForThread(noteEvent, accountViewModel, nav) + is NipTextEvent -> RenderNipHeaderForThread(noteEvent, accountViewModel, nav) is ClassifiedsEvent -> RenderClassifiedsReaderForThread(noteEvent, baseNote, accountViewModel, nav) } @@ -1157,3 +1159,24 @@ private fun RenderWikiHeaderForThread( } } } + +@Composable +private fun RenderNipHeaderForThread( + noteEvent: NipTextEvent, + accountViewModel: AccountViewModel, + nav: INav, +) { + val forkedAddress = remember(noteEvent) { noteEvent.forkFromAddress() } + + Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp)) { + Column { + forkedAddress?.let { + LoadAddressableNote(it, accountViewModel) { originalVersion -> + if (originalVersion != null) { + ForkInformationRow(originalVersion, Modifier.fillMaxWidth(), accountViewModel, nav) + } + } + } + } + } +} diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index d033e2736..e9b34537c 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1465,4 +1465,5 @@ Delete List Delete Pack + Kinds diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/IForkableEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/IForkableEvent.kt new file mode 100644 index 000000000..47fddb817 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/IForkableEvent.kt @@ -0,0 +1,4 @@ +package com.vitorpamplona.quartz.experimental.forks + +class IForkableEvent { +} \ No newline at end of file diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/NipTextEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/NipTextEvent.kt new file mode 100644 index 000000000..f00e84c3a --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/NipTextEvent.kt @@ -0,0 +1,151 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.nipsOnNostr + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.experimental.forks.parseFork +import com.vitorpamplona.quartz.experimental.nipsOnNostr.tags.ForkTag +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag +import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag +import com.vitorpamplona.quartz.nip01Core.tags.kinds.kinds +import com.vitorpamplona.quartz.nip10Notes.BaseNoteEvent +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag +import com.vitorpamplona.quartz.nip19Bech32.addressHints +import com.vitorpamplona.quartz.nip19Bech32.addressIds +import com.vitorpamplona.quartz.nip19Bech32.eventHints +import com.vitorpamplona.quartz.nip19Bech32.eventIds +import com.vitorpamplona.quartz.nip22Comments.RootScope +import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip50Search.SearchableEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +@Immutable +class NipTextEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig), + AddressableEvent, + RootScope, + EventHintProvider, + AddressHintProvider, + SearchableEvent { + override fun indexableContent() = "title: " + title() + "\n" + content + + override fun dTag() = tags.dTag() + + override fun aTag(relayHint: NormalizedRelayUrl?) = ATag(kind, pubKey, dTag(), relayHint) + + override fun address() = Address(kind, pubKey, dTag()) + + override fun addressTag() = Address.assemble(kind, pubKey, dTag()) + + override fun eventHints(): List { + val qHints = tags.mapNotNull(QTag::parseEventAsHint) + val nip19Hints = citedNIP19().eventHints() + + return qHints + nip19Hints + } + + override fun linkedEventIds(): List { + val qHints = tags.mapNotNull(QTag::parseEventId) + val nip19Hints = citedNIP19().eventIds() + + return qHints + nip19Hints + } + + override fun addressHints(): List { + val aHints = tags.mapNotNull(ATag::parseAsHint) + val qHints = tags.mapNotNull(QTag::parseAddressAsHint) + val nip19Hints = citedNIP19().addressHints() + + return aHints + qHints + nip19Hints + } + + override fun linkedAddressIds(): List { + val aHints = tags.mapNotNull(ATag::parseAddressId) + val qHints = tags.mapNotNull(QTag::parseAddressId) + val nip19Hints = citedNIP19().addressIds() + + return aHints + qHints + nip19Hints + } + + fun isAFork() = tags.any { it.size > 3 && (it[0] == "a" || it[0] == "e") && it[3] == "fork" } + + fun forkFromAddress() = tags.firstNotNullOfOrNull(ForkTag::parseAddress) + + fun forkFromVersion() = tags.firstNotNullOfOrNull(MarkedETag::parseFork) + + fun title() = tags.firstNotNullOfOrNull(TitleTag::parse) + + fun summary() = + content.lineSequence().drop(1).firstNotNullOfOrNull { + if (it.isBlank() || + it.startsWith("---") || + it.startsWith("`draft") || + it.startsWith("#") + ) { + null + } else { + it + } + } + + companion object { + const val KIND = 30817 + const val KIND_STR = "30817" + + @OptIn(ExperimentalUuidApi::class) + fun build( + description: String, + title: String, + kinds: List, + dTag: String = Uuid.random().toString(), + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, description, createdAt) { + dTag(dTag) + alt("NIP: $title") + + title(title) + kinds(kinds) + + initializer() + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/TagArrayBuilderExt.kt new file mode 100644 index 000000000..f77989cfa --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/TagArrayBuilderExt.kt @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.nipsOnNostr + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.tags.kinds.KindTag +import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag + +fun TagArrayBuilder.title(title: String) = addUnique(TitleTag.assemble(title)) + +fun TagArrayBuilder.kinds(kinds: List) = addAll(KindTag.assemble(kinds)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/tags/ForkTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/tags/ForkTag.kt new file mode 100644 index 000000000..27f9547fb --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/tags/ForkTag.kt @@ -0,0 +1,145 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.nipsOnNostr.tags + +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import com.vitorpamplona.quartz.utils.arrayOfNotNull +import com.vitorpamplona.quartz.utils.ensure + +class ForkTag( + val address: Address, + val relayHint: NormalizedRelayUrl? = null, +) { + fun toTag() = Address.assemble(address.kind, address.pubKeyHex, address.dTag) + + fun toTagArray() = assemble(address, relayHint) + + fun toTagIdOnly() = assemble(address, null) + + companion object { + const val TAG_NAME = "a" + + fun isTagged(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && Address.isOfKind(tag[1], NipTextEvent.KIND_STR) + + fun isTagged( + tag: Array, + addressId: String, + ) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == addressId + + fun isTagged( + tag: Array, + address: ForkTag, + ) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == address.toTag() + + fun isIn( + tag: Array, + addressIds: Set, + ) = tag.has(1) && tag[0] == TAG_NAME && tag[1] in addressIds + + fun parse(tag: Array): ForkTag? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure( + Address.Companion.isOfKind( + tag[1], + CommunityDefinitionEvent.Companion.KIND_STR, + ), + ) { return null } + + val address = Address.Companion.parse(tag[1]) ?: return null + val relayHint = tag.getOrNull(2)?.let { RelayUrlNormalizer.Companion.normalizeOrNull(it) } + return ForkTag(address, relayHint) + } + + fun parseValidAddress(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure( + Address.Companion.isOfKind( + tag[1], + CommunityDefinitionEvent.Companion.KIND_STR, + ), + ) { return null } + return Address.Companion.parse(tag[1])?.toValue() + } + + fun parseAddress(tag: Array): Address? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + val address = Address.parse(tag[1]) ?: return null + ensure(address.kind == NipTextEvent.KIND) { return null } + return address + } + + fun parseAddressId(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure( + Address.isOfKind( + tag[1], + NipTextEvent.KIND_STR, + ), + ) { return null } + return tag[1] + } + + fun parseAsHint(tag: Array): AddressHint? { + ensure(tag.has(2)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure( + Address.isOfKind( + tag[1], + NipTextEvent.KIND_STR, + ), + ) { return null } + ensure(tag[2].isNotEmpty()) { return null } + + val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + ensure(relayHint != null) { return null } + + return AddressHint(tag[1], relayHint) + } + + fun assemble( + aTagId: String, + relay: NormalizedRelayUrl?, + ) = arrayOfNotNull(TAG_NAME, aTagId, relay?.url, "fork") + + fun assemble( + address: Address, + relay: NormalizedRelayUrl?, + ) = assemble(address.toValue(), relay) + + fun assemble( + kind: Int, + pubKey: String, + dTag: String, + relay: NormalizedRelayUrl?, + ) = assemble(Address.assemble(kind, pubKey, dTag), relay) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/BaseThreadedEventExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/tags/TitleTag.kt similarity index 57% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/BaseThreadedEventExt.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/tags/TitleTag.kt index 0d38a4f5b..aa57f067d 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/BaseThreadedEventExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/tags/TitleTag.kt @@ -18,21 +18,22 @@ * 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.experimental.forks +package com.vitorpamplona.quartz.experimental.nipsOnNostr.tags -import com.vitorpamplona.quartz.nip01Core.core.Address -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent -import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure -fun BaseThreadedEvent.isAFork() = tags.any { it.size > 3 && (it[0] == "a" || it[0] == "e") && it[3] == "fork" } +class TitleTag { + companion object { + const val TAG_NAME = "title" -fun BaseThreadedEvent.forkFromAddress() = - tags.firstOrNull { it.size > 3 && it[0] == "a" && it[3] == "fork" }?.let { - val aTagValue = it[1] - Address.parse(aTagValue) + fun parse(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + fun assemble(title: String) = arrayOf(TAG_NAME, title) } - -fun BaseThreadedEvent.forkFromVersion() = tags.firstNotNullOfOrNull(MarkedETag::parseFork) - -fun BaseThreadedEvent.isForkFromAddressWithPubkey(authorHex: HexKey) = tags.any { it.size > 3 && it[0] == "a" && it[3] == "fork" && it[1].contains(authorHex) } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt index e58c8d1ce..b9fb1a08c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.quartz.nip10Notes import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedATags import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers @@ -79,7 +80,7 @@ open class BaseThreadedEvent( val tagAddresses = taggedATags() .filter { aTag -> - aTag.kind != CommunityDefinitionEvent.KIND && (kind != WikiNoteEvent.KIND || aTag.kind != WikiNoteEvent.KIND) + aTag.kind != CommunityDefinitionEvent.KIND && (kind != WikiNoteEvent.KIND || aTag.kind != WikiNoteEvent.KIND) && (kind != NipTextEvent.KIND || aTag.kind != NipTextEvent.KIND) // removes forks from itself. }.map { it.toTag() diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/TagArrayBuilderExt.kt new file mode 100644 index 000000000..f460e981e --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/TagArrayBuilderExt.kt @@ -0,0 +1,13 @@ +package com.vitorpamplona.quartz.nip54Wiki + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag + +fun TagArrayBuilder.title(title: String) = addUnique(TitleTag.assemble(title)) + +fun TagArrayBuilder.summary(summary: String) = addUnique(SummaryTag.assemble(summary)) + +fun TagArrayBuilder.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl)) + +fun TagArrayBuilder.publishedAt(publishedAt: Long) = addUnique(PublishedAtTag.assemble(publishedAt)) \ No newline at end of file diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt index ebc6d3a16..e2908f2b4 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt @@ -131,9 +131,7 @@ class WikiNoteEvent( fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1) override fun publishedAt(): Long? { - val publishedAt = tags.firstNotNullOfOrNull(PublishedAtTag::parse) - - if (publishedAt == null) return null + val publishedAt = tags.firstNotNullOfOrNull(PublishedAtTag::parse) ?: return null // removes posts in the future. return if (publishedAt <= createdAt) { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt index ef11c9d3f..2d6162696 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt @@ -32,6 +32,7 @@ import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.nns.NNSEvent import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent @@ -266,6 +267,7 @@ class EventFactory { MetadataEvent.KIND -> MetadataEvent(id, pubKey, createdAt, tags, content, sig) MuteListEvent.KIND -> MuteListEvent(id, pubKey, createdAt, tags, content, sig) NNSEvent.KIND -> NNSEvent(id, pubKey, createdAt, tags, content, sig) + NipTextEvent.KIND -> NipTextEvent(id, pubKey, createdAt, tags, content, sig) NostrConnectEvent.KIND -> NostrConnectEvent(id, pubKey, createdAt, tags, content, sig) NIP90StatusEvent.KIND -> NIP90StatusEvent(id, pubKey, createdAt, tags, content, sig) NIP90ContentDiscoveryRequestEvent.KIND -> NIP90ContentDiscoveryRequestEvent(id, pubKey, createdAt, tags, content, sig) From 1ead2983ac52d253b1d204a17bd4f3c3e6d94dff Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 10 Jan 2026 11:36:11 -0500 Subject: [PATCH 126/137] Moves to a fork model via interfaces to start creating specific screens for each event type --- .../amethyst/model/LocalCache.kt | 1 - .../amethyst/ui/note/NoteCompose.kt | 5 +- .../amethyst/ui/note/ReactionsRow.kt | 25 ++++--- .../amethyst/ui/note/elements/ForkInfo.kt | 8 +-- .../dal/NotificationFeedFilter.kt | 27 +++++--- .../experimental/forks/IForkableEvent.kt | 34 +++++++++- .../experimental/forks/MarkedETagExt.kt | 8 +++ .../experimental/nipsOnNostr/NipTextEvent.kt | 10 +-- .../quartz/nip10Notes/TextNoteEvent.kt | 10 +++ .../quartz/nip54Wiki/TagArrayBuilderExt.kt | 32 +++++++-- .../quartz/nip54Wiki/WikiNoteEvent.kt | 68 +++++++++++++------ 11 files changed, 169 insertions(+), 59 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 0bad1548b..ad2e47c4e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -805,7 +805,6 @@ object LocalCache : ILocalCache { fun computeReplyTo(event: Event): List = when (event) { is PollNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } - is WikiNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } is LongTextNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } is GitReplyEvent -> event.tagsWithoutCitations().filter { it != event.repository()?.toTag() }.mapNotNull { checkGetOrCreateNote(it) } is TextNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index d707d87fc..7956f2a8d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -164,7 +164,7 @@ import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.bounties.bountyBaseReward import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent -import com.vitorpamplona.quartz.experimental.forks.isAFork +import com.vitorpamplona.quartz.experimental.forks.IForkableEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent @@ -173,7 +173,6 @@ import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.tags.geohash.geoHashOrScope import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent -import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip13Pow.strongPoWOrNull import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent @@ -1088,7 +1087,7 @@ fun SecondUserInfoRow( modifier = UserNameMaxRowHeight, ) { Column(modifier = remember(noteEvent) { Modifier.weight(1f) }) { - if (noteEvent is BaseThreadedEvent && noteEvent.isAFork()) { + if (noteEvent is IForkableEvent && noteEvent.isAFork()) { ShowForkInformation(noteEvent, remember(noteEvent) { Modifier.weight(1f) }, accountViewModel, nav) } else { ObserveDisplayNip05Status(noteAuthor, accountViewModel, nav) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 279a3414f..a2814a464 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -159,6 +159,7 @@ import com.vitorpamplona.amethyst.ui.theme.reactionBox import com.vitorpamplona.amethyst.ui.theme.ripple24dp import com.vitorpamplona.amethyst.ui.theme.selectedReactionBoxModifier import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount @@ -1394,16 +1395,20 @@ private fun BoostTypeChoicePopup( Text(stringRes(R.string.quote), color = Color.White, textAlign = TextAlign.Center) } - Button( - modifier = Modifier.padding(horizontal = 3.dp), - onClick = onFork, - shape = ButtonBorder, - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - ), - ) { - Text(stringRes(R.string.fork), color = Color.White, textAlign = TextAlign.Center) + // removes the option to fork for now because we do not have screens for + // LongForm, Wiki and NIP posting. + if (baseNote.event is TextNoteEvent) { + Button( + modifier = Modifier.padding(horizontal = 3.dp), + onClick = onFork, + shape = ButtonBorder, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + ), + ) { + Text(stringRes(R.string.fork), color = Color.White, textAlign = TextAlign.Center) + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt index 6ad3d4bad..2c52fdaa7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt @@ -47,13 +47,11 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.nip05 -import com.vitorpamplona.quartz.experimental.forks.forkFromAddress -import com.vitorpamplona.quartz.experimental.forks.forkFromVersion -import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.experimental.forks.IForkableEvent @Composable fun ShowForkInformation( - noteEvent: BaseThreadedEvent, + noteEvent: IForkableEvent, modifier: Modifier, accountViewModel: AccountViewModel, nav: INav, @@ -67,7 +65,7 @@ fun ShowForkInformation( } } } else if (forkedEvent != null) { - LoadNote(forkedEvent.eventId, accountViewModel) { event -> + LoadNote(forkedEvent, accountViewModel) { event -> if (event != null) { ForkInformationRowLightColor(event, modifier, accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt index d07a6f284..b8a5ba010 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt @@ -29,14 +29,13 @@ import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent -import com.vitorpamplona.quartz.experimental.forks.forkFromVersion -import com.vitorpamplona.quartz.experimental.forks.isForkFromAddressWithPubkey +import com.vitorpamplona.quartz.experimental.forks.IForkableEvent import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent -import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip10Notes.BaseNoteEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent @@ -187,7 +186,7 @@ class NotificationFeedFilter( return true } - if (event is BaseThreadedEvent) { + if (event is BaseNoteEvent) { if (note.replyTo?.any { it.author?.pubkeyHex == authorHex } == true) { return true } @@ -210,11 +209,23 @@ class NotificationFeedFilter( } val isAuthoredPostCited = event.findCitations().any { LocalCache.getNoteIfExists(it)?.author?.pubkeyHex == authorHex } - val isAuthorDirectlyCited = event.citedUsers().contains(authorHex) - val isAuthorOfAFork = - event.isForkFromAddressWithPubkey(authorHex) || (event.forkFromVersion()?.let { LocalCache.getNoteIfExists(it.eventId)?.author?.pubkeyHex == authorHex } == true) - return isAuthoredPostCited || isAuthorDirectlyCited || isAuthorOfAFork + if (isAuthoredPostCited) return true + + val isAuthorDirectlyCited = event.citedUsers().contains(authorHex) + + if (isAuthorDirectlyCited) return true + + return if (event is IForkableEvent && event.isAFork()) { + val address = event.forkFromAddress() + val version = event.forkFromVersion() + + // Displays notifications about forks + address?.pubKeyHex == authorHex || + (version?.let { LocalCache.getNoteIfExists(it)?.author?.pubkeyHex == authorHex } == true) + } else { + false + } } if (event is ReactionEvent) { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/IForkableEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/IForkableEvent.kt index 47fddb817..e614cf866 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/IForkableEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/IForkableEvent.kt @@ -1,4 +1,34 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ package com.vitorpamplona.quartz.experimental.forks -class IForkableEvent { -} \ No newline at end of file +import androidx.compose.runtime.Stable +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip01Core.core.HexKey + +@Stable +interface IForkableEvent { + fun isAFork(): Boolean + + fun forkFromAddress(): Address? + + fun forkFromVersion(): HexKey? +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/MarkedETagExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/MarkedETagExt.kt index cc2971896..714404777 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/MarkedETagExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/MarkedETagExt.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.quartz.experimental.forks +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag.MARKER @@ -37,3 +38,10 @@ fun MarkedETag.Companion.parseFork(tag: Array): MarkedETag? { ), ) } + +fun MarkedETag.Companion.parseForkedEventId(tag: Array): HexKey? { + if (tag.size < 4 || tag[0] != "e") return null + if (tag[ORDER_MARKER] != MARKER.FORK.code) return null + // ["e", id hex, relay hint, marker, pubkey] + return tag[ORDER_EVT_ID] +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/NipTextEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/NipTextEvent.kt index f00e84c3a..07581a783 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/NipTextEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/NipTextEvent.kt @@ -21,7 +21,8 @@ package com.vitorpamplona.quartz.experimental.nipsOnNostr import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.experimental.forks.parseFork +import com.vitorpamplona.quartz.experimental.forks.IForkableEvent +import com.vitorpamplona.quartz.experimental.forks.parseForkedEventId import com.vitorpamplona.quartz.experimental.nipsOnNostr.tags.ForkTag import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent @@ -64,6 +65,7 @@ class NipTextEvent( RootScope, EventHintProvider, AddressHintProvider, + IForkableEvent, SearchableEvent { override fun indexableContent() = "title: " + title() + "\n" + content @@ -105,11 +107,11 @@ class NipTextEvent( return aHints + qHints + nip19Hints } - fun isAFork() = tags.any { it.size > 3 && (it[0] == "a" || it[0] == "e") && it[3] == "fork" } + override fun isAFork() = tags.any { it.size > 3 && (it[0] == "a" || it[0] == "e") && it[3] == "fork" } - fun forkFromAddress() = tags.firstNotNullOfOrNull(ForkTag::parseAddress) + override fun forkFromAddress() = tags.firstNotNullOfOrNull(ForkTag::parseAddress) - fun forkFromVersion() = tags.firstNotNullOfOrNull(MarkedETag::parseFork) + override fun forkFromVersion() = tags.firstNotNullOfOrNull(MarkedETag::parseForkedEventId) fun title() = tags.firstNotNullOfOrNull(TitleTag::parse) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt index f50a5dd48..b8ac14da8 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt @@ -21,6 +21,8 @@ package com.vitorpamplona.quartz.nip10Notes import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.experimental.forks.IForkableEvent +import com.vitorpamplona.quartz.experimental.forks.parseForkedEventId import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider @@ -47,6 +49,7 @@ import com.vitorpamplona.quartz.nip19Bech32.pubKeys import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip50Search.SearchableEvent import com.vitorpamplona.quartz.utils.TimeUtils +import kotlinx.serialization.json.JsonNull.content @Immutable class TextNoteEvent( @@ -60,6 +63,7 @@ class TextNoteEvent( EventHintProvider, AddressHintProvider, PubKeyHintProvider, + IForkableEvent, SearchableEvent { override fun indexableContent() = "Subject: " + subject() + "\n" + content @@ -109,6 +113,12 @@ class TextNoteEvent( return pHints + nip19Hints } + override fun isAFork() = tags.any { it.size > 3 && (it[0] == "a" || it[0] == "e") && it[3] == "fork" } + + override fun forkFromAddress() = tags.firstNotNullOfOrNull(ATag::parseAddress) + + override fun forkFromVersion() = tags.firstNotNullOfOrNull(MarkedETag::parseForkedEventId) + companion object { const val KIND = 1 const val ALT = "A short note: " diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/TagArrayBuilderExt.kt index f460e981e..f6715d8fd 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/TagArrayBuilderExt.kt @@ -1,13 +1,35 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ package com.vitorpamplona.quartz.nip54Wiki import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag +import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag +import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag -fun TagArrayBuilder.title(title: String) = addUnique(TitleTag.assemble(title)) +fun TagArrayBuilder.title(title: String) = addUnique(TitleTag.assemble(title)) -fun TagArrayBuilder.summary(summary: String) = addUnique(SummaryTag.assemble(summary)) +fun TagArrayBuilder.summary(summary: String) = addUnique(SummaryTag.assemble(summary)) -fun TagArrayBuilder.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl)) +fun TagArrayBuilder.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl)) -fun TagArrayBuilder.publishedAt(publishedAt: Long) = addUnique(PublishedAtTag.assemble(publishedAt)) \ No newline at end of file +fun TagArrayBuilder.publishedAt(publishedAt: Long) = addUnique(PublishedAtTag.assemble(publishedAt)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt index e2908f2b4..e81c56fbe 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt @@ -21,9 +21,13 @@ package com.vitorpamplona.quartz.nip54Wiki import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.experimental.forks.IForkableEvent +import com.vitorpamplona.quartz.experimental.forks.parseForkedEventId +import com.vitorpamplona.quartz.experimental.nipsOnNostr.tags.ForkTag import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider @@ -31,13 +35,14 @@ import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.publishedAt.PublishedAtProvider -import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip10Notes.BaseNoteEvent import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag import com.vitorpamplona.quartz.nip19Bech32.addressHints @@ -46,10 +51,16 @@ import com.vitorpamplona.quartz.nip19Bech32.eventHints import com.vitorpamplona.quartz.nip19Bech32.eventIds import com.vitorpamplona.quartz.nip19Bech32.pubKeyHints import com.vitorpamplona.quartz.nip19Bech32.pubKeys +import com.vitorpamplona.quartz.nip22Comments.RootScope +import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag -import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag +import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip50Search.SearchableEvent import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid @Immutable class WikiNoteEvent( @@ -59,12 +70,14 @@ class WikiNoteEvent( tags: Array>, content: String, sig: HexKey, -) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig), +) : BaseNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig), AddressableEvent, EventHintProvider, AddressHintProvider, PubKeyHintProvider, PublishedAtProvider, + IForkableEvent, + RootScope, SearchableEvent { override fun indexableContent() = "title: " + title() + "\nsummary: " + summary() + "\n" + content @@ -124,11 +137,17 @@ class WikiNoteEvent( fun topics() = hashtags() - fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1) + fun title() = tags.firstNotNullOfOrNull(TitleTag::parse) - fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1) + fun image() = tags.firstNotNullOfOrNull(ImageTag::parse) - fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1) + fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse) + + override fun isAFork() = tags.any { it.size > 3 && (it[0] == "a" || it[0] == "e") && it[3] == "fork" } + + override fun forkFromAddress() = tags.firstNotNullOfOrNull(ForkTag::parseAddress) + + override fun forkFromVersion() = tags.firstNotNullOfOrNull(MarkedETag::parseForkedEventId) override fun publishedAt(): Long? { val publishedAt = tags.firstNotNullOfOrNull(PublishedAtTag::parse) ?: return null @@ -144,20 +163,27 @@ class WikiNoteEvent( companion object { const val KIND = 30818 - suspend fun create( - msg: String, - title: String?, - replyTos: List?, - mentions: List?, - signer: NostrSigner, + @OptIn(ExperimentalUuidApi::class) + fun build( + description: String, + title: String, + summary: String? = null, + image: String? = null, + publishedAt: Long? = null, + dTag: String = Uuid.random().toString(), createdAt: Long = TimeUtils.now(), - ): WikiNoteEvent { - val tags = mutableListOf>() - replyTos?.forEach { tags.add(arrayOf("e", it)) } - mentions?.forEach { tags.add(arrayOf("p", it)) } - title?.let { tags.add(arrayOf("title", it)) } - tags.add(AltTag.assemble("Wiki Post: $title")) - return signer.sign(createdAt, KIND, tags.toTypedArray(), msg) - } + initializer: TagArrayBuilder.() -> Unit = {}, + ): EventTemplate = + eventTemplate(KIND, description, createdAt) { + dTag(dTag) + alt("Wiki entry: $title") + + title(title) + summary?.let { summary(it) } + image?.let { image(it) } + publishedAt?.let { publishedAt(it) } + + initializer() + } } } From 3c12be354e4bead5a5b6438e49cec5e02ade4414 Mon Sep 17 00:00:00 2001 From: davotoula Date: Fri, 9 Jan 2026 11:51:42 +0100 Subject: [PATCH 127/137] change from "hold to record" to "click to start, click to stop" --- .../ui/actions/uploads/RecordAudio.kt | 69 ++++---- .../amethyst/ui/components/ClickableBox.kt | 150 ++++-------------- 2 files changed, 70 insertions(+), 149 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt index c5083fb12..32af08e0b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt @@ -37,7 +37,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.components.ClickAndHoldBoxComposable +import com.vitorpamplona.amethyst.ui.components.ToggleableBox import com.vitorpamplona.amethyst.ui.stringRes import kotlinx.coroutines.delay import kotlinx.coroutines.isActive @@ -52,15 +52,16 @@ fun RecordAudioBox( val mediaRecorder = remember { mutableStateOf(null) } val context = LocalContext.current var elapsedSeconds by remember { mutableIntStateOf(0) } - var wantsToRecord by remember { mutableStateOf(false) } + var pendingPermissionStart by remember { mutableStateOf(false) } // Must be called at Composable scope, not in callback val recordPermissionState = rememberPermissionState(Manifest.permission.RECORD_AUDIO) val scope = rememberCoroutineScope() + val isRecording = mediaRecorder.value != null + DisposableEffect(Unit) { onDispose { - wantsToRecord = false mediaRecorder.value?.stop() mediaRecorder.value = null } @@ -74,8 +75,25 @@ fun RecordAudioBox( } } - LaunchedEffect(recordPermissionState.status.isGranted, wantsToRecord) { - if (recordPermissionState.status.isGranted && wantsToRecord) { + fun stopRecording() { + val result = mediaRecorder.value?.stop() + mediaRecorder.value = null + if (result != null) { + onRecordTaken(result) + } else { + Toast + .makeText( + context, + stringRes(context, R.string.record_a_message_description), + Toast.LENGTH_SHORT, + ).show() + } + } + + // Start recording after permission is granted + LaunchedEffect(recordPermissionState.status.isGranted) { + if (recordPermissionState.status.isGranted && pendingPermissionStart) { + pendingPermissionStart = false startRecording() } } @@ -96,38 +114,21 @@ fun RecordAudioBox( } } - ClickAndHoldBoxComposable( + ToggleableBox( modifier = modifier, - onPress = { - wantsToRecord = true - if (!recordPermissionState.status.isGranted) { - recordPermissionState.launchPermissionRequest() + isActive = isRecording, + onClick = { + if (isRecording) { + stopRecording() } else { - // Start immediately for responsive UX when permission already granted - startRecording() + if (!recordPermissionState.status.isGranted) { + pendingPermissionStart = true + recordPermissionState.launchPermissionRequest() + } else { + startRecording() + } } }, - onRelease = { - wantsToRecord = false - val result = mediaRecorder.value?.stop() - mediaRecorder.value = null - if (result != null) { - onRecordTaken(result) - } else { - // less disruptive than error messages - Toast - .makeText( - context, - stringRes(context, R.string.record_a_message_description), - Toast.LENGTH_SHORT, - ).show() - } - }, - onCancel = { - wantsToRecord = false - mediaRecorder.value?.stop() - mediaRecorder.value = null - }, - content = @Composable { isRecording -> content(isRecording, elapsedSeconds) }, + content = { active -> content(active, elapsedSeconds) }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableBox.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableBox.kt index 6df79b5cd..a63656d4b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableBox.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableBox.kt @@ -28,17 +28,12 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.PressInteraction -import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale @@ -64,116 +59,6 @@ fun ClickableBox( } } -@Composable -fun ClickAndHoldBox( - modifier: Modifier = Modifier, - onPress: () -> Unit, - onRelease: () -> Unit, - content: @Composable (Boolean) -> Unit, -) { - val interactionSource = remember { MutableInteractionSource() } - val isPressed by interactionSource.collectIsPressedAsState() - - LaunchedEffect(isPressed) { - if (isPressed) { - // Button is pressed - onPress() - } else { - // Button is released - onRelease() - } - } - - // Animation for the button scale - val scale by animateFloatAsState( - targetValue = if (isPressed) 1.5f else 1.0f, // Scale up when recording - animationSpec = tween(durationMillis = 150), // Smooth animation - ) - - // Animation for the button color - val backgroundColor by animateColorAsState( - targetValue = if (isPressed) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.background, - animationSpec = tween(durationMillis = 150), - ) - - Box( - modifier - .scale(scale) - .background(backgroundColor, CircleShape) - .clickable( - role = Role.Button, - interactionSource = interactionSource, - indication = ripple24dp, - onClick = { }, - ), - contentAlignment = Alignment.Center, - ) { - content(isPressed) - } -} - -@Composable -fun ClickAndHoldBoxComposable( - modifier: Modifier = Modifier, - onPress: () -> Unit, - onRelease: suspend () -> Unit, - onCancel: suspend () -> Unit, - content: @Composable (Boolean) -> Unit, -) { - val interactionSource = remember { MutableInteractionSource() } - var isPressed by remember { mutableStateOf(false) } - - LaunchedEffect(interactionSource) { - val pressInteractions = mutableListOf() - interactionSource.interactions.collect { interaction -> - when (interaction) { - is PressInteraction.Press -> { - if (pressInteractions.isEmpty()) { - onPress() - } - pressInteractions.add(interaction) - } - is PressInteraction.Release -> { - onRelease() - pressInteractions.remove(interaction.press) - } - is PressInteraction.Cancel -> { - onCancel() - pressInteractions.remove(interaction.press) - } - } - isPressed = pressInteractions.isNotEmpty() - } - } - - // Animation for the button scale - val scale by animateFloatAsState( - targetValue = if (isPressed) 1.5f else 1.0f, // Scale up when recording - animationSpec = tween(durationMillis = 150), // Smooth animation - ) - - // Animation for the button color - val backgroundColor by animateColorAsState( - targetValue = if (isPressed) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.background, - animationSpec = tween(durationMillis = 150), - ) - - Box( - modifier - .scale(scale) - .background(backgroundColor, CircleShape) - .clickable( - role = Role.Button, - interactionSource = interactionSource, - indication = ripple24dp, - onClick = { }, - ), - contentAlignment = Alignment.Center, - ) { - content(isPressed) - } -} - @OptIn(ExperimentalFoundationApi::class) @Composable fun ClickableBox( @@ -195,3 +80,38 @@ fun ClickableBox( content() } } + +@Composable +fun ToggleableBox( + modifier: Modifier = Modifier, + isActive: Boolean, + onClick: () -> Unit, + content: @Composable (Boolean) -> Unit, +) { + // Animation for the button scale + val scale by animateFloatAsState( + targetValue = if (isActive) 1.5f else 1.0f, + animationSpec = tween(durationMillis = 150), + ) + + // Animation for the button color + val backgroundColor by animateColorAsState( + targetValue = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.background, + animationSpec = tween(durationMillis = 150), + ) + + Box( + modifier + .scale(scale) + .background(backgroundColor, CircleShape) + .clickable( + role = Role.Button, + interactionSource = remember { MutableInteractionSource() }, + indication = ripple24dp, + onClick = onClick, + ), + contentAlignment = Alignment.Center, + ) { + content(isActive) + } +} From 524180bec9da4a4692476beb2f7c9e47367b2092 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 10 Jan 2026 16:41:22 +0100 Subject: [PATCH 128/137] change icon to stop icon --- .../amethyst/ui/actions/uploads/RecordingIndicators.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordingIndicators.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordingIndicators.kt index a4e3ecd65..0f7fb0946 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordingIndicators.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordingIndicators.kt @@ -35,7 +35,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.FiberManualRecord +import androidx.compose.material.icons.filled.Stop import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -206,8 +206,8 @@ fun FloatingRecordingIndicator( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = innerPadding), ) { - // Pulsing red dot - val infiniteTransition = rememberInfiniteTransition(label = "recording_dot") + // Pulsing stop square + val infiniteTransition = rememberInfiniteTransition(label = "recording_stop") val dotAlpha by infiniteTransition.animateFloat( initialValue = 1f, targetValue = 0.5f, @@ -220,7 +220,7 @@ fun FloatingRecordingIndicator( ) Icon( - imageVector = Icons.Default.FiberManualRecord, + imageVector = Icons.Default.Stop, contentDescription = recordingLabel, tint = Color.White, modifier = From 5faa439a8b9798051b1499f102987453a479a76c Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 10 Jan 2026 16:55:06 +0100 Subject: [PATCH 129/137] fix re-record button --- .../screen/loggedIn/home/VoiceReplyScreen.kt | 49 +++++++++++++------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt index da1dd4b1c..15f30bb19 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt @@ -35,6 +35,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Mic +import androidx.compose.material.icons.filled.Stop import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -53,6 +54,7 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow import com.vitorpamplona.amethyst.ui.actions.uploads.RecordAudioBox import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview +import com.vitorpamplona.amethyst.ui.actions.uploads.formatSecondsToTime import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.ui.note.NoteCompose @@ -212,29 +214,44 @@ private fun ReRecordButton(viewModel: VoiceReplyViewModel) { onRecordTaken = { recording -> viewModel.selectRecording(recording) }, - ) { isRecording, _ -> + ) { isRecording, elapsedSeconds -> + val contentColor = + if (isRecording) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onBackground + } + val icon = + if (isRecording) { + Icons.Default.Stop + } else { + Icons.Default.Mic + } + val label = + if (isRecording) { + formatSecondsToTime(elapsedSeconds) + } else { + stringRes(id = R.string.re_record) + } + val iconDescription = + if (isRecording) { + stringRes(id = R.string.recording_indicator_description) + } else { + stringRes(id = R.string.record_a_message) + } Row( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), ) { Icon( - imageVector = Icons.Default.Mic, - contentDescription = stringRes(id = R.string.record_a_message), - tint = - if (isRecording) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onBackground - }, + imageVector = icon, + contentDescription = iconDescription, + tint = contentColor, ) Text( - text = stringRes(id = R.string.re_record), - color = - if (isRecording) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onBackground - }, + text = label, + color = contentColor, ) } } From 7b6b0f66a5b449bbd08d7e9765df7c742dd3d54c Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 10 Jan 2026 17:49:02 +0100 Subject: [PATCH 130/137] guard for NaN crash with short recordings --- .../ui/components/AudioWaveformReadOnly.kt | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AudioWaveformReadOnly.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AudioWaveformReadOnly.kt index b6453b4cf..490352912 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AudioWaveformReadOnly.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AudioWaveformReadOnly.kt @@ -26,7 +26,6 @@ import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.requiredHeight -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf @@ -71,7 +70,6 @@ fun AudioWaveformReadOnly( progressBrush: Brush = SolidColor(Color.Blue), waveformAlignment: WaveformAlignment = WaveformAlignment.Center, amplitudeType: AmplitudeType = AmplitudeType.Avg, - onProgressChangeFinished: (() -> Unit)? = null, spikeAnimationSpec: AnimationSpec = tween(500), spikeWidth: Dp = 3.dp, spikeRadius: Dp = 2.dp, @@ -80,7 +78,6 @@ fun AudioWaveformReadOnly( amplitudes: List, onProgressChange: (Float) -> Unit, ) { - val backgroundColor = MaterialTheme.colorScheme.background val progressState = remember(progress) { progress.coerceIn(MIN_PROGRESS, MAX_PROGRESS) } val spikeWidthState = remember(spikeWidth) { spikeWidth.coerceIn(MinSpikeWidthDp, MaxSpikeWidthDp) } @@ -195,7 +192,20 @@ internal fun Iterable.chunkToSize( internal fun Iterable.normalize( min: Float, max: Float, -): List = map { (max - min) * ((it - min()) / (max() - min())) + min } +): List { + val values = toList() + if (values.isEmpty()) return emptyList() + + val currentMin = values.minOrNull() ?: return emptyList() + val currentMax = values.maxOrNull() ?: return emptyList() + val range = currentMax - currentMin + if (!range.isFinite() || range == 0f) { + return List(values.size) { min } + } + + val scale = max - min + return values.map { scale * ((it - currentMin) / range) + min } +} private fun Int.safeDiv(value: Int): Float { return if (value == 0) return 0F else this / value.toFloat() From d52906fda089343dc510767a90509db89e64c5e2 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 10 Jan 2026 17:58:25 +0100 Subject: [PATCH 131/137] remove unused imports --- .../com/vitorpamplona/amethyst/commons/ui/note/NoteCard.kt | 1 - .../com/vitorpamplona/amethyst/commons/util/TimeAgoFormatter.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCard.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCard.kt index 304929af4..67a70a500 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCard.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCard.kt @@ -28,7 +28,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.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/TimeAgoFormatter.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/TimeAgoFormatter.kt index 12307ecd2..43221805b 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/TimeAgoFormatter.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/TimeAgoFormatter.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.commons.util -import com.sun.org.apache.xalan.internal.lib.ExsltDatetime.time import com.vitorpamplona.quartz.utils.TimeUtils import java.text.SimpleDateFormat import java.util.Locale From cf0f71d04f78cbb859f33d8762b3d2eab4c6c89d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 10 Jan 2026 12:02:26 -0500 Subject: [PATCH 132/137] Fixes: https://github.com/vitorpamplona/amethyst/issues/1621 Renders the fork information at the second line for the Master Note as well. --- .../amethyst/ui/note/NoteCompose.kt | 2 +- .../amethyst/ui/note/elements/ForkInfo.kt | 64 ++----------------- .../loggedIn/threadview/ThreadFeedView.kt | 51 ++++----------- 3 files changed, 16 insertions(+), 101 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 7956f2a8d..aa5928c7f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -1088,7 +1088,7 @@ fun SecondUserInfoRow( ) { Column(modifier = remember(noteEvent) { Modifier.weight(1f) }) { if (noteEvent is IForkableEvent && noteEvent.isAFork()) { - ShowForkInformation(noteEvent, remember(noteEvent) { Modifier.weight(1f) }, accountViewModel, nav) + ShowForkInformation(noteEvent, Modifier, accountViewModel, nav) } else { ObserveDisplayNip05Status(noteAuthor, accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt index 2c52fdaa7..b36093573 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt @@ -21,32 +21,24 @@ package com.vitorpamplona.amethyst.ui.note.elements import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.style.TextOverflow import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUser -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji import com.vitorpamplona.amethyst.ui.components.LoadNote -import com.vitorpamplona.amethyst.ui.components.appendLink import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Font14SP -import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer -import com.vitorpamplona.amethyst.ui.theme.nip05 import com.vitorpamplona.quartz.experimental.forks.IForkableEvent @Composable @@ -86,30 +78,14 @@ fun ForkInformationRowLightColor( val route = remember(note) { routeFor(note, accountViewModel.account) } if (route != null) { - Row(modifier) { - Text( - text = - buildAnnotatedString { - appendLink(stringRes(id = R.string.forked_from) + " ") { - nav.nav(route) - } - }, - style = - LocalTextStyle.current.copy( - color = MaterialTheme.colorScheme.nip05, - fontSize = Font14SP, - ), - maxLines = 1, - overflow = TextOverflow.Visible, - ) - + Row(modifier, verticalAlignment = Alignment.CenterVertically) { val userState by observeUser(author, accountViewModel) userState?.user?.toBestDisplayName()?.let { CreateClickableTextWithEmoji( - clickablePart = it, + clickablePart = stringRes(id = R.string.forked_from) + " " + it, maxLines = 1, route = route, - overrideColor = MaterialTheme.colorScheme.nip05, + overrideColor = MaterialTheme.colorScheme.primary, fontSize = Font14SP, nav = nav, tags = userState?.user?.info?.tags, @@ -118,35 +94,3 @@ fun ForkInformationRowLightColor( } } } - -@Composable -fun ForkInformationRow( - originalVersion: Note, - modifier: Modifier = Modifier, - accountViewModel: AccountViewModel, - nav: INav, -) { - val noteState by observeNote(originalVersion, accountViewModel) - val note = noteState.note - val route = remember(note) { routeFor(note, accountViewModel.account) } - - if (route != null) { - Row(modifier) { - val author = note.author ?: return - val meta by observeUserInfo(author, accountViewModel) - - Text(stringRes(id = R.string.forked_from)) - Spacer(modifier = StdHorzSpacer) - - val userMetadata by observeUserInfo(author, accountViewModel) - - CreateClickableTextWithEmoji( - clickablePart = remember(meta) { meta?.bestName() ?: author.pubkeyDisplayHex() }, - maxLines = 1, - route = route, - nav = nav, - tags = userMetadata?.tags, - ) - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index b48551417..e3260439e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -113,9 +113,9 @@ import com.vitorpamplona.amethyst.ui.note.elements.DisplayFollowingHashtagsInPos import com.vitorpamplona.amethyst.ui.note.elements.DisplayLocation import com.vitorpamplona.amethyst.ui.note.elements.DisplayPoW import com.vitorpamplona.amethyst.ui.note.elements.DisplayReward -import com.vitorpamplona.amethyst.ui.note.elements.ForkInformationRow 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.observeEdits import com.vitorpamplona.amethyst.ui.note.showAmount @@ -187,11 +187,10 @@ import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.bounties.bountyBaseReward import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent -import com.vitorpamplona.quartz.experimental.forks.forkFromAddress +import com.vitorpamplona.quartz.experimental.forks.IForkableEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent -import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.core.Event @@ -509,11 +508,15 @@ private fun FullBleedNoteCompose( Column( remember { Modifier.weight(1f) }, ) { - ObserveDisplayNip05Status( - baseNote, - accountViewModel, - nav, - ) + if (noteEvent is IForkableEvent && noteEvent.isAFork()) { + ShowForkInformation(noteEvent, Modifier, accountViewModel, nav) + } else { + ObserveDisplayNip05Status( + baseNote, + accountViewModel, + nav, + ) + } } val geo = remember { noteEvent.geoHashOrScope() } @@ -546,7 +549,6 @@ private fun FullBleedNoteCompose( is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote, accountViewModel) is LongTextNoteEvent -> RenderLongFormHeaderForThread(noteEvent, baseNote, accountViewModel) is WikiNoteEvent -> RenderWikiHeaderForThread(noteEvent, accountViewModel, nav) - is NipTextEvent -> RenderNipHeaderForThread(noteEvent, accountViewModel, nav) is ClassifiedsEvent -> RenderClassifiedsReaderForThread(noteEvent, baseNote, accountViewModel, nav) } @@ -1110,8 +1112,6 @@ private fun RenderWikiHeaderForThread( accountViewModel: AccountViewModel, nav: INav, ) { - val forkedAddress = remember(noteEvent) { noteEvent.forkFromAddress() } - Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp)) { Column { noteEvent.image()?.let { @@ -1137,14 +1137,6 @@ private fun RenderWikiHeaderForThread( ) } - forkedAddress?.let { - LoadAddressableNote(it, accountViewModel) { originalVersion -> - if (originalVersion != null) { - ForkInformationRow(originalVersion, Modifier.fillMaxWidth(), accountViewModel, nav) - } - } - } - noteEvent .summary() ?.ifBlank { null } @@ -1159,24 +1151,3 @@ private fun RenderWikiHeaderForThread( } } } - -@Composable -private fun RenderNipHeaderForThread( - noteEvent: NipTextEvent, - accountViewModel: AccountViewModel, - nav: INav, -) { - val forkedAddress = remember(noteEvent) { noteEvent.forkFromAddress() } - - Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp)) { - Column { - forkedAddress?.let { - LoadAddressableNote(it, accountViewModel) { originalVersion -> - if (originalVersion != null) { - ForkInformationRow(originalVersion, Modifier.fillMaxWidth(), accountViewModel, nav) - } - } - } - } - } -} From a935c7cecf2c3a4bfbac35985a3f6f42e4c54f5e Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Sat, 10 Jan 2026 17:06:00 +0000 Subject: [PATCH 133/137] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-cs-rCZ/strings.xml | 1 + amethyst/src/main/res/values-de-rDE/strings.xml | 1 + amethyst/src/main/res/values-pl-rPL/strings.xml | 4 +++- amethyst/src/main/res/values-pt-rBR/strings.xml | 3 +++ amethyst/src/main/res/values-sv-rSE/strings.xml | 1 + 5 files changed, 9 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 8ff4d900a..7db2029cb 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -673,6 +673,7 @@ Politika příspěvků Zásady ochrany soukromí Podmínky & ujednání + N/A Chyby a upozornění z tohoto relé Délka zprávy Předplatné diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index f31726918..d1e1a9f3e 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -678,6 +678,7 @@ anz der Bedingungen ist erforderlich Veröffentlichungsrichtlinie Datenschutzerklärung Allgemeine Geschäftsbedingungen + N/A Fehler und Hinweise von diesem Relais Nachrichtenlänge Abonnements diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index bb966d36b..cb844defb 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -64,7 +64,7 @@ Zapy Liczba wyświetleń Powtórz - powtórzony + powtórzono edytowano edytuj #%1$s oryginalny @@ -135,6 +135,8 @@ Dodaj użytkownika Dodaj Transmiter Imię + Imię (dla @tagging) + Moje imię @tag Nazwa użytkownika Mój nick G Braun diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index d3d01e993..dcd1a2b45 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -673,6 +673,7 @@ Política de postagem Política de privacidade Termos & condições + N/A Erros e Avisos deste Relé Tamanho da mensagem Assinaturas @@ -686,6 +687,8 @@ Aceita até %1$s no futuro há %1$s + %1$s zeros + %1$s bits Retenção de eventos Tamanho do conteúdo Conectividade diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 623a0eadb..74ffeae20 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -672,6 +672,7 @@ Publiceringspolicy Integritetspolicy Villkor & bestämmelser + N/A Fel och meddelanden från detta relä Meddelandelängd Prenumerationer From 30e5b3d35e7f926963e290ef6dd7aa671ba1750f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 10 Jan 2026 12:08:42 -0500 Subject: [PATCH 134/137] removes bringing back all versions of an Addressable on the search screen. --- .../main/java/com/vitorpamplona/amethyst/model/LocalCache.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index ad2e47c4e..f13edbb7f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -2100,6 +2100,10 @@ object LocalCache : ILocalCache { } return notes.filter { _, note -> + if (note.event is AddressableEvent) { + return@filter false + } + if (excludeNoteEventFromSearchResults(note)) { return@filter false } From d24fe03bdd9ee6eaf9dc924713b413f2079375f8 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 10 Jan 2026 18:34:01 -0500 Subject: [PATCH 135/137] Avoids crashing when the k tag is a text --- .../com/vitorpamplona/quartz/nip01Core/tags/kinds/KindTag.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/kinds/KindTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/kinds/KindTag.kt index 62569ff8d..623c81715 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/kinds/KindTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/kinds/KindTag.kt @@ -44,7 +44,7 @@ class KindTag { ensure(tag.has(1)) { return null } ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].isNotEmpty()) { return null } - return tag[1].toInt() + return tag[1].toIntOrNull() } fun assemble(kind: Int) = arrayOf(TAG_NAME, kind.toString()) From d9a9cdd374bff9e45053ea8b1c02d16c615caf41 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 11 Jan 2026 12:18:21 -0500 Subject: [PATCH 136/137] Fixes lack of update in follow count on the UserProfile page --- .../service/relayClient/reqCommand/user/UserObservers.kt | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt index d40d4fb69..d2d2b59da 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt @@ -249,15 +249,12 @@ fun observeUserFollowCount( remember(user) { user .flow() - .followers.stateFlow - .sample(200) - .mapLatest { userState -> - userState.user.transientFollowCount() ?: 0 - }.distinctUntilChanged() + .follows.stateFlow + .mapLatest { it.user.transientFollowCount() ?: 0 } .flowOn(Dispatchers.IO) } - return flow.collectAsStateWithLifecycle(0) + return flow.collectAsStateWithLifecycle(user.transientFollowCount() ?: 0) } @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) From b71bb8cd2b45cd7b65492cbc2957f4d3d818df16 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Sun, 11 Jan 2026 17:20:41 +0000 Subject: [PATCH 137/137] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-cs-rCZ/strings.xml | 1 + amethyst/src/main/res/values-de-rDE/strings.xml | 1 + amethyst/src/main/res/values-hu-rHU/strings.xml | 3 +++ amethyst/src/main/res/values-pl-rPL/strings.xml | 1 + amethyst/src/main/res/values-pt-rBR/strings.xml | 1 + amethyst/src/main/res/values-sv-rSE/strings.xml | 1 + 6 files changed, 8 insertions(+) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 7db2029cb..9605e4c53 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -1227,4 +1227,5 @@ Broadcast sady doporučení Smazat seznam Smazat sadu doporučení + Typy diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index d1e1a9f3e..7a521e907 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -1232,4 +1232,5 @@ anz der Bedingungen ist erforderlich Empfehlungspaket veröffentlichen Liste löschen Empfehlungspaket löschen + Typen diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index c4fce36db..d6716c99c 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -135,6 +135,8 @@ Felhasználó ozzáadása Egy átjátszó hozzáadása Név + Név (az @említéshez) + Saját @említési név Megjelenítendő név Saját megjelenítendő név Strucc McNagyszerű @@ -1230,4 +1232,5 @@ Közvetítési csomag Lista törlése Csomag törlése + Változatok diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index cb844defb..8d5434880 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -1229,4 +1229,5 @@ Upowszechnij kategorię Usuń listę Usuń kategorię + Typy diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index dcd1a2b45..a3f8b297c 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -1227,4 +1227,5 @@ Transmitir pacote de recomendações Excluir lista Excluir pacote de recomendações + Tipos diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 74ffeae20..7a56f1c6e 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -1226,4 +1226,5 @@ Sänd rekommendationspaket Ta bort lista Ta bort rekommendationspaket + Typer