Moves status to a user property to avoid searching on scrolling
This commit is contained in:
@@ -133,7 +133,6 @@ import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
|
|||||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||||
import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
|
import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
|
||||||
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
||||||
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
|
||||||
import com.vitorpamplona.quartz.nip40Expiration.isExpirationBefore
|
import com.vitorpamplona.quartz.nip40Expiration.isExpirationBefore
|
||||||
import com.vitorpamplona.quartz.nip40Expiration.isExpired
|
import com.vitorpamplona.quartz.nip40Expiration.isExpired
|
||||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
|
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
|
||||||
@@ -198,8 +197,6 @@ import com.vitorpamplona.quartz.utils.Hex
|
|||||||
import com.vitorpamplona.quartz.utils.Log
|
import com.vitorpamplona.quartz.utils.Log
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import com.vitorpamplona.quartz.utils.cache.LargeCache
|
import com.vitorpamplona.quartz.utils.cache.LargeCache
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
@@ -1224,33 +1221,15 @@ object LocalCache : ILocalCache, ICacheProvider {
|
|||||||
relay: NormalizedRelayUrl?,
|
relay: NormalizedRelayUrl?,
|
||||||
wasVerified: Boolean,
|
wasVerified: Boolean,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val version = getOrCreateNote(event.id)
|
|
||||||
val note = getOrCreateAddressableNote(event.address())
|
|
||||||
val author = getOrCreateUser(event.pubKey)
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
val note = event.toAddressableNote()
|
||||||
|
val new = consumeBaseReplaceable(event, relay, wasVerified)
|
||||||
|
|
||||||
val isVerified =
|
if (new) {
|
||||||
if (version.event == null && (wasVerified || justVerify(event))) {
|
author.statusState().addStatus(note)
|
||||||
version.loadEvent(event, author, emptyList())
|
|
||||||
version.moveAllReferencesTo(note)
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
wasVerified
|
|
||||||
}
|
|
||||||
|
|
||||||
// Already processed this event.
|
|
||||||
if (note.event?.id == event.id) return false
|
|
||||||
|
|
||||||
if (event.createdAt > (note.createdAt() ?: 0L) && (isVerified || justVerify(event))) {
|
|
||||||
note.loadEvent(event, author, emptyList())
|
|
||||||
|
|
||||||
author.flowSet?.statuses?.invalidateData()
|
|
||||||
|
|
||||||
refreshNewNoteObservers(note)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return new
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Event.toNote() = getOrCreateNote(id)
|
fun Event.toNote() = getOrCreateNote(id)
|
||||||
@@ -1513,6 +1492,10 @@ object LocalCache : ILocalCache, ICacheProvider {
|
|||||||
getUserIfExists(deletedEvent.aboutUser())?.cardsOrNull()?.removeCard(deleteNote)
|
getUserIfExists(deletedEvent.aboutUser())?.cardsOrNull()?.removeCard(deleteNote)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (deleteNote is AddressableNote && deletedEvent is StatusEvent) {
|
||||||
|
deleteNote.author?.statusStateOrNull()?.removeStatus(deleteNote)
|
||||||
|
}
|
||||||
|
|
||||||
if (deletedEvent is TorrentCommentEvent) {
|
if (deletedEvent is TorrentCommentEvent) {
|
||||||
deletedEvent.torrentIds()?.let {
|
deletedEvent.torrentIds()?.let {
|
||||||
getNoteIfExists(it)?.removeReply(deleteNote)
|
getNoteIfExists(it)?.removeReply(deleteNote)
|
||||||
@@ -2308,23 +2291,6 @@ object LocalCache : ILocalCache, ICacheProvider {
|
|||||||
|
|
||||||
fun getPeopleListNotesFor(user: User): List<AddressableNote> = addressables.filter(PeopleListEvent.KIND, user.pubkeyHex)
|
fun getPeopleListNotesFor(user: User): List<AddressableNote> = addressables.filter(PeopleListEvent.KIND, user.pubkeyHex)
|
||||||
|
|
||||||
fun findStatusesForUser(user: User): ImmutableList<AddressableNote> {
|
|
||||||
checkNotInMainThread()
|
|
||||||
|
|
||||||
return addressables
|
|
||||||
.filter { _, it ->
|
|
||||||
val noteEvent = it.event
|
|
||||||
(
|
|
||||||
noteEvent is StatusEvent &&
|
|
||||||
noteEvent.pubKey == user.pubkeyHex &&
|
|
||||||
!noteEvent.isExpired() &&
|
|
||||||
noteEvent.content.isNotBlank()
|
|
||||||
)
|
|
||||||
}.sortedWith(compareBy({ it.event?.expiration() ?: it.event?.createdAt }, { it.idHex }))
|
|
||||||
.reversed()
|
|
||||||
.toImmutableList()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun findEarliestOtsForNote(
|
suspend fun findEarliestOtsForNote(
|
||||||
note: Note,
|
note: Note,
|
||||||
otsVerifCache: VerificationStateCache,
|
otsVerifCache: VerificationStateCache,
|
||||||
@@ -2628,10 +2594,15 @@ object LocalCache : ILocalCache, ICacheProvider {
|
|||||||
getAddressableNoteIfExists(it.address)?.removeReport(note)
|
getAddressableNoteIfExists(it.address)?.removeReport(note)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note is AddressableNote && noteEvent is ContactCardEvent) {
|
if (note is AddressableNote && noteEvent is ContactCardEvent) {
|
||||||
getUserIfExists(noteEvent.aboutUser())?.cardsOrNull()?.removeCard(note)
|
getUserIfExists(noteEvent.aboutUser())?.cardsOrNull()?.removeCard(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (note is AddressableNote && noteEvent is StatusEvent) {
|
||||||
|
note.author?.statusStateOrNull()?.removeStatus(note)
|
||||||
|
}
|
||||||
|
|
||||||
note.clearFlow()
|
note.clearFlow()
|
||||||
|
|
||||||
notes.remove(note.idHex)
|
notes.remove(note.idHex)
|
||||||
|
|||||||
+1
-16
@@ -30,7 +30,6 @@ import com.vitorpamplona.amethyst.commons.model.nip01Core.UserInfo
|
|||||||
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
|
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
|
||||||
import com.vitorpamplona.amethyst.model.NoteState
|
import com.vitorpamplona.amethyst.model.NoteState
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.model.UserState
|
import com.vitorpamplona.amethyst.model.UserState
|
||||||
@@ -555,21 +554,7 @@ fun observeUserStatuses(
|
|||||||
// Subscribe in the relay for changes in the metadata of this user.
|
// Subscribe in the relay for changes in the metadata of this user.
|
||||||
UserFinderFilterAssemblerSubscription(user, accountViewModel)
|
UserFinderFilterAssemblerSubscription(user, accountViewModel)
|
||||||
|
|
||||||
// Subscribe in the LocalCache for changes that arrive in the device
|
return user.statusState().statuses.collectAsStateWithLifecycle(persistentListOf())
|
||||||
val flow =
|
|
||||||
remember(user) {
|
|
||||||
user
|
|
||||||
.flow()
|
|
||||||
.statuses
|
|
||||||
.stateFlow
|
|
||||||
.sample(1000)
|
|
||||||
.mapLatest { userState ->
|
|
||||||
LocalCache.findStatusesForUser(userState.user)
|
|
||||||
}.distinctUntilChanged()
|
|
||||||
.flowOn(Dispatchers.IO)
|
|
||||||
}
|
|
||||||
|
|
||||||
return flow.collectAsStateWithLifecycle(persistentListOf())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
||||||
|
|||||||
+9
-9
@@ -97,12 +97,12 @@ import com.vitorpamplona.amethyst.model.User
|
|||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
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.observeUserContactCardsFollowerCount
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||||
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserStatuses
|
||||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||||
import com.vitorpamplona.amethyst.ui.note.LoadStatuses
|
|
||||||
import com.vitorpamplona.amethyst.ui.painterRes
|
import com.vitorpamplona.amethyst.ui.painterRes
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup.AccountBackupDialog
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup.AccountBackupDialog
|
||||||
@@ -274,15 +274,15 @@ private fun EditStatusBoxes(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
LoadStatuses(user = baseAccountUser, accountViewModel) { statuses ->
|
val statuses by observeUserStatuses(baseAccountUser, accountViewModel)
|
||||||
if (statuses.isEmpty()) {
|
|
||||||
StatusEditBar(accountViewModel = accountViewModel, nav = nav)
|
|
||||||
} else {
|
|
||||||
statuses.forEach {
|
|
||||||
val noteStatus by observeNote(it, accountViewModel)
|
|
||||||
|
|
||||||
StatusEditBar(noteStatus.note.event?.content, it.address, accountViewModel, nav)
|
if (statuses.isEmpty()) {
|
||||||
}
|
StatusEditBar(accountViewModel = accountViewModel, nav = nav)
|
||||||
|
} else {
|
||||||
|
statuses.forEach {
|
||||||
|
val noteStatus by observeNote(it, accountViewModel)
|
||||||
|
|
||||||
|
StatusEditBar(noteStatus.note.event?.content, it.address, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,14 +38,11 @@ import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActiviti
|
|||||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
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.User
|
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteOts
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteOts
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserStatuses
|
|
||||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
|
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -110,17 +107,6 @@ fun LoadAddressableNote(
|
|||||||
content(note)
|
content(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun LoadStatuses(
|
|
||||||
user: User,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
content: @Composable (ImmutableList<AddressableNote>) -> Unit,
|
|
||||||
) {
|
|
||||||
val userStatuses by observeUserStatuses(user, accountViewModel)
|
|
||||||
|
|
||||||
content(userStatuses)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LoadOts(
|
fun LoadOts(
|
||||||
note: Note,
|
note: Note,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import androidx.compose.runtime.Immutable
|
|||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import com.vitorpamplona.amethyst.commons.model.nip01Core.UserMetadataCache
|
import com.vitorpamplona.amethyst.commons.model.nip01Core.UserMetadataCache
|
||||||
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.UserNip05Cache
|
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.UserNip05Cache
|
||||||
|
import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.UserStatusCache
|
||||||
import com.vitorpamplona.amethyst.commons.model.nip56Reports.UserReportCache
|
import com.vitorpamplona.amethyst.commons.model.nip56Reports.UserReportCache
|
||||||
import com.vitorpamplona.amethyst.commons.model.trustedAssertions.UserCardsCache
|
import com.vitorpamplona.amethyst.commons.model.trustedAssertions.UserCardsCache
|
||||||
import com.vitorpamplona.amethyst.commons.util.toShortDisplay
|
import com.vitorpamplona.amethyst.commons.util.toShortDisplay
|
||||||
@@ -54,6 +55,7 @@ class User(
|
|||||||
private var reports: UserReportCache? = null
|
private var reports: UserReportCache? = null
|
||||||
private var cards: UserCardsCache? = null
|
private var cards: UserCardsCache? = null
|
||||||
private var nip05: UserNip05Cache? = null
|
private var nip05: UserNip05Cache? = null
|
||||||
|
private var status: UserStatusCache? = null
|
||||||
|
|
||||||
var zaps = mapOf<Note, Note?>()
|
var zaps = mapOf<Note, Note?>()
|
||||||
private set
|
private set
|
||||||
@@ -218,6 +220,10 @@ class User(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun statusStateOrNull(): UserStatusCache? = status
|
||||||
|
|
||||||
|
fun statusState(): UserStatusCache = status ?: UserStatusCache().also { status = it }
|
||||||
|
|
||||||
fun containsAny(hiddenWordsCase: List<DualCase>): Boolean {
|
fun containsAny(hiddenWordsCase: List<DualCase>): Boolean {
|
||||||
if (hiddenWordsCase.isEmpty()) return false
|
if (hiddenWordsCase.isEmpty()) return false
|
||||||
|
|
||||||
@@ -301,12 +307,10 @@ class UserFlowSet(
|
|||||||
// Observers line up here.
|
// Observers line up here.
|
||||||
val usedRelays = UserBundledRefresherFlow(u)
|
val usedRelays = UserBundledRefresherFlow(u)
|
||||||
val zaps = UserBundledRefresherFlow(u)
|
val zaps = UserBundledRefresherFlow(u)
|
||||||
val statuses = UserBundledRefresherFlow(u)
|
|
||||||
|
|
||||||
fun isInUse(): Boolean =
|
fun isInUse(): Boolean =
|
||||||
usedRelays.hasObservers() ||
|
usedRelays.hasObservers() ||
|
||||||
zaps.hasObservers() ||
|
zaps.hasObservers()
|
||||||
statuses.hasObservers()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|||||||
+59
@@ -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.commons.model.nip38UserStatuses
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.commons.model.AddressableNote
|
||||||
|
import com.vitorpamplona.amethyst.commons.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.commons.model.UserDependencies
|
||||||
|
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
|
||||||
|
class UserStatusCache : UserDependencies {
|
||||||
|
val statuses = MutableStateFlow<ImmutableList<AddressableNote>>(persistentListOf())
|
||||||
|
|
||||||
|
val sortModel: Comparator<Note> =
|
||||||
|
compareBy(
|
||||||
|
{ it.event?.expiration() ?: it.event?.createdAt },
|
||||||
|
{ it.idHex },
|
||||||
|
)
|
||||||
|
|
||||||
|
fun addStatus(note: AddressableNote) {
|
||||||
|
// if it's already there, quick exit
|
||||||
|
if (statuses.value.contains(note) || note.event?.content.isNullOrBlank()) return
|
||||||
|
|
||||||
|
statuses.update {
|
||||||
|
(it + note).sortedWith(sortModel).toImmutableList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeStatus(deleteNote: AddressableNote) {
|
||||||
|
// if it's not already there, quick exit
|
||||||
|
if (!statuses.value.contains(deleteNote)) return
|
||||||
|
|
||||||
|
statuses.update {
|
||||||
|
(it - deleteNote).toImmutableList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user