Adds a cache system for WoT scores
This commit is contained in:
@@ -121,7 +121,6 @@ import com.vitorpamplona.quartz.experimental.profileGallery.dimension
|
|||||||
import com.vitorpamplona.quartz.experimental.profileGallery.fromEvent
|
import com.vitorpamplona.quartz.experimental.profileGallery.fromEvent
|
||||||
import com.vitorpamplona.quartz.experimental.profileGallery.hash
|
import com.vitorpamplona.quartz.experimental.profileGallery.hash
|
||||||
import com.vitorpamplona.quartz.experimental.profileGallery.mimeType
|
import com.vitorpamplona.quartz.experimental.profileGallery.mimeType
|
||||||
import com.vitorpamplona.quartz.experimental.relationshipStatus.ContactCardEvent
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
@@ -210,17 +209,13 @@ import com.vitorpamplona.quartz.utils.containsAny
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.emitAll
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.transformLatest
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -1733,25 +1728,6 @@ class Account(
|
|||||||
).toSet()
|
).toSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
fun loadUserCardFlow(target: HexKey): Flow<Int?> =
|
|
||||||
trustProviderList.liveUserRankProvider
|
|
||||||
.transformLatest { provider ->
|
|
||||||
if (provider != null) {
|
|
||||||
emitAll(
|
|
||||||
cache
|
|
||||||
.getOrCreateAddressableNote(
|
|
||||||
ContactCardEvent.createAddress(provider.pubkey, target),
|
|
||||||
).flow()
|
|
||||||
.metadata.stateFlow,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
emit(null)
|
|
||||||
}
|
|
||||||
}.map {
|
|
||||||
(it?.note?.event as? ContactCardEvent)?.rank()
|
|
||||||
}.flowOn(Dispatchers.IO)
|
|
||||||
|
|
||||||
suspend fun saveDMRelayList(dmRelays: List<NormalizedRelayUrl>) = sendLiterallyEverywhere(dmRelayList.saveRelayList(dmRelays))
|
suspend fun saveDMRelayList(dmRelays: List<NormalizedRelayUrl>) = sendLiterallyEverywhere(dmRelayList.saveRelayList(dmRelays))
|
||||||
|
|
||||||
suspend fun savePrivateOutboxRelayList(relays: List<NormalizedRelayUrl>) = sendMyPublicAndPrivateOutbox(privateStorageRelayList.saveRelayList(relays))
|
suspend fun savePrivateOutboxRelayList(relays: List<NormalizedRelayUrl>) = sendMyPublicAndPrivateOutbox(privateStorageRelayList.saveRelayList(relays))
|
||||||
|
|||||||
@@ -1093,11 +1093,26 @@ object LocalCache : ILocalCache {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Event.toNote() = getOrCreateNote(id)
|
||||||
|
|
||||||
|
fun AddressableEvent.toAddressableNote() = getOrCreateAddressableNote(address())
|
||||||
|
|
||||||
fun consume(
|
fun consume(
|
||||||
event: ContactCardEvent,
|
event: ContactCardEvent,
|
||||||
relay: NormalizedRelayUrl?,
|
relay: NormalizedRelayUrl?,
|
||||||
wasVerified: Boolean,
|
wasVerified: Boolean,
|
||||||
) = consumeBaseReplaceable(event, relay, wasVerified)
|
): Boolean {
|
||||||
|
val note = event.toAddressableNote()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new
|
||||||
|
}
|
||||||
|
|
||||||
fun consume(
|
fun consume(
|
||||||
event: OtsEvent,
|
event: OtsEvent,
|
||||||
@@ -1335,6 +1350,9 @@ object LocalCache : ILocalCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (deleteNote is AddressableNote && deletedEvent is ContactCardEvent) {
|
||||||
|
getUserIfExists(deletedEvent.aboutUser())?.cardsOrNull()?.removeCard(deleteNote)
|
||||||
|
}
|
||||||
|
|
||||||
if (deletedEvent is TorrentCommentEvent) {
|
if (deletedEvent is TorrentCommentEvent) {
|
||||||
deletedEvent.torrentIds()?.let {
|
deletedEvent.torrentIds()?.let {
|
||||||
@@ -2460,6 +2478,9 @@ object LocalCache : ILocalCache {
|
|||||||
getAddressableNoteIfExists(it.address)?.removeReport(note)
|
getAddressableNoteIfExists(it.address)?.removeReport(note)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (note is AddressableNote && noteEvent is ContactCardEvent) {
|
||||||
|
getUserIfExists(noteEvent.aboutUser())?.cardsOrNull()?.removeCard(note)
|
||||||
|
}
|
||||||
|
|
||||||
note.clearFlow()
|
note.clearFlow()
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model
|
|||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import com.vitorpamplona.amethyst.model.nip56Reports.UserReportCache
|
import com.vitorpamplona.amethyst.model.nip56Reports.UserReportCache
|
||||||
|
import com.vitorpamplona.amethyst.model.trustedAssertions.UserCardsCache
|
||||||
import com.vitorpamplona.amethyst.ui.note.toShortDisplay
|
import com.vitorpamplona.amethyst.ui.note.toShortDisplay
|
||||||
import com.vitorpamplona.quartz.lightning.Lud06
|
import com.vitorpamplona.quartz.lightning.Lud06
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
|
import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
|
||||||
@@ -53,6 +54,7 @@ class User(
|
|||||||
val dmRelayListNote: Note,
|
val dmRelayListNote: Note,
|
||||||
) {
|
) {
|
||||||
private var reports: UserReportCache? = null
|
private var reports: UserReportCache? = null
|
||||||
|
private var cards: UserCardsCache? = null
|
||||||
|
|
||||||
// private var deps = ScatterMap<KClass<out UserDependencies>, UserDependencies>()
|
// private var deps = ScatterMap<KClass<out UserDependencies>, UserDependencies>()
|
||||||
|
|
||||||
@@ -227,6 +229,10 @@ class User(
|
|||||||
|
|
||||||
// fun reports(): UserReports = deps.getOrPut(UserReports::class) { UserReports() } 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<DualCase>): Boolean {
|
fun containsAny(hiddenWordsCase: List<DualCase>): Boolean {
|
||||||
if (hiddenWordsCase.isEmpty()) return false
|
if (hiddenWordsCase.isEmpty()) return false
|
||||||
|
|
||||||
|
|||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* 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.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.quartz.experimental.relationshipStatus.ContactCardEvent
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combineTransform
|
||||||
|
import kotlinx.coroutines.flow.emitAll
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
|
||||||
|
class UserCardsCache : UserDependencies {
|
||||||
|
val receivedCards = MutableStateFlow(mapOf<User, AddressableNote>())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
|
||||||
|
val cardBy = receivedCards.value[author]
|
||||||
|
|
||||||
|
// if it's already there, quick exit
|
||||||
|
if (cardBy != null && cardBy == note) return
|
||||||
|
|
||||||
|
receivedCards.update {
|
||||||
|
val author = note.author
|
||||||
|
if (author == null) {
|
||||||
|
it
|
||||||
|
} else {
|
||||||
|
it + (author to note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeCard(note: AddressableNote) {
|
||||||
|
val author = note.author ?: return
|
||||||
|
val cardBy = receivedCards.value[author]
|
||||||
|
|
||||||
|
// if it's not already there, quick exit
|
||||||
|
if (cardBy == null || cardBy != note) return
|
||||||
|
|
||||||
|
receivedCards.update {
|
||||||
|
val author = note.author
|
||||||
|
if (author == null) {
|
||||||
|
it
|
||||||
|
} else {
|
||||||
|
val reportsByInner = it[author]
|
||||||
|
if (reportsByInner == null) {
|
||||||
|
it
|
||||||
|
} else {
|
||||||
|
it - author
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rankFlow(trustProviderList: TrustProviderListState) =
|
||||||
|
combineTransform(receivedCards, trustProviderList.liveUserRankProvider) { cards, provider ->
|
||||||
|
if (provider != null) {
|
||||||
|
val flow =
|
||||||
|
cards.firstNotNullOfOrNull {
|
||||||
|
if (it.key.pubkeyHex == provider.pubkey) {
|
||||||
|
it.value
|
||||||
|
.flow()
|
||||||
|
.metadata.stateFlow
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flow != null) {
|
||||||
|
emitAll(flow)
|
||||||
|
} else {
|
||||||
|
emit(null)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emit(null)
|
||||||
|
}
|
||||||
|
}.map {
|
||||||
|
(it?.note?.event as? ContactCardEvent)?.rank()
|
||||||
|
}.flowOn(Dispatchers.IO)
|
||||||
|
}
|
||||||
+1
-1
@@ -47,7 +47,7 @@ class UserFinderFilterAssembler(
|
|||||||
UserOutboxFinderSubAssembler(client, cache, failureTracker, ::allKeys),
|
UserOutboxFinderSubAssembler(client, cache, failureTracker, ::allKeys),
|
||||||
UserWatcherSubAssembler(client, cache, ::allKeys),
|
UserWatcherSubAssembler(client, cache, ::allKeys),
|
||||||
UserReportsSubAssembler(client, cache, ::allKeys),
|
UserReportsSubAssembler(client, cache, ::allKeys),
|
||||||
UserCardsSubAssembler(client, ::allKeys),
|
UserCardsSubAssembler(client, cache, ::allKeys),
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun invalidateFilters() = group.forEach { it.invalidateFilters() }
|
override fun invalidateFilters() = group.forEach { it.invalidateFilters() }
|
||||||
|
|||||||
+15
-1
@@ -44,7 +44,6 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.flow
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
@@ -602,6 +601,21 @@ fun observeUserReportCount(
|
|||||||
return flow.collectAsStateWithLifecycle(0)
|
return flow.collectAsStateWithLifecycle(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
||||||
|
@Composable
|
||||||
|
fun observeUserContactCardsScore(
|
||||||
|
user: User,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
): State<Int?> {
|
||||||
|
// Subscribe in the relay for changes in the metadata of this user.
|
||||||
|
UserFinderFilterAssemblerSubscription(user, accountViewModel)
|
||||||
|
|
||||||
|
// Subscribe in the LocalCache for changes that arrive in the device
|
||||||
|
val flow = remember(user) { user.cards().rankFlow(accountViewModel.account.trustProviderList) }
|
||||||
|
|
||||||
|
return flow.collectAsStateWithLifecycle(null)
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun observeUserStatuses(
|
fun observeUserStatuses(
|
||||||
|
|||||||
+16
-23
@@ -20,7 +20,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers
|
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
|
||||||
import com.vitorpamplona.quartz.experimental.relationshipStatus.ContactCardEvent
|
import com.vitorpamplona.quartz.experimental.relationshipStatus.ContactCardEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||||
@@ -29,27 +28,21 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
|||||||
|
|
||||||
val ContactCardKindList = listOf(ContactCardEvent.KIND)
|
val ContactCardKindList = listOf(ContactCardEvent.KIND)
|
||||||
|
|
||||||
fun filterContactCardsToKeysFromTrusted(
|
fun filterContactCardsToTargetKeysFromTrustedAccountsInTheRelay(
|
||||||
targets: Set<HexKey>,
|
targets: Set<HexKey>,
|
||||||
trustedAccounts: Map<NormalizedRelayUrl, Set<HexKey>>,
|
trustedAccounts: List<HexKey>,
|
||||||
since: SincePerRelayMap?,
|
relay: NormalizedRelayUrl,
|
||||||
): List<RelayBasedFilter> {
|
since: Long?,
|
||||||
if (targets.isEmpty() || trustedAccounts.isEmpty()) return emptyList()
|
): RelayBasedFilter? {
|
||||||
val sortedTargets = mapOf("d" to targets.sorted())
|
if (targets.isEmpty() || trustedAccounts.isEmpty()) return null
|
||||||
return trustedAccounts.mapNotNull { relayAuthors ->
|
return RelayBasedFilter(
|
||||||
if (relayAuthors.value.isNotEmpty()) {
|
relay = relay,
|
||||||
RelayBasedFilter(
|
filter =
|
||||||
relay = relayAuthors.key,
|
Filter(
|
||||||
filter =
|
kinds = ContactCardKindList,
|
||||||
Filter(
|
authors = trustedAccounts,
|
||||||
kinds = ContactCardKindList,
|
tags = mapOf("d" to targets.sorted()),
|
||||||
authors = relayAuthors.value.sorted(),
|
since = since,
|
||||||
tags = sortedTargets,
|
),
|
||||||
since = since?.get(relayAuthors.key)?.time,
|
)
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+62
-58
@@ -20,10 +20,11 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers
|
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.User
|
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.eoseManagers.SingleSubEoseManager
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState
|
||||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast
|
|
||||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||||
import com.vitorpamplona.ammolite.relays.filters.MutableTime
|
import com.vitorpamplona.ammolite.relays.filters.MutableTime
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
@@ -32,26 +33,24 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
|||||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
import com.vitorpamplona.quartz.utils.mapOfSet
|
import com.vitorpamplona.quartz.utils.mapOfSet
|
||||||
|
import kotlin.collections.component1
|
||||||
|
import kotlin.collections.component2
|
||||||
|
|
||||||
class UserCardsSubAssembler(
|
class UserCardsSubAssembler(
|
||||||
client: INostrClient,
|
client: INostrClient,
|
||||||
|
val cache: LocalCache,
|
||||||
allKeys: () -> Set<UserFinderQueryState>,
|
allKeys: () -> Set<UserFinderQueryState>,
|
||||||
) : SingleSubEoseManager<UserFinderQueryState>(client, allKeys) {
|
) : SingleSubEoseManager<UserFinderQueryState>(client, allKeys) {
|
||||||
var lastUsersOnFilter: Set<User> = emptySet()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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: EOSEAccountFast<User> = EOSEAccountFast<User>(2000)
|
|
||||||
|
|
||||||
override fun newEose(
|
override fun newEose(
|
||||||
relay: NormalizedRelayUrl,
|
relay: NormalizedRelayUrl,
|
||||||
time: Long,
|
time: Long,
|
||||||
filters: List<Filter>?,
|
filters: List<Filter>?,
|
||||||
) {
|
) {
|
||||||
lastUsersOnFilter.forEach {
|
filters?.forEach { filter ->
|
||||||
latestEOSEs.newEose(it, relay, time)
|
filter.tags?.get("p")?.forEach {
|
||||||
|
val targetUser = cache.getUserIfExists(it)
|
||||||
|
targetUser?.cardsOrNull()?.latestEOSEs?.newEose(relay, time)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
super.newEose(relay, time, filters)
|
super.newEose(relay, time, filters)
|
||||||
}
|
}
|
||||||
@@ -62,7 +61,7 @@ class UserCardsSubAssembler(
|
|||||||
): List<RelayBasedFilter>? {
|
): List<RelayBasedFilter>? {
|
||||||
if (keys.isEmpty()) return null
|
if (keys.isEmpty()) return null
|
||||||
|
|
||||||
lastUsersOnFilter = keys.mapTo(mutableSetOf()) { it.user }
|
val lastUsersOnFilter = keys.mapTo(mutableSetOf()) { it.user }
|
||||||
|
|
||||||
if (lastUsersOnFilter.isEmpty()) return null
|
if (lastUsersOnFilter.isEmpty()) return null
|
||||||
|
|
||||||
@@ -71,7 +70,7 @@ class UserCardsSubAssembler(
|
|||||||
val trustedAccounts: Map<NormalizedRelayUrl, Set<HexKey>> =
|
val trustedAccounts: Map<NormalizedRelayUrl, Set<HexKey>> =
|
||||||
mapOfSet {
|
mapOfSet {
|
||||||
accounts.forEach { account ->
|
accounts.forEach { account ->
|
||||||
account.outboxRelays.flow.value.map {
|
account.homeRelays.flow.value.map {
|
||||||
add(it, account.userProfile().pubkeyHex)
|
add(it, account.userProfile().pubkeyHex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,65 +81,70 @@ class UserCardsSubAssembler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return groupByRelayPresence(lastUsersOnFilter, latestEOSEs, trustedAccounts.keys)
|
return trustedAccounts
|
||||||
.map { group ->
|
.flatMap { (relay, trustedUsersInThisRelay) ->
|
||||||
val groupIds = group.map { it.pubkeyHex }.toSet()
|
// this relay + accounts are where we could find cards.
|
||||||
if (groupIds.isNotEmpty()) {
|
// we might have already loaded them, so let's separate new targets that were checked before from the others
|
||||||
val minEOSEs = findMinimumEOSEsForUsers(group, latestEOSEs)
|
val groups = groupByRelayPresence(lastUsersOnFilter, relay)
|
||||||
filterContactCardsToKeysFromTrusted(groupIds, trustedAccounts, minEOSEs)
|
val trustedAccounts = trustedUsersInThisRelay.sorted()
|
||||||
} else {
|
listOfNotNull(
|
||||||
emptyList()
|
filterContactCardsToTargetKeysFromTrustedAccountsInTheRelay(
|
||||||
}
|
targets = groups.usersWithoutEose.toHexSet(),
|
||||||
}.flatten()
|
trustedAccounts = trustedAccounts,
|
||||||
|
relay = relay,
|
||||||
|
since = null,
|
||||||
|
),
|
||||||
|
filterContactCardsToTargetKeysFromTrustedAccountsInTheRelay(
|
||||||
|
targets = groups.usersWithEose.toHexSet(),
|
||||||
|
trustedAccounts = trustedAccounts,
|
||||||
|
relay = relay,
|
||||||
|
since = findMinimumEOSEsForUsers(groups.usersWithEose, relay),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PresenceGroup(
|
||||||
|
val usersWithEose: List<User> = emptyList(),
|
||||||
|
val usersWithoutEose: List<User> = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
fun groupByRelayPresence(
|
fun groupByRelayPresence(
|
||||||
users: Iterable<User>,
|
targetUsers: Iterable<User>,
|
||||||
eoseCache: EOSEAccountFast<User>,
|
relay: NormalizedRelayUrl,
|
||||||
inRelays: Set<NormalizedRelayUrl>,
|
): PresenceGroup {
|
||||||
): Collection<List<User>> {
|
if (targetUsers.none()) return PresenceGroup()
|
||||||
if (users.none()) return emptyList()
|
|
||||||
|
|
||||||
val relaySnapshot = inRelays.toSet()
|
val groups =
|
||||||
|
targetUsers.groupBy { user ->
|
||||||
return users
|
relay in
|
||||||
.groupBy { user ->
|
user
|
||||||
val relaysForUser = eoseCache.sinceRelaySet(user)
|
.cards()
|
||||||
if (relaysForUser.isNullOrEmpty() || relaySnapshot.isEmpty()) {
|
.latestEOSEs.relayList.keys
|
||||||
null
|
|
||||||
} else {
|
|
||||||
val intersection = relaysForUser.filter { it in relaySnapshot }.sorted()
|
|
||||||
if (intersection.isEmpty()) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
intersection.hashCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.values
|
|
||||||
.map {
|
|
||||||
// important to keep in order otherwise the Relay thinks the filter has changed and we REQ again
|
|
||||||
it.sortedBy { it.pubkeyHex }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return PresenceGroup(
|
||||||
|
groups[true]?.sortedBy { it.pubkeyHex } ?: emptyList(),
|
||||||
|
groups[false]?.sortedBy { it.pubkeyHex } ?: emptyList(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findMinimumEOSEsForUsers(
|
fun findMinimumEOSEsForUsers(
|
||||||
users: List<User>,
|
users: List<User>,
|
||||||
eoseCache: EOSEAccountFast<User>,
|
relay: NormalizedRelayUrl,
|
||||||
): SincePerRelayMap {
|
): Long? {
|
||||||
val minLatestEOSEs = mutableMapOf<NormalizedRelayUrl, MutableTime>()
|
var min: MutableTime? = null
|
||||||
|
|
||||||
users.forEach {
|
users.forEach {
|
||||||
eoseCache.since(it)?.forEach {
|
val eose = it.cards().latestEOSEs.since()[relay]
|
||||||
val minEose = minLatestEOSEs[it.key]
|
if (min != null && eose != null) {
|
||||||
if (minEose == null) {
|
min.updateIfOlder(eose.time)
|
||||||
minLatestEOSEs.put(it.key, it.value.copy())
|
} else if (eose != null) {
|
||||||
} else {
|
min = MutableTime(eose.time)
|
||||||
minEose.updateIfOlder(it.value.time)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return minLatestEOSEs
|
return min?.time
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun distinct(key: UserFinderQueryState) = key.user
|
override fun distinct(key: UserFinderQueryState) = key.user
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
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.service.relayClient.reqCommand.user.observeUserInfo
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
|
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||||
@@ -368,11 +369,7 @@ fun BaseUserPicture(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WatchUserCards(baseUser.pubkeyHex, accountViewModel) { score ->
|
ObserveAndRenderUserCards(baseUser, size, Modifier.align(Alignment.BottomCenter), accountViewModel)
|
||||||
if (score != null) {
|
|
||||||
ScoreTag(score, size, Modifier.align(Alignment.BottomCenter))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,15 +382,21 @@ fun BaseUserPicture(
|
|||||||
outerModifier: Modifier = Modifier.size(size),
|
outerModifier: Modifier = Modifier.size(size),
|
||||||
) {
|
) {
|
||||||
Box(outerModifier, contentAlignment = Alignment.TopEnd) {
|
Box(outerModifier, contentAlignment = Alignment.TopEnd) {
|
||||||
LoadUserProfilePicture(baseUserHex, accountViewModel) { userProfilePicture, userName ->
|
LoadUser(baseUserHex, accountViewModel) {
|
||||||
InnerUserPicture(
|
if (it != null) {
|
||||||
userHex = baseUserHex,
|
ObserveAndDrawInnerUserPicture(it, size, accountViewModel, innerModifier)
|
||||||
userPicture = userProfilePicture,
|
|
||||||
userName = userName,
|
ObserveAndRenderUserCards(it, size, Modifier.align(Alignment.BottomCenter), accountViewModel)
|
||||||
size = size,
|
} else {
|
||||||
modifier = innerModifier,
|
InnerUserPicture(
|
||||||
accountViewModel = accountViewModel,
|
userHex = baseUserHex,
|
||||||
)
|
userPicture = null,
|
||||||
|
userName = null,
|
||||||
|
size = size,
|
||||||
|
modifier = innerModifier,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WatchUserFollows(baseUserHex, accountViewModel) { newFollowingState ->
|
WatchUserFollows(baseUserHex, accountViewModel) { newFollowingState ->
|
||||||
@@ -401,15 +404,28 @@ fun BaseUserPicture(
|
|||||||
FollowingIcon(Modifier.size(size.div(3.5f)))
|
FollowingIcon(Modifier.size(size.div(3.5f)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WatchUserCards(baseUserHex, accountViewModel) { score ->
|
|
||||||
if (score != null) {
|
|
||||||
ScoreTag(score, size, Modifier.align(Alignment.BottomCenter))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ObserveAndDrawInnerUserPicture(
|
||||||
|
user: User,
|
||||||
|
size: Dp,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
innerModifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val userProfile by observeUserInfo(user, accountViewModel)
|
||||||
|
|
||||||
|
InnerUserPicture(
|
||||||
|
userHex = user.pubkeyHex,
|
||||||
|
userPicture = userProfile?.profilePicture(),
|
||||||
|
userName = userProfile?.bestName(),
|
||||||
|
size = size,
|
||||||
|
modifier = innerModifier,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun ScoreTag55Preview() {
|
fun ScoreTag55Preview() {
|
||||||
@@ -579,17 +595,15 @@ fun WatchUserFollows(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WatchUserCards(
|
fun ObserveAndRenderUserCards(
|
||||||
userHex: String,
|
user: User,
|
||||||
|
size: Dp,
|
||||||
|
modifier: Modifier,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
onScoreChanges: @Composable (Int?) -> Unit,
|
|
||||||
) {
|
) {
|
||||||
val flow =
|
val score by observeUserContactCardsScore(user, accountViewModel)
|
||||||
remember(userHex) {
|
|
||||||
accountViewModel.account.loadUserCardFlow(userHex)
|
|
||||||
}
|
|
||||||
|
|
||||||
val score by flow.collectAsStateWithLifecycle(null)
|
score?.let {
|
||||||
|
ScoreTag(it, size, modifier)
|
||||||
onScoreChanges(score)
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user