mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-07-22 07:48:27 +00:00
Compare commits
1 Commits
b6bf3b8a70
...
claude/pag
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4435b8c1bd |
@@ -56,11 +56,13 @@ class AccountFilterAssembler(
|
||||
failureTracker: RelayOfflineTracker,
|
||||
scope: CoroutineScope,
|
||||
) : ComposeSubscriptionManager<AccountQueryState>() {
|
||||
val giftWraps = AccountGiftWrapsEoseManager(client, ::allKeys)
|
||||
|
||||
val group =
|
||||
listOf(
|
||||
AccountMetadataEoseManager(client, ::allKeys),
|
||||
AccountFollowsLoaderSubAssembler(client, cache, scope, authenticator, failureTracker, ::allKeys),
|
||||
AccountGiftWrapsEoseManager(client, ::allKeys),
|
||||
giftWraps,
|
||||
AccountDraftsEoseManager(client, ::allKeys),
|
||||
AccountNotificationsEoseFromInboxRelaysManager(client, ::allKeys),
|
||||
AccountNotificationsEoseFromRandomRelaysManager(client, ::allKeys),
|
||||
|
||||
@@ -26,10 +26,16 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.Account
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -37,23 +43,90 @@ class AccountGiftWrapsEoseManager(
|
||||
client: INostrClient,
|
||||
allKeys: () -> Set<AccountQueryState>,
|
||||
) : PerUserEoseManager<AccountQueryState>(client, allKeys) {
|
||||
companion object {
|
||||
const val TARGET_CHATROOMS = 50
|
||||
const val BATCH_SIZE = 100
|
||||
}
|
||||
|
||||
override fun user(key: AccountQueryState) = key.account.userProfile()
|
||||
|
||||
// Pagination state
|
||||
private var paginationCursor: Long? = null
|
||||
private var paginationStartTime: Long = TimeUtils.now()
|
||||
private var initialLoadComplete = false
|
||||
private var oldestEventInBatch: Long? = null
|
||||
private var eventsInBatch = 0
|
||||
private var batchEoseReceived = false
|
||||
|
||||
private val _hasMore = MutableStateFlow(true)
|
||||
val hasMore = _hasMore.asStateFlow()
|
||||
|
||||
private val _isLoadingMore = MutableStateFlow(false)
|
||||
val isLoadingMore = _isLoadingMore.asStateFlow()
|
||||
|
||||
private var currentKey: AccountQueryState? = null
|
||||
|
||||
override fun updateFilter(
|
||||
key: AccountQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter> {
|
||||
// Only loads DMs if the account is writeable
|
||||
return if (key.account.isWriteable()) {
|
||||
key.account.dmRelays.flow.value.flatMap { relay ->
|
||||
if (!key.account.isWriteable()) return emptyList()
|
||||
|
||||
currentKey = key
|
||||
val pubkey = user(key).pubkeyHex
|
||||
|
||||
if (initialLoadComplete && !_isLoadingMore.value) {
|
||||
// Live mode: normal since-based tracking
|
||||
return key.account.dmRelays.flow.value.flatMap { relay ->
|
||||
filterGiftWrapsToPubkey(
|
||||
relay = relay,
|
||||
pubkey = user(key).pubkeyHex,
|
||||
pubkey = pubkey,
|
||||
since = since?.get(relay)?.time,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
// Pagination mode (initial load or load-more)
|
||||
if (batchEoseReceived) {
|
||||
val roomCount =
|
||||
key.account.chatroomList.rooms
|
||||
.size()
|
||||
|
||||
if (roomCount >= TARGET_CHATROOMS || eventsInBatch == 0) {
|
||||
// Pagination phase done
|
||||
if (!initialLoadComplete) {
|
||||
initialLoadComplete = true
|
||||
}
|
||||
_hasMore.value = eventsInBatch > 0
|
||||
_isLoadingMore.value = false
|
||||
batchEoseReceived = false
|
||||
|
||||
// Switch to live mode
|
||||
return key.account.dmRelays.flow.value.flatMap { relay ->
|
||||
filterGiftWrapsToPubkey(
|
||||
relay = relay,
|
||||
pubkey = pubkey,
|
||||
since = paginationStartTime.minus(TimeUtils.twoDays()),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Need more: advance cursor
|
||||
paginationCursor = oldestEventInBatch?.minus(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Reset batch tracking for new page
|
||||
batchEoseReceived = false
|
||||
eventsInBatch = 0
|
||||
oldestEventInBatch = null
|
||||
|
||||
return key.account.dmRelays.flow.value.flatMap { relay ->
|
||||
filterGiftWrapsToPubkeyPaginated(
|
||||
relay = relay,
|
||||
pubkey = pubkey,
|
||||
until = paginationCursor,
|
||||
limit = BATCH_SIZE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +145,50 @@ class AccountGiftWrapsEoseManager(
|
||||
},
|
||||
)
|
||||
|
||||
return super.newSub(key)
|
||||
currentKey = key
|
||||
paginationStartTime = TimeUtils.now()
|
||||
initialLoadComplete = false
|
||||
paginationCursor = null
|
||||
batchEoseReceived = false
|
||||
eventsInBatch = 0
|
||||
oldestEventInBatch = null
|
||||
_hasMore.value = true
|
||||
_isLoadingMore.value = false
|
||||
|
||||
return requestNewSubscription(
|
||||
object : SubscriptionListener {
|
||||
override fun onEose(
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
if (initialLoadComplete && !_isLoadingMore.value) {
|
||||
newEose(key, relay, TimeUtils.now(), forFilters)
|
||||
} else {
|
||||
batchEoseReceived = true
|
||||
invalidateFilters()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEvent(
|
||||
event: com.vitorpamplona.quartz.nip01Core.core.Event,
|
||||
isLive: Boolean,
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
if (!initialLoadComplete || _isLoadingMore.value) {
|
||||
eventsInBatch++
|
||||
val eventTime = event.createdAt
|
||||
val oldest = oldestEventInBatch
|
||||
if (oldest == null || eventTime < oldest) {
|
||||
oldestEventInBatch = eventTime
|
||||
}
|
||||
}
|
||||
if (isLive) {
|
||||
newEose(key, relay, TimeUtils.now(), forFilters)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun endSub(
|
||||
@@ -82,4 +198,13 @@ class AccountGiftWrapsEoseManager(
|
||||
super.endSub(key, subId)
|
||||
userJobMap[key]?.forEach { it.cancel() }
|
||||
}
|
||||
|
||||
fun loadMore() {
|
||||
if (!initialLoadComplete || !_hasMore.value || _isLoadingMore.value) return
|
||||
_isLoadingMore.value = true
|
||||
batchEoseReceived = false
|
||||
eventsInBatch = 0
|
||||
oldestEventInBatch = null
|
||||
invalidateFilters()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,3 +46,25 @@ fun filterGiftWrapsToPubkey(
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun filterGiftWrapsToPubkeyPaginated(
|
||||
relay: NormalizedRelayUrl,
|
||||
pubkey: HexKey?,
|
||||
until: Long?,
|
||||
limit: Int,
|
||||
): List<RelayBasedFilter> {
|
||||
if (pubkey.isNullOrEmpty()) return emptyList()
|
||||
|
||||
return listOf(
|
||||
RelayBasedFilter(
|
||||
relay = relay,
|
||||
filter =
|
||||
Filter(
|
||||
kinds = listOf(GiftWrapEvent.KIND),
|
||||
tags = mapOf("p" to listOf(pubkey)),
|
||||
until = until,
|
||||
limit = limit,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,15 +21,25 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.feed
|
||||
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||
@@ -102,6 +112,27 @@ private fun FeedLoaded(
|
||||
) {
|
||||
val items by loaded.feed.collectAsStateWithLifecycle()
|
||||
|
||||
val giftWrapsManager = accountViewModel.dataSources().account.giftWraps
|
||||
val isLoadingMore by giftWrapsManager.isLoadingMore.collectAsStateWithLifecycle()
|
||||
val hasMore by giftWrapsManager.hasMore.collectAsStateWithLifecycle()
|
||||
|
||||
val shouldLoadMore by remember {
|
||||
derivedStateOf {
|
||||
val lastVisibleIndex =
|
||||
listState.layoutInfo.visibleItemsInfo
|
||||
.lastOrNull()
|
||||
?.index ?: 0
|
||||
val totalItems = listState.layoutInfo.totalItemsCount
|
||||
lastVisibleIndex >= totalItems - 5 && !isLoadingMore && hasMore && items.list.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(shouldLoadMore) {
|
||||
if (shouldLoadMore) {
|
||||
giftWrapsManager.loadMore()
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = FeedPadding,
|
||||
state = listState,
|
||||
@@ -122,5 +153,23 @@ private fun FeedLoaded(
|
||||
thickness = DividerThickness,
|
||||
)
|
||||
}
|
||||
|
||||
if (isLoadingMore) {
|
||||
item(key = "loading_more") {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
strokeWidth = 2.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user