From ff5612203dac415da2b1a0bcb9dc16fe3702bdf4 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 21 Mar 2024 17:32:29 -0400 Subject: [PATCH 1/9] Fixes the width of muted messages on chat feeds. --- .../vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt index a6d4c2b12..3112e6f0d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt @@ -348,7 +348,7 @@ private fun DiscoverFeedLoaded( ChannelCardCompose( baseNote = item, routeForLastRead = routeForLastRead, - modifier = Modifier, + modifier = Modifier.fillMaxWidth(), forceEventKind = forceEventKind, accountViewModel = accountViewModel, nav = nav, From 6f9dbbb8c3251d49cfeb9d5405e38e0920bed2f0 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 22 Mar 2024 18:30:49 +0000 Subject: [PATCH 2/9] New Crowdin translations by GitHub Action --- app/src/main/res/values-es-rES/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 67effe4b5..1f72f897c 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -441,6 +441,8 @@ Siempre Solo Wi-Fi Nunca + Completo + Simplificado Sistema Claro Oscuro @@ -452,6 +454,8 @@ Vista previa de URL Desplazamiento inmersivo Ocultar barras de navegaciĆ³n al desplazarse + Modo de interfaz + Elegir el estilo de publicaciĆ³n Cargar imagen Spammers Silenciado. Hacer clic para reactivar el sonido. From c843e07709574890a4380bb93bd53630bbd9c1d0 Mon Sep 17 00:00:00 2001 From: Terry Yiu <963907+tyiu@users.noreply.github.com> Date: Fri, 22 Mar 2024 23:51:57 -0400 Subject: [PATCH 3/9] Fix Kotlin encryptDecryptTest to decrypt with swapped private and public keys to follow NIP-44 documentation --- .../com/vitorpamplona/quartz/NIP44v2Test.kt | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP44v2Test.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP44v2Test.kt index 081141154..51d8f6f47 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP44v2Test.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP44v2Test.kt @@ -38,16 +38,16 @@ import java.security.MessageDigest import java.security.SecureRandom @RunWith(AndroidJUnit4::class) -public class NIP44v2Test { - val vectors: VectorFile = +class NIP44v2Test { + private val vectors: VectorFile = jacksonObjectMapper() .readValue( getInstrumentation().context.assets.open("nip44.vectors.json"), VectorFile::class.java, ) - val random = SecureRandom() - val nip44v2 = Nip44v2(Secp256k1.get(), random) + private val random = SecureRandom() + private val nip44v2 = Nip44v2(Secp256k1.get(), random) @Test fun conversationKeyTest() { @@ -71,21 +71,25 @@ public class NIP44v2Test { fun encryptDecryptTest() { for (v in vectors.v2?.valid?.encryptDecrypt!!) { val pub2 = com.vitorpamplona.quartz.crypto.KeyPair(v.sec2!!.hexToByteArray()) - val conversationKey = nip44v2.getConversationKey(v.sec1!!.hexToByteArray(), pub2.pubKey) - assertEquals(v.conversationKey, conversationKey.toHexKey()) + val conversationKey1 = nip44v2.getConversationKey(v.sec1!!.hexToByteArray(), pub2.pubKey) + assertEquals(v.conversationKey, conversationKey1.toHexKey()) val ciphertext = nip44v2 .encryptWithNonce( v.plaintext!!, - conversationKey, + conversationKey1, v.nonce!!.hexToByteArray(), ) .encodePayload() assertEquals(v.payload, ciphertext) - val decrypted = nip44v2.decrypt(v.payload!!, conversationKey) + val pub1 = com.vitorpamplona.quartz.crypto.KeyPair(v.sec1.hexToByteArray()) + val conversationKey2 = nip44v2.getConversationKey(v.sec2.hexToByteArray(), pub1.pubKey) + assertEquals(v.conversationKey, conversationKey2.toHexKey()) + + val decrypted = nip44v2.decrypt(v.payload!!, conversationKey2) assertEquals(v.plaintext, decrypted) } } @@ -116,7 +120,7 @@ public class NIP44v2Test { } @Test - fun invalidMessageLenghts() { + fun invalidMessageLengths() { for (v in vectors.v2?.invalid?.encryptMsgLengths!!) { val key = ByteArray(32) random.nextBytes(key) @@ -154,7 +158,7 @@ public class NIP44v2Test { } } - fun sha256Hex(data: ByteArray): String { + private fun sha256Hex(data: ByteArray): String { // Creates a new buffer every time return MessageDigest.getInstance("SHA-256").digest(data).toHexKey() } From 8d70664cc1b7793e10289fb1ebdb1d8677063118 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 23 Mar 2024 17:15:19 -0400 Subject: [PATCH 4/9] Fixes the list selection on the Discovery page. --- .../vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt index 3112e6f0d..e6bf7d8d2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt @@ -316,7 +316,7 @@ fun WatchAccountForDiscoveryScreen( discoveryChatFeedViewModel: NostrDiscoverChatFeedViewModel, accountViewModel: AccountViewModel, ) { - val listState by accountViewModel.account.liveStoriesFollowLists.collectAsStateWithLifecycle() + val listState by accountViewModel.account.liveDiscoveryFollowLists.collectAsStateWithLifecycle() LaunchedEffect(accountViewModel, listState) { NostrDiscoveryDataSource.resetFilters() From 2c5e07de87573ffc7d8b1211856689b4d8b68372 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 23 Mar 2024 17:15:38 -0400 Subject: [PATCH 5/9] removes the use of data classes to speed up comparisons. --- app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 4905e5e6f..c097d8b9a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -207,7 +207,7 @@ class Account( val saveable: AccountLiveData = AccountLiveData(this) @Immutable - data class LiveFollowLists( + class LiveFollowLists( val users: ImmutableSet = persistentSetOf(), val hashtags: ImmutableSet = persistentSetOf(), val geotags: ImmutableSet = persistentSetOf(), From eb6d31cf2b35f5b14885d6d0cb617eaae8c8f77e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 23 Mar 2024 17:16:43 -0400 Subject: [PATCH 6/9] Better filter to bring public chat messages and avoid public chat creation and metadata updates --- .../amethyst/service/NostrDiscoveryDataSource.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt index 2a94fd7aa..ba9f22348 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt @@ -178,9 +178,8 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") { filter = JsonFilter( authors = follows, - kinds = - listOf(ChannelCreateEvent.KIND, ChannelMetadataEvent.KIND, ChannelMessageEvent.KIND), - limit = 300, + kinds = listOf(ChannelMessageEvent.KIND), + limit = 500, since = latestEOSEs.users[account.userProfile()] ?.followList @@ -194,7 +193,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") { filter = JsonFilter( ids = followChats, - kinds = listOf(ChannelCreateEvent.KIND), + kinds = listOf(ChannelCreateEvent.KIND, ChannelMessageEvent.KIND), limit = 300, since = latestEOSEs.users[account.userProfile()] From 5a1c9f5a4aa31b5319b126f720a45e685ccd13b9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 23 Mar 2024 17:18:27 -0400 Subject: [PATCH 7/9] Ranks communities by the last post in each community in the discovery screen --- .../ui/dal/DiscoverCommunityFeedFilter.kt | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverCommunityFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverCommunityFeedFilter.kt index 163905fad..1e321a7a0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverCommunityFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverCommunityFeedFilter.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.dal 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.quartz.encoders.ATag import com.vitorpamplona.quartz.events.CommunityDefinitionEvent import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent @@ -112,21 +111,15 @@ open class DiscoverCommunityFeedFilter(val account: Account) : AdditiveFeedFilte ) = aTag != null && aTag.kind == CommunityDefinitionEvent.KIND && params.match(aTag) override fun sort(collection: Set): List { - val followingKeySet = - account.liveDiscoveryFollowLists.value?.users ?: account.liveKind3Follows.value.users - - val counter = ParticipantListBuilder() - val participantCounts = - collection.associate { it to counter.countFollowsThatParticipateOn(it, followingKeySet) } - - val allParticipants = - collection.associate { it to counter.countFollowsThatParticipateOn(it, null) } + val lastNote = + collection.associateWith { note -> + note.boosts.maxOfOrNull { it.createdAt() ?: 0 } ?: 0 + } return collection .sortedWith( compareBy( - { participantCounts[it] }, - { allParticipants[it] }, + { lastNote[it] }, { it.createdAt() }, { it.idHex }, ), From 0b40b6d1d85eed212658624e53080157c2482401 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 23 Mar 2024 17:19:54 -0400 Subject: [PATCH 8/9] Sorts chats by the latest message in Discovery --- .../java/com/vitorpamplona/amethyst/model/Channel.kt | 7 +++++-- .../amethyst/ui/dal/DiscoverChatFeedFilter.kt | 12 +++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt index 18865dbb4..0c127556e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt @@ -108,10 +108,9 @@ class LiveActivitiesChannel(val address: ATag) : Channel(address.toTag()) { @Stable abstract class Channel(val idHex: String) { var creator: User? = null - var updatedMetadataAt: Long = 0 - val notes = LargeCache() + var lastNoteCreatedAt: Long = 0 open fun id() = Hex.decode(idHex) @@ -147,6 +146,10 @@ abstract class Channel(val idHex: String) { fun addNote(note: Note) { notes.put(note.idHex, note) + + if ((note.createdAt() ?: 0) > lastNoteCreatedAt) { + lastNoteCreatedAt = note.createdAt() ?: 0 + } } fun removeNote(note: Note) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt index 2a58fb1f5..82eee98a3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt @@ -85,17 +85,15 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter): List { - val followingKeySet = - account.liveDiscoveryFollowLists.value?.users ?: account.liveKind3Follows.value.users - - val counter = ParticipantListBuilder() - val participantCounts = - collection.associate { it to counter.countFollowsThatParticipateOn(it, followingKeySet) } + val lastNote = + collection.associateWith { note -> + LocalCache.getChannelIfExists(note.idHex)?.lastNoteCreatedAt ?: 0 + } return collection .sortedWith( compareBy( - { participantCounts[it] }, + { lastNote[it] }, { it.createdAt() }, { it.idHex }, ), From 9990f458bf54767031486ce625237b8d763e7781 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 23 Mar 2024 17:21:02 -0400 Subject: [PATCH 9/9] Moves Channel to LargeCache Fixes filter for Chat Messages in discovery --- .../amethyst/model/LocalCache.kt | 54 +++++++++---------- .../amethyst/ui/dal/DiscoverChatFeedFilter.kt | 37 ++++++++++--- .../amethyst/ui/dal/DiscoverLiveFeedFilter.kt | 4 +- 3 files changed, 57 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index eb4f270c3..874593c96 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -129,7 +129,7 @@ object LocalCache { val notes = LargeCache() val addressables = LargeCache() - val channels = ConcurrentHashMap() + val channels = LargeCache() val awaitingPaymentRequests = ConcurrentHashMap Unit>>(10) fun checkGetOrCreateUser(key: String): User? { @@ -164,7 +164,7 @@ object LocalCache { } fun getChannelIfExists(key: String): Channel? { - return channels[key] + return channels.get(key) } fun checkGetOrCreateNote(key: String): Note? { @@ -217,15 +217,24 @@ object LocalCache { } } + fun getOrCreateChannel( + key: String, + channelFactory: (String) -> Channel, + ): Channel { + checkNotInMainThread() + + return channels.getOrCreate(key, channelFactory) + } + fun checkGetOrCreateChannel(key: String): Channel? { checkNotInMainThread() if (isValidHex(key)) { - return getOrCreateChannel(key) { PublicChatChannel(key) } + return channels.getOrCreate(key) { PublicChatChannel(key) } } val aTag = ATag.parse(key, null) if (aTag != null) { - return getOrCreateChannel(aTag.toTag()) { LiveActivitiesChannel(aTag) } + return channels.getOrCreate(aTag.toTag()) { LiveActivitiesChannel(aTag) } } return null } @@ -237,19 +246,6 @@ object LocalCache { return HexValidator.isHex(key) } - fun getOrCreateChannel( - key: String, - channelFactory: (String) -> Channel, - ): Channel { - checkNotInMainThread() - - return channels[key] - ?: run { - val newObject = channelFactory(key) - channels.putIfAbsent(key, newObject) ?: newObject - } - } - fun checkGetOrCreateAddressableNote(key: String): AddressableNote? { return try { val addr = ATag.parse(key, null) // relay doesn't matter for the index. @@ -941,10 +937,10 @@ object LocalCache { masterNote.removeReport(deleteNote) } - deleteNote.channelHex()?.let { channels[it]?.removeNote(deleteNote) } + deleteNote.channelHex()?.let { getChannelIfExists(it)?.removeNote(deleteNote) } (deleteNote.event as? LiveActivitiesChatMessageEvent)?.activity()?.let { - channels[it.toTag()]?.removeNote(deleteNote) + getChannelIfExists(it.toTag())?.removeNote(deleteNote) } if (deleteNote.event is PrivateDmEvent) { @@ -1681,14 +1677,14 @@ object LocalCache { checkNotInMainThread() val key = decodeEventIdAsHexOrNull(text) - if (key != null && channels[key] != null) { - return listOfNotNull(channels[key]) + if (key != null && getChannelIfExists(key) != null) { + return listOfNotNull(getChannelIfExists(key)) } - return channels.values.filter { - it.anyNameStartsWith(text) || - it.idHex.startsWith(text, true) || - it.idNote().startsWith(text, true) + return channels.filter { _, channel -> + channel.anyNameStartsWith(text) || + channel.idHex.startsWith(text, true) || + channel.idNote().startsWith(text, true) } } @@ -1767,8 +1763,8 @@ object LocalCache { fun pruneOldAndHiddenMessages(account: Account) { checkNotInMainThread() - channels.forEach { it -> - val toBeRemoved = it.value.pruneOldAndHiddenMessages(account) + channels.forEach { _, channel -> + val toBeRemoved = channel.pruneOldAndHiddenMessages(account) val childrenToBeRemoved = mutableListOf() @@ -1780,9 +1776,9 @@ object LocalCache { removeFromCache(childrenToBeRemoved) - if (toBeRemoved.size > 100 || it.value.notes.size() > 100) { + if (toBeRemoved.size > 100 || channel.notes.size() > 100) { println( - "PRUNE: ${toBeRemoved.size} messages removed from ${it.value.toBestDisplayName()}. ${it.value.notes.size()} kept", + "PRUNE: ${toBeRemoved.size} messages removed from ${channel.toBestDisplayName()}. ${channel.notes.size()} kept", ) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt index 82eee98a3..0eabe3422 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.dal 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.PublicChatChannel import com.vitorpamplona.quartz.events.ChannelCreateEvent import com.vitorpamplona.quartz.events.IsInPublicChatChannel import com.vitorpamplona.quartz.events.MuteListEvent @@ -42,12 +42,25 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter { + val params = buildFilterParams(account) + val allChannelNotes = - LocalCache.channels.values.mapNotNull { LocalCache.getNoteIfExists(it.idHex) } + LocalCache.channels.mapNotNullIntoSet { _, channel -> + if (channel is PublicChatChannel) { + val note = LocalCache.getNoteIfExists(channel.idHex) + val noteEvent = note?.event - val notes = innerApplyFilter(allChannelNotes) + if (noteEvent == null || params.match(noteEvent)) { + note + } else { + null + } + } else { + null + } + } - return sort(notes) + return sort(allChannelNotes) } override fun applyFilter(collection: Set): Set { @@ -70,11 +83,21 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter 0) { + note + } else { + null + } } else if (noteEvent is IsInPublicChatChannel) { val channel = noteEvent.channel()?.let { LocalCache.checkGetOrCreateNote(it) } - if (channel != null && (channel.event == null || params.match(channel.event))) { - channel + if (channel != null && + (channel.event == null || (channel.event is ChannelCreateEvent && params.match(channel.event))) + ) { + if ((LocalCache.getChannelIfExists(channel.idHex)?.notes?.size() ?: 0) > 0) { + channel + } else { + null + } } else { null } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt index 30aa19176..ebce2500a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt @@ -48,8 +48,8 @@ open class DiscoverLiveFeedFilter( } override fun feed(): List { - val allChannelNotes = LocalCache.channels.values.mapNotNull { LocalCache.getNoteIfExists(it.idHex) } - val allMessageNotes = LocalCache.channels.values.map { it.notes.filter { key, it -> it.event is LiveActivitiesEvent } }.flatten() + val allChannelNotes = LocalCache.channels.mapNotNull { _, channel -> LocalCache.getNoteIfExists(channel.idHex) } + val allMessageNotes = LocalCache.channels.map { _, channel -> channel.notes.filter { key, it -> it.event is LiveActivitiesEvent } }.flatten() val notes = innerApplyFilter(allChannelNotes + allMessageNotes)