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 f3045fdbf..da8ba2f09 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -208,7 +208,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(), 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/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 1d4a08760..21f48c2c4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -130,7 +130,7 @@ object LocalCache { val notes = LargeCache() val addressables = LargeCache() val drafts = ConcurrentHashMap>() - val channels = ConcurrentHashMap() + val channels = LargeCache() val awaitingPaymentRequests = ConcurrentHashMap Unit>>(10) fun checkGetOrCreateUser(key: String): User? { @@ -193,7 +193,7 @@ object LocalCache { } fun getChannelIfExists(key: String): Channel? { - return channels[key] + return channels.get(key) } fun checkGetOrCreateNote(key: String): Note? { @@ -246,15 +246,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 } @@ -266,19 +275,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. @@ -970,10 +966,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) { @@ -1710,14 +1706,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) } } @@ -1796,8 +1792,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() @@ -1809,9 +1805,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/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()] 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..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 } @@ -85,17 +108,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 }, ), 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 }, ), 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) 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..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() @@ -348,7 +348,7 @@ private fun DiscoverFeedLoaded( ChannelCardCompose( baseNote = item, routeForLastRead = routeForLastRead, - modifier = Modifier, + modifier = Modifier.fillMaxWidth(), forceEventKind = forceEventKind, accountViewModel = accountViewModel, nav = nav, 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. diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP44v2Test.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP44v2Test.kt index 60740cfce..c146d5ca3 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() }