update metadata loading
This commit is contained in:
@@ -85,6 +85,7 @@ import com.vitorpamplona.amethyst.commons.ui.relay.RelayStatusCard
|
||||
import com.vitorpamplona.amethyst.commons.ui.screens.MessagesPlaceholder
|
||||
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
|
||||
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
||||
import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator
|
||||
import com.vitorpamplona.amethyst.desktop.ui.BookmarksScreen
|
||||
import com.vitorpamplona.amethyst.desktop.ui.ComposeNoteDialog
|
||||
import com.vitorpamplona.amethyst.desktop.ui.FeedScreen
|
||||
@@ -251,6 +252,17 @@ fun App(
|
||||
val accountState by accountManager.accountState.collectAsState()
|
||||
val scope = remember { CoroutineScope(SupervisorJob() + Dispatchers.Main) }
|
||||
|
||||
// Subscriptions coordinator for metadata/reactions loading
|
||||
val subscriptionsCoordinator =
|
||||
remember(relayManager, localCache) {
|
||||
DesktopRelaySubscriptionsCoordinator(
|
||||
client = relayManager.client,
|
||||
scope = scope,
|
||||
indexRelays = relayManager.availableRelays.value,
|
||||
localCache = localCache,
|
||||
)
|
||||
}
|
||||
|
||||
// Try to load saved account on startup
|
||||
DisposableEffect(Unit) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
@@ -260,7 +272,12 @@ fun App(
|
||||
|
||||
relayManager.addDefaultRelays()
|
||||
relayManager.connect()
|
||||
|
||||
// Start subscriptions coordinator
|
||||
subscriptionsCoordinator.start()
|
||||
|
||||
onDispose {
|
||||
subscriptionsCoordinator.clear()
|
||||
relayManager.disconnect()
|
||||
}
|
||||
}
|
||||
@@ -296,6 +313,7 @@ fun App(
|
||||
accountManager = accountManager,
|
||||
account = account,
|
||||
nwcConnection = nwcConnection,
|
||||
subscriptionsCoordinator = subscriptionsCoordinator,
|
||||
onShowComposeDialog = onShowComposeDialog,
|
||||
onShowReplyDialog = onShowReplyDialog,
|
||||
)
|
||||
@@ -324,6 +342,7 @@ fun MainContent(
|
||||
accountManager: AccountManager,
|
||||
account: AccountState.LoggedIn,
|
||||
nwcConnection: Nip47WalletConnect.Nip47URINorm?,
|
||||
subscriptionsCoordinator: DesktopRelaySubscriptionsCoordinator,
|
||||
onShowComposeDialog: () -> Unit,
|
||||
onShowReplyDialog: (com.vitorpamplona.quartz.nip01Core.core.Event) -> Unit,
|
||||
) {
|
||||
@@ -429,6 +448,7 @@ fun MainContent(
|
||||
localCache = localCache,
|
||||
account = account,
|
||||
nwcConnection = nwcConnection,
|
||||
subscriptionsCoordinator = subscriptionsCoordinator,
|
||||
onCompose = onShowComposeDialog,
|
||||
onNavigateToProfile = { pubKeyHex ->
|
||||
onScreenChange(DesktopScreen.UserProfile(pubKeyHex))
|
||||
@@ -454,6 +474,7 @@ fun MainContent(
|
||||
SearchScreen(
|
||||
localCache = localCache,
|
||||
relayManager = relayManager,
|
||||
subscriptionsCoordinator = subscriptionsCoordinator,
|
||||
onNavigateToProfile = { pubKeyHex ->
|
||||
onScreenChange(DesktopScreen.UserProfile(pubKeyHex))
|
||||
},
|
||||
@@ -467,6 +488,7 @@ fun MainContent(
|
||||
localCache = localCache,
|
||||
account = account,
|
||||
nwcConnection = nwcConnection,
|
||||
subscriptionsCoordinator = subscriptionsCoordinator,
|
||||
onNavigateToProfile = { pubKeyHex ->
|
||||
onScreenChange(DesktopScreen.UserProfile(pubKeyHex))
|
||||
},
|
||||
@@ -476,7 +498,7 @@ fun MainContent(
|
||||
onZapFeedback = onZapFeedback,
|
||||
)
|
||||
DesktopScreen.Messages -> MessagesPlaceholder()
|
||||
DesktopScreen.Notifications -> NotificationsScreen(relayManager, account)
|
||||
DesktopScreen.Notifications -> NotificationsScreen(relayManager, account, subscriptionsCoordinator)
|
||||
DesktopScreen.MyProfile ->
|
||||
UserProfileScreen(
|
||||
pubKeyHex = account.pubKeyHex,
|
||||
@@ -484,6 +506,7 @@ fun MainContent(
|
||||
localCache = localCache,
|
||||
account = account,
|
||||
nwcConnection = nwcConnection,
|
||||
subscriptionsCoordinator = subscriptionsCoordinator,
|
||||
onBack = { onScreenChange(DesktopScreen.Feed) },
|
||||
onCompose = onShowComposeDialog,
|
||||
onNavigateToProfile = { pubKeyHex ->
|
||||
@@ -498,6 +521,7 @@ fun MainContent(
|
||||
localCache = localCache,
|
||||
account = account,
|
||||
nwcConnection = nwcConnection,
|
||||
subscriptionsCoordinator = subscriptionsCoordinator,
|
||||
onBack = { onScreenChange(DesktopScreen.Feed) },
|
||||
onCompose = onShowComposeDialog,
|
||||
onNavigateToProfile = { pubKeyHex ->
|
||||
@@ -512,6 +536,7 @@ fun MainContent(
|
||||
localCache = localCache,
|
||||
account = account,
|
||||
nwcConnection = nwcConnection,
|
||||
subscriptionsCoordinator = subscriptionsCoordinator,
|
||||
onBack = { onScreenChange(DesktopScreen.Feed) },
|
||||
onNavigateToProfile = { pubKeyHex ->
|
||||
onScreenChange(DesktopScreen.UserProfile(pubKeyHex))
|
||||
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* 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.desktop.subscriptions
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.assemblers.FeedMetadataCoordinator
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.preload.MetadataPreloader
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.preload.MetadataRateLimiter
|
||||
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
/**
|
||||
* Desktop-specific relay subscriptions coordinator.
|
||||
* Manages metadata and reactions loading with rate limiting and prioritization.
|
||||
*
|
||||
* This coordinator ensures:
|
||||
* - Display names and avatars load before reactions
|
||||
* - Metadata requests are rate-limited (20/sec) to avoid relay flooding
|
||||
* - Subscriptions are batched efficiently
|
||||
*
|
||||
* Usage:
|
||||
* ```
|
||||
* val coordinator = DesktopRelaySubscriptionsCoordinator(
|
||||
* client = relayManager.client,
|
||||
* scope = viewModelScope,
|
||||
* indexRelays = relayManager.availableRelays.value,
|
||||
* )
|
||||
* coordinator.start()
|
||||
*
|
||||
* // In screens:
|
||||
* LaunchedEffect(notes) {
|
||||
* coordinator.loadMetadataForNotes(notes)
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
class DesktopRelaySubscriptionsCoordinator(
|
||||
private val client: INostrClient,
|
||||
private val scope: CoroutineScope,
|
||||
private val indexRelays: Set<NormalizedRelayUrl>,
|
||||
private val localCache: DesktopLocalCache,
|
||||
) {
|
||||
// Rate limiter: 20 requests per second to avoid flooding relays
|
||||
private val rateLimiter = MetadataRateLimiter(maxRequestsPerSecond = 20, scope = scope)
|
||||
|
||||
// Preloader handles metadata + avatar prefetching
|
||||
private val preloader = MetadataPreloader(rateLimiter, imagePrefetcher = null)
|
||||
|
||||
// Feed metadata coordinator with priority queue
|
||||
val feedMetadata =
|
||||
FeedMetadataCoordinator(
|
||||
client = client,
|
||||
scope = scope,
|
||||
indexRelays = indexRelays,
|
||||
preloader = preloader,
|
||||
onEvent = { event, _ ->
|
||||
// Consume metadata events into local cache
|
||||
if (event is MetadataEvent) {
|
||||
localCache.consumeMetadata(event)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* Start the coordinator.
|
||||
* Call once when app starts or user logs in.
|
||||
*/
|
||||
fun start() {
|
||||
// Start rate limiter to process queued metadata requests
|
||||
rateLimiter.start { pubkey ->
|
||||
// When rate limiter dequeues a pubkey, subscribe to its metadata
|
||||
client.openReqSubscription(
|
||||
filters =
|
||||
indexRelays.associateWith {
|
||||
listOf(
|
||||
com.vitorpamplona.quartz.nip01Core.relay.filters.Filter(
|
||||
kinds = listOf(com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent.KIND),
|
||||
authors = listOf(pubkey),
|
||||
limit = 1,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Start feed metadata coordinator
|
||||
feedMetadata.start()
|
||||
}
|
||||
|
||||
/**
|
||||
* Load metadata and reactions for notes.
|
||||
* Delegates to FeedMetadataCoordinator.
|
||||
*/
|
||||
fun loadMetadataForNotes(notes: List<Note>) {
|
||||
feedMetadata.loadMetadataForNotes(notes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load metadata for specific pubkeys.
|
||||
*/
|
||||
fun loadMetadataForPubkeys(pubkeys: List<HexKey>) {
|
||||
feedMetadata.loadMetadataForPubkeys(pubkeys)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load reactions for specific notes.
|
||||
*/
|
||||
fun loadReactionsForNotes(noteIds: List<HexKey>) {
|
||||
feedMetadata.loadReactionsForNotes(noteIds)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all queued requests.
|
||||
* Call when switching accounts or during cleanup.
|
||||
*/
|
||||
fun clear() {
|
||||
feedMetadata.clear()
|
||||
rateLimiter.reset()
|
||||
}
|
||||
}
|
||||
+12
@@ -55,6 +55,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.NoteCard
|
||||
import com.vitorpamplona.amethyst.commons.util.toNoteDisplayData
|
||||
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
|
||||
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
||||
import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark
|
||||
@@ -71,6 +72,7 @@ fun BookmarksScreen(
|
||||
localCache: DesktopLocalCache,
|
||||
account: AccountState.LoggedIn,
|
||||
nwcConnection: com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect.Nip47URINorm? = null,
|
||||
subscriptionsCoordinator: DesktopRelaySubscriptionsCoordinator? = null,
|
||||
onNavigateToProfile: (String) -> Unit = {},
|
||||
onNavigateToThread: (String) -> Unit = {},
|
||||
onZapFeedback: (ZapFeedback) -> Unit = {},
|
||||
@@ -109,6 +111,16 @@ fun BookmarksScreen(
|
||||
}
|
||||
val privateEvents by privateEventState.items.collectAsState()
|
||||
|
||||
// Load metadata for bookmark authors via coordinator
|
||||
LaunchedEffect(publicEvents, privateEvents, subscriptionsCoordinator) {
|
||||
if (subscriptionsCoordinator != null) {
|
||||
val pubkeys = (publicEvents + privateEvents).map { it.pubKey }.distinct()
|
||||
if (pubkeys.isNotEmpty()) {
|
||||
subscriptionsCoordinator.loadMetadataForPubkeys(pubkeys)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to user's bookmark list (kind 30001)
|
||||
rememberSubscription(relayStatuses, account.pubKeyHex, relayManager = relayManager) {
|
||||
val configuredRelays = relayStatuses.keys
|
||||
|
||||
@@ -43,6 +43,7 @@ import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -72,6 +73,7 @@ import com.vitorpamplona.amethyst.commons.util.toNoteDisplayData
|
||||
import com.vitorpamplona.amethyst.desktop.DesktopPreferences
|
||||
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
|
||||
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
||||
import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
@@ -146,6 +148,7 @@ fun FeedScreen(
|
||||
localCache: DesktopLocalCache,
|
||||
account: AccountState.LoggedIn? = null,
|
||||
nwcConnection: com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect.Nip47URINorm? = null,
|
||||
subscriptionsCoordinator: DesktopRelaySubscriptionsCoordinator? = null,
|
||||
onCompose: () -> Unit = {},
|
||||
onNavigateToProfile: (String) -> Unit = {},
|
||||
onNavigateToThread: (String) -> Unit = {},
|
||||
@@ -168,9 +171,14 @@ fun FeedScreen(
|
||||
var feedMode by remember { mutableStateOf(DesktopPreferences.feedMode) }
|
||||
var followedUsers by remember { mutableStateOf<Set<String>>(emptySet()) }
|
||||
var zapsByEvent by remember { mutableStateOf<Map<String, List<ZapReceipt>>>(emptyMap()) }
|
||||
var reactionsByEvent by remember { mutableStateOf<Map<String, Int>>(emptyMap()) }
|
||||
var repliesByEvent by remember { mutableStateOf<Map<String, Int>>(emptyMap()) }
|
||||
var repostsByEvent by remember { mutableStateOf<Map<String, Int>>(emptyMap()) }
|
||||
// Track reaction event IDs per target event to deduplicate
|
||||
var reactionIdsByEvent by remember { mutableStateOf<Map<String, Set<String>>>(emptyMap()) }
|
||||
val reactionsByEvent = reactionIdsByEvent.mapValues { it.value.size }
|
||||
// Track reply/repost event IDs per target event to deduplicate
|
||||
var replyIdsByEvent by remember { mutableStateOf<Map<String, Set<String>>>(emptyMap()) }
|
||||
val repliesByEvent = replyIdsByEvent.mapValues { it.value.size }
|
||||
var repostIdsByEvent by remember { mutableStateOf<Map<String, Set<String>>>(emptyMap()) }
|
||||
val repostsByEvent = repostIdsByEvent.mapValues { it.value.size }
|
||||
var bookmarkList by remember { mutableStateOf<BookmarkListEvent?>(null) }
|
||||
var bookmarkedEventIds by remember { mutableStateOf<Set<String>>(emptySet()) }
|
||||
|
||||
@@ -355,9 +363,10 @@ fun FeedScreen(
|
||||
onEvent = { event, _, _, _ ->
|
||||
if (event is ReactionEvent) {
|
||||
val targetEventId = event.originalPost().firstOrNull() ?: return@createReactionsSubscription
|
||||
reactionsByEvent =
|
||||
reactionsByEvent.toMutableMap().apply {
|
||||
this[targetEventId] = (this[targetEventId] ?: 0) + 1
|
||||
reactionIdsByEvent =
|
||||
reactionIdsByEvent.toMutableMap().apply {
|
||||
val existing = this[targetEventId] ?: emptySet()
|
||||
this[targetEventId] = existing + event.id
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -382,9 +391,10 @@ fun FeedScreen(
|
||||
.lastOrNull()
|
||||
?.get(1) ?: return@createRepliesSubscription
|
||||
if (replyToId in eventIds) {
|
||||
repliesByEvent =
|
||||
repliesByEvent.toMutableMap().apply {
|
||||
this[replyToId] = (this[replyToId] ?: 0) + 1
|
||||
replyIdsByEvent =
|
||||
replyIdsByEvent.toMutableMap().apply {
|
||||
val existing = this[replyToId] ?: emptySet()
|
||||
this[replyToId] = existing + event.id
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -404,9 +414,10 @@ fun FeedScreen(
|
||||
onEvent = { event, _, _, _ ->
|
||||
if (event is RepostEvent) {
|
||||
val targetEventId = event.boostedEventId() ?: return@createRepostsSubscription
|
||||
repostsByEvent =
|
||||
repostsByEvent.toMutableMap().apply {
|
||||
this[targetEventId] = (this[targetEventId] ?: 0) + 1
|
||||
repostIdsByEvent =
|
||||
repostIdsByEvent.toMutableMap().apply {
|
||||
val existing = this[targetEventId] ?: emptySet()
|
||||
this[targetEventId] = existing + event.id
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -415,7 +426,21 @@ fun FeedScreen(
|
||||
|
||||
// Subscribe to metadata for note authors (to enable zaps and populate search cache)
|
||||
val authorPubkeys = events.map { it.pubKey }.distinct()
|
||||
rememberSubscription(relayStatuses, authorPubkeys, relayManager = relayManager) {
|
||||
|
||||
// Use coordinator for rate-limited metadata loading (preferred)
|
||||
LaunchedEffect(authorPubkeys, subscriptionsCoordinator) {
|
||||
if (subscriptionsCoordinator != null && authorPubkeys.isNotEmpty()) {
|
||||
subscriptionsCoordinator.loadMetadataForPubkeys(authorPubkeys)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback subscription if coordinator not available
|
||||
rememberSubscription(relayStatuses, authorPubkeys, subscriptionsCoordinator, relayManager = relayManager) {
|
||||
// Skip if using coordinator
|
||||
if (subscriptionsCoordinator != null) {
|
||||
return@rememberSubscription null
|
||||
}
|
||||
|
||||
val configuredRelays = relayStatuses.keys
|
||||
if (configuredRelays.isEmpty() || authorPubkeys.isEmpty()) {
|
||||
return@rememberSubscription null
|
||||
|
||||
+11
@@ -39,6 +39,7 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -60,6 +61,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.LoadingState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feed.FeedHeader
|
||||
import com.vitorpamplona.amethyst.commons.util.toTimeAgo
|
||||
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
||||
import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
@@ -108,6 +110,7 @@ sealed class NotificationItem(
|
||||
fun NotificationsScreen(
|
||||
relayManager: DesktopRelayConnectionManager,
|
||||
account: AccountState.LoggedIn,
|
||||
subscriptionsCoordinator: DesktopRelaySubscriptionsCoordinator? = null,
|
||||
) {
|
||||
val connectedRelays by relayManager.connectedRelays.collectAsState()
|
||||
val relayStatuses by relayManager.relayStatuses.collectAsState()
|
||||
@@ -123,6 +126,14 @@ fun NotificationsScreen(
|
||||
}
|
||||
val notifications by notificationState.items.collectAsState()
|
||||
|
||||
// Load metadata for notification authors via coordinator
|
||||
LaunchedEffect(notifications, subscriptionsCoordinator) {
|
||||
if (subscriptionsCoordinator != null && notifications.isNotEmpty()) {
|
||||
val pubkeys = notifications.map { it.event.pubKey }.distinct()
|
||||
subscriptionsCoordinator.loadMetadataForPubkeys(pubkeys)
|
||||
}
|
||||
}
|
||||
|
||||
// Track EOSE to know when initial load is complete
|
||||
var eoseReceivedCount by remember { mutableStateOf(0) }
|
||||
val initialLoadComplete = eoseReceivedCount > 0
|
||||
|
||||
@@ -69,6 +69,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.UserSearchCard
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.SearchBarState
|
||||
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
|
||||
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
||||
import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull
|
||||
|
||||
@@ -76,6 +77,7 @@ import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull
|
||||
fun SearchScreen(
|
||||
localCache: DesktopLocalCache,
|
||||
relayManager: DesktopRelayConnectionManager,
|
||||
subscriptionsCoordinator: DesktopRelaySubscriptionsCoordinator? = null,
|
||||
onNavigateToProfile: (String) -> Unit,
|
||||
onNavigateToThread: (String) -> Unit,
|
||||
onNavigateToHashtag: (String) -> Unit = {},
|
||||
|
||||
+35
-12
@@ -41,6 +41,7 @@ import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -68,6 +69,7 @@ import com.vitorpamplona.amethyst.commons.ui.thread.drawReplyLevel
|
||||
import com.vitorpamplona.amethyst.commons.util.toNoteDisplayData
|
||||
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
|
||||
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
||||
import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
|
||||
@@ -87,6 +89,7 @@ fun ThreadScreen(
|
||||
localCache: DesktopLocalCache,
|
||||
account: AccountState.LoggedIn?,
|
||||
nwcConnection: com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect.Nip47URINorm? = null,
|
||||
subscriptionsCoordinator: DesktopRelaySubscriptionsCoordinator? = null,
|
||||
onBack: () -> Unit,
|
||||
onNavigateToProfile: (String) -> Unit = {},
|
||||
onNavigateToThread: (String) -> Unit = {},
|
||||
@@ -121,14 +124,31 @@ fun ThreadScreen(
|
||||
|
||||
// Track zaps per event
|
||||
var zapsByEvent by remember(noteId) { mutableStateOf<Map<String, List<ZapReceipt>>>(emptyMap()) }
|
||||
var reactionsByEvent by remember(noteId) { mutableStateOf<Map<String, Int>>(emptyMap()) }
|
||||
var repliesByEvent by remember(noteId) { mutableStateOf<Map<String, Int>>(emptyMap()) }
|
||||
var repostsByEvent by remember(noteId) { mutableStateOf<Map<String, Int>>(emptyMap()) }
|
||||
// Track reaction event IDs per target event to deduplicate
|
||||
var reactionIdsByEvent by remember(noteId) { mutableStateOf<Map<String, Set<String>>>(emptyMap()) }
|
||||
val reactionsByEvent = reactionIdsByEvent.mapValues { it.value.size }
|
||||
// Track reply/repost event IDs per target event to deduplicate
|
||||
var replyIdsByEvent by remember(noteId) { mutableStateOf<Map<String, Set<String>>>(emptyMap()) }
|
||||
val repliesByEvent = replyIdsByEvent.mapValues { it.value.size }
|
||||
var repostIdsByEvent by remember(noteId) { mutableStateOf<Map<String, Set<String>>>(emptyMap()) }
|
||||
val repostsByEvent = repostIdsByEvent.mapValues { it.value.size }
|
||||
|
||||
// Bookmark state
|
||||
var bookmarkList by remember { mutableStateOf<BookmarkListEvent?>(null) }
|
||||
var bookmarkedEventIds by remember { mutableStateOf<Set<String>>(emptySet()) }
|
||||
|
||||
// Load metadata for thread authors via coordinator
|
||||
LaunchedEffect(rootNote, replyEvents, subscriptionsCoordinator) {
|
||||
if (subscriptionsCoordinator != null) {
|
||||
val pubkeys = mutableListOf<String>()
|
||||
rootNote?.let { pubkeys.add(it.pubKey) }
|
||||
pubkeys.addAll(replyEvents.map { it.pubKey })
|
||||
if (pubkeys.isNotEmpty()) {
|
||||
subscriptionsCoordinator.loadMetadataForPubkeys(pubkeys.distinct())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to user's bookmark list
|
||||
rememberSubscription(relayStatuses, account, relayManager = relayManager) {
|
||||
val configuredRelays = relayStatuses.keys
|
||||
@@ -244,9 +264,10 @@ fun ThreadScreen(
|
||||
onEvent = { event, _, _, _ ->
|
||||
if (event is ReactionEvent) {
|
||||
val targetEventId = event.originalPost().firstOrNull() ?: return@createReactionsSubscription
|
||||
reactionsByEvent =
|
||||
reactionsByEvent.toMutableMap().apply {
|
||||
this[targetEventId] = (this[targetEventId] ?: 0) + 1
|
||||
reactionIdsByEvent =
|
||||
reactionIdsByEvent.toMutableMap().apply {
|
||||
val existing = this[targetEventId] ?: emptySet()
|
||||
this[targetEventId] = existing + event.id
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -270,9 +291,10 @@ fun ThreadScreen(
|
||||
.lastOrNull()
|
||||
?.get(1) ?: return@createRepliesSubscription
|
||||
if (replyToId in allEventIds) {
|
||||
repliesByEvent =
|
||||
repliesByEvent.toMutableMap().apply {
|
||||
this[replyToId] = (this[replyToId] ?: 0) + 1
|
||||
replyIdsByEvent =
|
||||
replyIdsByEvent.toMutableMap().apply {
|
||||
val existing = this[replyToId] ?: emptySet()
|
||||
this[replyToId] = existing + event.id
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -292,9 +314,10 @@ fun ThreadScreen(
|
||||
onEvent = { event, _, _, _ ->
|
||||
if (event is RepostEvent) {
|
||||
val targetEventId = event.boostedEventId() ?: return@createRepostsSubscription
|
||||
repostsByEvent =
|
||||
repostsByEvent.toMutableMap().apply {
|
||||
this[targetEventId] = (this[targetEventId] ?: 0) + 1
|
||||
repostIdsByEvent =
|
||||
repostIdsByEvent.toMutableMap().apply {
|
||||
val existing = this[targetEventId] ?: emptySet()
|
||||
this[targetEventId] = existing + event.id
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+2
@@ -73,6 +73,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.LoadingState
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.UserAvatar
|
||||
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
|
||||
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
||||
import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
@@ -94,6 +95,7 @@ fun UserProfileScreen(
|
||||
localCache: DesktopLocalCache,
|
||||
account: AccountState.LoggedIn?,
|
||||
nwcConnection: com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect.Nip47URINorm? = null,
|
||||
subscriptionsCoordinator: DesktopRelaySubscriptionsCoordinator? = null,
|
||||
onBack: () -> Unit,
|
||||
onCompose: () -> Unit = {},
|
||||
onNavigateToProfile: (String) -> Unit = {},
|
||||
|
||||
Reference in New Issue
Block a user