update metadata loading

This commit is contained in:
nrobi144
2026-01-20 15:18:11 +02:00
parent 8f6de1d304
commit 8155cfee9d
77 changed files with 1490 additions and 107 deletions
@@ -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))
@@ -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()
}
}
@@ -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
@@ -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 = {},
@@ -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
}
}
},
@@ -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 = {},