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.privateOutbox.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
||||
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
||||
import com.vitorpamplona.quartz.nip40Expiration.isExpirationBefore
|
||||
import com.vitorpamplona.quartz.nip40Expiration.isExpired
|
||||
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.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.cache.LargeCache
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
@@ -1224,33 +1221,15 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
relay: NormalizedRelayUrl?,
|
||||
wasVerified: Boolean,
|
||||
): Boolean {
|
||||
val version = getOrCreateNote(event.id)
|
||||
val note = getOrCreateAddressableNote(event.address())
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
val note = event.toAddressableNote()
|
||||
val new = consumeBaseReplaceable(event, relay, wasVerified)
|
||||
|
||||
val isVerified =
|
||||
if (version.event == null && (wasVerified || justVerify(event))) {
|
||||
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
|
||||
if (new) {
|
||||
author.statusState().addStatus(note)
|
||||
}
|
||||
|
||||
return false
|
||||
return new
|
||||
}
|
||||
|
||||
fun Event.toNote() = getOrCreateNote(id)
|
||||
@@ -1513,6 +1492,10 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
getUserIfExists(deletedEvent.aboutUser())?.cardsOrNull()?.removeCard(deleteNote)
|
||||
}
|
||||
|
||||
if (deleteNote is AddressableNote && deletedEvent is StatusEvent) {
|
||||
deleteNote.author?.statusStateOrNull()?.removeStatus(deleteNote)
|
||||
}
|
||||
|
||||
if (deletedEvent is TorrentCommentEvent) {
|
||||
deletedEvent.torrentIds()?.let {
|
||||
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 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(
|
||||
note: Note,
|
||||
otsVerifCache: VerificationStateCache,
|
||||
@@ -2628,10 +2594,15 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
getAddressableNoteIfExists(it.address)?.removeReport(note)
|
||||
}
|
||||
}
|
||||
|
||||
if (note is AddressableNote && noteEvent is ContactCardEvent) {
|
||||
getUserIfExists(noteEvent.aboutUser())?.cardsOrNull()?.removeCard(note)
|
||||
}
|
||||
|
||||
if (note is AddressableNote && noteEvent is StatusEvent) {
|
||||
note.author?.statusStateOrNull()?.removeStatus(note)
|
||||
}
|
||||
|
||||
note.clearFlow()
|
||||
|
||||
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.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.UserState
|
||||
@@ -555,21 +554,7 @@ fun observeUserStatuses(
|
||||
// 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
|
||||
.flow()
|
||||
.statuses
|
||||
.stateFlow
|
||||
.sample(1000)
|
||||
.mapLatest { userState ->
|
||||
LocalCache.findStatusesForUser(userState.user)
|
||||
}.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
}
|
||||
|
||||
return flow.collectAsStateWithLifecycle(persistentListOf())
|
||||
return user.statusState().statuses.collectAsStateWithLifecycle(persistentListOf())
|
||||
}
|
||||
|
||||
@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.user.observeUserContactCardsFollowerCount
|
||||
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.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
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.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup.AccountBackupDialog
|
||||
@@ -274,15 +274,15 @@ private fun EditStatusBoxes(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
LoadStatuses(user = baseAccountUser, accountViewModel) { statuses ->
|
||||
if (statuses.isEmpty()) {
|
||||
StatusEditBar(accountViewModel = accountViewModel, nav = nav)
|
||||
} else {
|
||||
statuses.forEach {
|
||||
val noteStatus by observeNote(it, accountViewModel)
|
||||
val statuses by observeUserStatuses(baseAccountUser, 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.LocalCache
|
||||
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.user.observeUserStatuses
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -110,17 +107,6 @@ fun LoadAddressableNote(
|
||||
content(note)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadStatuses(
|
||||
user: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
content: @Composable (ImmutableList<AddressableNote>) -> Unit,
|
||||
) {
|
||||
val userStatuses by observeUserStatuses(user, accountViewModel)
|
||||
|
||||
content(userStatuses)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadOts(
|
||||
note: Note,
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.model.nip01Core.UserMetadataCache
|
||||
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.trustedAssertions.UserCardsCache
|
||||
import com.vitorpamplona.amethyst.commons.util.toShortDisplay
|
||||
@@ -54,6 +55,7 @@ class User(
|
||||
private var reports: UserReportCache? = null
|
||||
private var cards: UserCardsCache? = null
|
||||
private var nip05: UserNip05Cache? = null
|
||||
private var status: UserStatusCache? = null
|
||||
|
||||
var zaps = mapOf<Note, Note?>()
|
||||
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 {
|
||||
if (hiddenWordsCase.isEmpty()) return false
|
||||
|
||||
@@ -301,12 +307,10 @@ class UserFlowSet(
|
||||
// Observers line up here.
|
||||
val usedRelays = UserBundledRefresherFlow(u)
|
||||
val zaps = UserBundledRefresherFlow(u)
|
||||
val statuses = UserBundledRefresherFlow(u)
|
||||
|
||||
fun isInUse(): Boolean =
|
||||
usedRelays.hasObservers() ||
|
||||
zaps.hasObservers() ||
|
||||
statuses.hasObservers()
|
||||
zaps.hasObservers()
|
||||
}
|
||||
|
||||
@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