Merge branch 'main' into main

This commit is contained in:
greenart7c3
2024-03-25 07:08:08 -03:00
committed by GitHub
10 changed files with 96 additions and 76 deletions
@@ -208,7 +208,7 @@ class Account(
val saveable: AccountLiveData = AccountLiveData(this) val saveable: AccountLiveData = AccountLiveData(this)
@Immutable @Immutable
data class LiveFollowLists( class LiveFollowLists(
val users: ImmutableSet<String> = persistentSetOf(), val users: ImmutableSet<String> = persistentSetOf(),
val hashtags: ImmutableSet<String> = persistentSetOf(), val hashtags: ImmutableSet<String> = persistentSetOf(),
val geotags: ImmutableSet<String> = persistentSetOf(), val geotags: ImmutableSet<String> = persistentSetOf(),
@@ -108,10 +108,9 @@ class LiveActivitiesChannel(val address: ATag) : Channel(address.toTag()) {
@Stable @Stable
abstract class Channel(val idHex: String) { abstract class Channel(val idHex: String) {
var creator: User? = null var creator: User? = null
var updatedMetadataAt: Long = 0 var updatedMetadataAt: Long = 0
val notes = LargeCache<HexKey, Note>() val notes = LargeCache<HexKey, Note>()
var lastNoteCreatedAt: Long = 0
open fun id() = Hex.decode(idHex) open fun id() = Hex.decode(idHex)
@@ -147,6 +146,10 @@ abstract class Channel(val idHex: String) {
fun addNote(note: Note) { fun addNote(note: Note) {
notes.put(note.idHex, note) notes.put(note.idHex, note)
if ((note.createdAt() ?: 0) > lastNoteCreatedAt) {
lastNoteCreatedAt = note.createdAt() ?: 0
}
} }
fun removeNote(note: Note) { fun removeNote(note: Note) {
@@ -130,7 +130,7 @@ object LocalCache {
val notes = LargeCache<HexKey, Note>() val notes = LargeCache<HexKey, Note>()
val addressables = LargeCache<String, AddressableNote>() val addressables = LargeCache<String, AddressableNote>()
val drafts = ConcurrentHashMap<String, MutableList<Drafts>>() val drafts = ConcurrentHashMap<String, MutableList<Drafts>>()
val channels = ConcurrentHashMap<HexKey, Channel>() val channels = LargeCache<HexKey, Channel>()
val awaitingPaymentRequests = ConcurrentHashMap<HexKey, Pair<Note?, (LnZapPaymentResponseEvent) -> Unit>>(10) val awaitingPaymentRequests = ConcurrentHashMap<HexKey, Pair<Note?, (LnZapPaymentResponseEvent) -> Unit>>(10)
fun checkGetOrCreateUser(key: String): User? { fun checkGetOrCreateUser(key: String): User? {
@@ -193,7 +193,7 @@ object LocalCache {
} }
fun getChannelIfExists(key: String): Channel? { fun getChannelIfExists(key: String): Channel? {
return channels[key] return channels.get(key)
} }
fun checkGetOrCreateNote(key: String): Note? { 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? { fun checkGetOrCreateChannel(key: String): Channel? {
checkNotInMainThread() checkNotInMainThread()
if (isValidHex(key)) { if (isValidHex(key)) {
return getOrCreateChannel(key) { PublicChatChannel(key) } return channels.getOrCreate(key) { PublicChatChannel(key) }
} }
val aTag = ATag.parse(key, null) val aTag = ATag.parse(key, null)
if (aTag != null) { if (aTag != null) {
return getOrCreateChannel(aTag.toTag()) { LiveActivitiesChannel(aTag) } return channels.getOrCreate(aTag.toTag()) { LiveActivitiesChannel(aTag) }
} }
return null return null
} }
@@ -266,19 +275,6 @@ object LocalCache {
return HexValidator.isHex(key) 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? { fun checkGetOrCreateAddressableNote(key: String): AddressableNote? {
return try { return try {
val addr = ATag.parse(key, null) // relay doesn't matter for the index. val addr = ATag.parse(key, null) // relay doesn't matter for the index.
@@ -970,10 +966,10 @@ object LocalCache {
masterNote.removeReport(deleteNote) masterNote.removeReport(deleteNote)
} }
deleteNote.channelHex()?.let { channels[it]?.removeNote(deleteNote) } deleteNote.channelHex()?.let { getChannelIfExists(it)?.removeNote(deleteNote) }
(deleteNote.event as? LiveActivitiesChatMessageEvent)?.activity()?.let { (deleteNote.event as? LiveActivitiesChatMessageEvent)?.activity()?.let {
channels[it.toTag()]?.removeNote(deleteNote) getChannelIfExists(it.toTag())?.removeNote(deleteNote)
} }
if (deleteNote.event is PrivateDmEvent) { if (deleteNote.event is PrivateDmEvent) {
@@ -1710,14 +1706,14 @@ object LocalCache {
checkNotInMainThread() checkNotInMainThread()
val key = decodeEventIdAsHexOrNull(text) val key = decodeEventIdAsHexOrNull(text)
if (key != null && channels[key] != null) { if (key != null && getChannelIfExists(key) != null) {
return listOfNotNull(channels[key]) return listOfNotNull(getChannelIfExists(key))
} }
return channels.values.filter { return channels.filter { _, channel ->
it.anyNameStartsWith(text) || channel.anyNameStartsWith(text) ||
it.idHex.startsWith(text, true) || channel.idHex.startsWith(text, true) ||
it.idNote().startsWith(text, true) channel.idNote().startsWith(text, true)
} }
} }
@@ -1796,8 +1792,8 @@ object LocalCache {
fun pruneOldAndHiddenMessages(account: Account) { fun pruneOldAndHiddenMessages(account: Account) {
checkNotInMainThread() checkNotInMainThread()
channels.forEach { it -> channels.forEach { _, channel ->
val toBeRemoved = it.value.pruneOldAndHiddenMessages(account) val toBeRemoved = channel.pruneOldAndHiddenMessages(account)
val childrenToBeRemoved = mutableListOf<Note>() val childrenToBeRemoved = mutableListOf<Note>()
@@ -1809,9 +1805,9 @@ object LocalCache {
removeFromCache(childrenToBeRemoved) removeFromCache(childrenToBeRemoved)
if (toBeRemoved.size > 100 || it.value.notes.size() > 100) { if (toBeRemoved.size > 100 || channel.notes.size() > 100) {
println( 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",
) )
} }
} }
@@ -178,9 +178,8 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
filter = filter =
JsonFilter( JsonFilter(
authors = follows, authors = follows,
kinds = kinds = listOf(ChannelMessageEvent.KIND),
listOf(ChannelCreateEvent.KIND, ChannelMetadataEvent.KIND, ChannelMessageEvent.KIND), limit = 500,
limit = 300,
since = since =
latestEOSEs.users[account.userProfile()] latestEOSEs.users[account.userProfile()]
?.followList ?.followList
@@ -194,7 +193,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
filter = filter =
JsonFilter( JsonFilter(
ids = followChats, ids = followChats,
kinds = listOf(ChannelCreateEvent.KIND), kinds = listOf(ChannelCreateEvent.KIND, ChannelMessageEvent.KIND),
limit = 300, limit = 300,
since = since =
latestEOSEs.users[account.userProfile()] latestEOSEs.users[account.userProfile()]
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note 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.ChannelCreateEvent
import com.vitorpamplona.quartz.events.IsInPublicChatChannel import com.vitorpamplona.quartz.events.IsInPublicChatChannel
import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.MuteListEvent
@@ -42,12 +42,25 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter<Not
} }
override fun feed(): List<Note> { override fun feed(): List<Note> {
val params = buildFilterParams(account)
val allChannelNotes = 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<Note>): Set<Note> { override fun applyFilter(collection: Set<Note>): Set<Note> {
@@ -70,11 +83,21 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter<Not
// note event here will never be null // note event here will never be null
val noteEvent = note.event val noteEvent = note.event
if (noteEvent is ChannelCreateEvent && params.match(noteEvent)) { if (noteEvent is ChannelCreateEvent && params.match(noteEvent)) {
note if ((LocalCache.getChannelIfExists(noteEvent.id)?.notes?.size() ?: 0) > 0) {
note
} else {
null
}
} else if (noteEvent is IsInPublicChatChannel) { } else if (noteEvent is IsInPublicChatChannel) {
val channel = noteEvent.channel()?.let { LocalCache.checkGetOrCreateNote(it) } val channel = noteEvent.channel()?.let { LocalCache.checkGetOrCreateNote(it) }
if (channel != null && (channel.event == null || params.match(channel.event))) { if (channel != null &&
channel (channel.event == null || (channel.event is ChannelCreateEvent && params.match(channel.event)))
) {
if ((LocalCache.getChannelIfExists(channel.idHex)?.notes?.size() ?: 0) > 0) {
channel
} else {
null
}
} else { } else {
null null
} }
@@ -85,17 +108,15 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter<Not
} }
override fun sort(collection: Set<Note>): List<Note> { override fun sort(collection: Set<Note>): List<Note> {
val followingKeySet = val lastNote =
account.liveDiscoveryFollowLists.value?.users ?: account.liveKind3Follows.value.users collection.associateWith { note ->
LocalCache.getChannelIfExists(note.idHex)?.lastNoteCreatedAt ?: 0
val counter = ParticipantListBuilder() }
val participantCounts =
collection.associate { it to counter.countFollowsThatParticipateOn(it, followingKeySet) }
return collection return collection
.sortedWith( .sortedWith(
compareBy( compareBy(
{ participantCounts[it] }, { lastNote[it] },
{ it.createdAt() }, { it.createdAt() },
{ it.idHex }, { it.idHex },
), ),
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.ParticipantListBuilder
import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent 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) ) = aTag != null && aTag.kind == CommunityDefinitionEvent.KIND && params.match(aTag)
override fun sort(collection: Set<Note>): List<Note> { override fun sort(collection: Set<Note>): List<Note> {
val followingKeySet = val lastNote =
account.liveDiscoveryFollowLists.value?.users ?: account.liveKind3Follows.value.users collection.associateWith { note ->
note.boosts.maxOfOrNull { it.createdAt() ?: 0 } ?: 0
val counter = ParticipantListBuilder() }
val participantCounts =
collection.associate { it to counter.countFollowsThatParticipateOn(it, followingKeySet) }
val allParticipants =
collection.associate { it to counter.countFollowsThatParticipateOn(it, null) }
return collection return collection
.sortedWith( .sortedWith(
compareBy( compareBy(
{ participantCounts[it] }, { lastNote[it] },
{ allParticipants[it] },
{ it.createdAt() }, { it.createdAt() },
{ it.idHex }, { it.idHex },
), ),
@@ -48,8 +48,8 @@ open class DiscoverLiveFeedFilter(
} }
override fun feed(): List<Note> { override fun feed(): List<Note> {
val allChannelNotes = LocalCache.channels.values.mapNotNull { LocalCache.getNoteIfExists(it.idHex) } val allChannelNotes = LocalCache.channels.mapNotNull { _, channel -> LocalCache.getNoteIfExists(channel.idHex) }
val allMessageNotes = LocalCache.channels.values.map { it.notes.filter { key, it -> it.event is LiveActivitiesEvent } }.flatten() val allMessageNotes = LocalCache.channels.map { _, channel -> channel.notes.filter { key, it -> it.event is LiveActivitiesEvent } }.flatten()
val notes = innerApplyFilter(allChannelNotes + allMessageNotes) val notes = innerApplyFilter(allChannelNotes + allMessageNotes)
@@ -316,7 +316,7 @@ fun WatchAccountForDiscoveryScreen(
discoveryChatFeedViewModel: NostrDiscoverChatFeedViewModel, discoveryChatFeedViewModel: NostrDiscoverChatFeedViewModel,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
) { ) {
val listState by accountViewModel.account.liveStoriesFollowLists.collectAsStateWithLifecycle() val listState by accountViewModel.account.liveDiscoveryFollowLists.collectAsStateWithLifecycle()
LaunchedEffect(accountViewModel, listState) { LaunchedEffect(accountViewModel, listState) {
NostrDiscoveryDataSource.resetFilters() NostrDiscoveryDataSource.resetFilters()
@@ -348,7 +348,7 @@ private fun DiscoverFeedLoaded(
ChannelCardCompose( ChannelCardCompose(
baseNote = item, baseNote = item,
routeForLastRead = routeForLastRead, routeForLastRead = routeForLastRead,
modifier = Modifier, modifier = Modifier.fillMaxWidth(),
forceEventKind = forceEventKind, forceEventKind = forceEventKind,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
@@ -441,6 +441,8 @@
<string name="connectivity_type_always">Siempre</string> <string name="connectivity_type_always">Siempre</string>
<string name="connectivity_type_wifi_only">Solo Wi-Fi</string> <string name="connectivity_type_wifi_only">Solo Wi-Fi</string>
<string name="connectivity_type_never">Nunca</string> <string name="connectivity_type_never">Nunca</string>
<string name="ui_feature_set_type_complete">Completo</string>
<string name="ui_feature_set_type_simplified">Simplificado</string>
<string name="system">Sistema</string> <string name="system">Sistema</string>
<string name="light">Claro</string> <string name="light">Claro</string>
<string name="dark">Oscuro</string> <string name="dark">Oscuro</string>
@@ -452,6 +454,8 @@
<string name="automatically_show_url_preview">Vista previa de URL</string> <string name="automatically_show_url_preview">Vista previa de URL</string>
<string name="automatically_hide_nav_bars">Desplazamiento inmersivo</string> <string name="automatically_hide_nav_bars">Desplazamiento inmersivo</string>
<string name="automatically_hide_nav_bars_description">Ocultar barras de navegación al desplazarse</string> <string name="automatically_hide_nav_bars_description">Ocultar barras de navegación al desplazarse</string>
<string name="ui_style">Modo de interfaz</string>
<string name="ui_style_description">Elegir el estilo de publicación</string>
<string name="load_image">Cargar imagen</string> <string name="load_image">Cargar imagen</string>
<string name="spamming_users">Spammers</string> <string name="spamming_users">Spammers</string>
<string name="muted_button">Silenciado. Hacer clic para reactivar el sonido.</string> <string name="muted_button">Silenciado. Hacer clic para reactivar el sonido.</string>
@@ -38,16 +38,16 @@ import java.security.MessageDigest
import java.security.SecureRandom import java.security.SecureRandom
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
public class NIP44v2Test { class NIP44v2Test {
val vectors: VectorFile = private val vectors: VectorFile =
jacksonObjectMapper() jacksonObjectMapper()
.readValue( .readValue(
getInstrumentation().context.assets.open("nip44.vectors.json"), getInstrumentation().context.assets.open("nip44.vectors.json"),
VectorFile::class.java, VectorFile::class.java,
) )
val random = SecureRandom() private val random = SecureRandom()
val nip44v2 = Nip44v2(Secp256k1.get(), random) private val nip44v2 = Nip44v2(Secp256k1.get(), random)
@Test @Test
fun conversationKeyTest() { fun conversationKeyTest() {
@@ -71,21 +71,25 @@ public class NIP44v2Test {
fun encryptDecryptTest() { fun encryptDecryptTest() {
for (v in vectors.v2?.valid?.encryptDecrypt!!) { for (v in vectors.v2?.valid?.encryptDecrypt!!) {
val pub2 = com.vitorpamplona.quartz.crypto.KeyPair(v.sec2!!.hexToByteArray()) val pub2 = com.vitorpamplona.quartz.crypto.KeyPair(v.sec2!!.hexToByteArray())
val conversationKey = nip44v2.getConversationKey(v.sec1!!.hexToByteArray(), pub2.pubKey) val conversationKey1 = nip44v2.getConversationKey(v.sec1!!.hexToByteArray(), pub2.pubKey)
assertEquals(v.conversationKey, conversationKey.toHexKey()) assertEquals(v.conversationKey, conversationKey1.toHexKey())
val ciphertext = val ciphertext =
nip44v2 nip44v2
.encryptWithNonce( .encryptWithNonce(
v.plaintext!!, v.plaintext!!,
conversationKey, conversationKey1,
v.nonce!!.hexToByteArray(), v.nonce!!.hexToByteArray(),
) )
.encodePayload() .encodePayload()
assertEquals(v.payload, ciphertext) 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) assertEquals(v.plaintext, decrypted)
} }
} }
@@ -116,7 +120,7 @@ public class NIP44v2Test {
} }
@Test @Test
fun invalidMessageLenghts() { fun invalidMessageLengths() {
for (v in vectors.v2?.invalid?.encryptMsgLengths!!) { for (v in vectors.v2?.invalid?.encryptMsgLengths!!) {
val key = ByteArray(32) val key = ByteArray(32)
random.nextBytes(key) 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 // Creates a new buffer every time
return MessageDigest.getInstance("SHA-256").digest(data).toHexKey() return MessageDigest.getInstance("SHA-256").digest(data).toHexKey()
} }