update desktop with refactored classes

This commit is contained in:
nrobi144
2026-01-02 14:54:24 +02:00
parent 2751855186
commit 36bb89fd36
8 changed files with 676 additions and 116 deletions
@@ -45,14 +45,15 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.account.AccountState
import com.vitorpamplona.amethyst.commons.state.EventCollectionState
import com.vitorpamplona.amethyst.commons.ui.components.LoadingState
import com.vitorpamplona.amethyst.commons.ui.note.NoteCard
import com.vitorpamplona.amethyst.commons.util.toNoteDisplayData
@@ -108,8 +109,17 @@ fun FeedScreen(
) {
val connectedRelays by relayManager.connectedRelays.collectAsState()
val relayStatuses by relayManager.relayStatuses.collectAsState()
val events = remember { mutableStateListOf<Event>() }
val seenIds = remember { mutableSetOf<String>() }
val scope = rememberCoroutineScope()
val eventState =
remember {
EventCollectionState<Event>(
getId = { it.id },
sortComparator = compareByDescending { it.createdAt },
maxSize = 200,
scope = scope,
)
}
val events by eventState.items.collectAsState()
var replyToEvent by remember { mutableStateOf<Event?>(null) }
var feedMode by remember { mutableStateOf(FeedMode.GLOBAL) }
var followedUsers by remember { mutableStateOf<Set<String>>(emptySet()) }
@@ -162,8 +172,7 @@ fun FeedScreen(
val configuredRelays = relayStatuses.keys
if (configuredRelays.isNotEmpty()) {
// Clear previous events when switching modes
events.clear()
seenIds.clear()
eventState.clear()
val subId = "${feedMode.name.lowercase()}-feed-${System.currentTimeMillis()}"
val filters =
@@ -203,14 +212,7 @@ fun FeedScreen(
relay: NormalizedRelayUrl,
forFilters: List<Filter>?,
) {
if (event.id !in seenIds) {
seenIds.add(event.id)
events.add(0, event)
if (events.size > 200) {
val removed = events.removeAt(events.size - 1)
seenIds.remove(removed.id)
}
}
eventState.addItem(event)
}
override fun onEose(
@@ -42,8 +42,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@@ -51,6 +51,7 @@ import com.vitorpamplona.amethyst.commons.account.AccountState
import com.vitorpamplona.amethyst.commons.icons.Reply
import com.vitorpamplona.amethyst.commons.icons.Repost
import com.vitorpamplona.amethyst.commons.icons.Zap
import com.vitorpamplona.amethyst.commons.state.EventCollectionState
import com.vitorpamplona.amethyst.commons.ui.components.LoadingState
import com.vitorpamplona.amethyst.commons.ui.feed.FeedHeader
import com.vitorpamplona.amethyst.commons.util.toTimeAgo
@@ -109,8 +110,17 @@ fun NotificationsScreen(
) {
val connectedRelays by relayManager.connectedRelays.collectAsState()
val relayStatuses by relayManager.relayStatuses.collectAsState()
val notifications = remember { mutableStateListOf<NotificationItem>() }
val seenIds = remember { mutableSetOf<String>() }
val scope = rememberCoroutineScope()
val notificationState =
remember {
EventCollectionState<NotificationItem>(
getId = { it.event.id },
sortComparator = null, // Prepend new items (no sorting)
maxSize = 200,
scope = scope,
)
}
val notifications by notificationState.items.collectAsState()
DisposableEffect(relayStatuses, account.pubKeyHex) {
val configuredRelays = relayStatuses.keys
@@ -150,50 +160,42 @@ fun NotificationsScreen(
return
}
if (event.id !in seenIds) {
seenIds.add(event.id)
val notification =
when (event) {
is ReactionEvent ->
NotificationItem.Reaction(
event = event,
timestamp = event.createdAt,
content = event.content,
)
is RepostEvent, is GenericRepostEvent ->
NotificationItem.Repost(
event = event,
timestamp = event.createdAt,
)
is LnZapEvent -> {
// Extract amount from zap (simplified - full parsing in production)
val amount = event.amount?.toLong()
NotificationItem.Zap(
event = event,
timestamp = event.createdAt,
amount = amount,
)
}
is TextNoteEvent -> {
// Check if it's a reply (has e-tag) or mention
val eTags = event.tags.filter { it.size > 1 && it[0] == "e" }
val isReply = eTags.isNotEmpty()
if (isReply) {
NotificationItem.Reply(event, event.createdAt)
} else {
NotificationItem.Mention(event, event.createdAt)
}
}
else -> NotificationItem.Mention(event, event.createdAt)
val notification =
when (event) {
is ReactionEvent ->
NotificationItem.Reaction(
event = event,
timestamp = event.createdAt,
content = event.content,
)
is RepostEvent, is GenericRepostEvent ->
NotificationItem.Repost(
event = event,
timestamp = event.createdAt,
)
is LnZapEvent -> {
// Extract amount from zap (simplified - full parsing in production)
val amount = event.amount?.toLong()
NotificationItem.Zap(
event = event,
timestamp = event.createdAt,
amount = amount,
)
}
notifications.add(0, notification)
if (notifications.size > 200) {
val removed = notifications.removeAt(notifications.size - 1)
seenIds.remove(removed.event.id)
is TextNoteEvent -> {
// Check if it's a reply (has e-tag) or mention
val eTags = event.tags.filter { it.size > 1 && it[0] == "e" }
val isReply = eTags.isNotEmpty()
if (isReply) {
NotificationItem.Reply(event, event.createdAt)
} else {
NotificationItem.Mention(event, event.createdAt)
}
}
else -> NotificationItem.Mention(event, event.createdAt)
}
}
notificationState.addItem(notification)
}
override fun onEose(
@@ -51,7 +51,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@@ -63,6 +62,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.account.AccountState
import com.vitorpamplona.amethyst.commons.actions.FollowAction
import com.vitorpamplona.amethyst.commons.state.EventCollectionState
import com.vitorpamplona.amethyst.commons.state.FollowState
import com.vitorpamplona.amethyst.commons.ui.components.LoadingState
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
import com.vitorpamplona.quartz.nip01Core.core.Event
@@ -70,7 +71,6 @@ import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip19Bech32.toNpub
@@ -100,19 +100,28 @@ fun UserProfileScreen(
var followersCount by remember { mutableStateOf(0) }
var followingCount by remember { mutableStateOf(0) }
val scope = rememberCoroutineScope()
// User's posts
val events = remember { mutableStateListOf<Event>() }
val seenIds = remember { mutableSetOf<String>() }
val eventState =
remember {
EventCollectionState<Event>(
getId = { it.id },
sortComparator = compareByDescending { it.createdAt },
maxSize = 200,
scope = scope,
)
}
val events by eventState.items.collectAsState()
var postsLoading by remember { mutableStateOf(true) }
var postsError by remember { mutableStateOf<String?>(null) }
var retryTrigger by remember { mutableStateOf(0) }
// Follow state
var isFollowing by remember { mutableStateOf(false) }
var currentContactList by remember { mutableStateOf<ContactListEvent?>(null) }
var isFollowLoading by remember { mutableStateOf(false) }
var followError by remember { mutableStateOf<String?>(null) }
val scope = rememberCoroutineScope()
val followState =
remember(account) {
FollowState(myPubKeyHex = account?.pubKeyHex ?: "")
}
// Load current user's contact list (for follow state)
DisposableEffect(relayStatuses, account) {
@@ -139,8 +148,7 @@ fun UserProfileScreen(
forFilters: List<Filter>?,
) {
if (event is ContactListEvent) {
currentContactList = event
isFollowing = event.isTaggedUser(pubKeyHex)
followState.updateContactList(event, pubKeyHex)
}
}
@@ -165,8 +173,7 @@ fun UserProfileScreen(
if (configuredRelays.isNotEmpty()) {
postsLoading = true
postsError = null
events.clear()
seenIds.clear()
eventState.clear()
// Metadata subscription (kind 0)
val metadataSubId = "profile-metadata-$pubKeyHex-${System.currentTimeMillis()}"
relayManager.subscribe(
@@ -227,14 +234,7 @@ fun UserProfileScreen(
relay: NormalizedRelayUrl,
forFilters: List<Filter>?,
) {
if (event.id !in seenIds) {
seenIds.add(event.id)
events.add(0, event)
if (events.size > 100) {
val removed = events.removeAt(events.size - 1)
seenIds.remove(removed.id)
}
}
eventState.addItem(event)
}
override fun onEose(
@@ -292,26 +292,28 @@ fun UserProfileScreen(
Button(
onClick = {
scope.launch {
isFollowLoading = true
followError = null
followState.setFollowLoading()
try {
if (isFollowing) {
unfollowUser(pubKeyHex, account, relayManager, currentContactList)
isFollowing = false
} else {
followUser(pubKeyHex, account, relayManager, currentContactList)
isFollowing = true
}
val currentStatus = followState.currentStatusOrNull()
val updatedEvent =
if (currentStatus?.isFollowing == true) {
unfollowUser(pubKeyHex, account, relayManager, currentStatus.contactList)
} else {
followUser(pubKeyHex, account, relayManager, currentStatus?.contactList)
}
followState.setFollowSuccess(updatedEvent, pubKeyHex)
} catch (e: Exception) {
followError = e.message ?: "Failed to update follow status"
} finally {
isFollowLoading = false
followState.setFollowError(e.message ?: "Failed to update follow status", e)
}
}
},
enabled = !isFollowLoading,
enabled = followState.state.value !is com.vitorpamplona.amethyst.commons.state.LoadingState.Loading,
) {
if (isFollowLoading) {
val state = followState.state.collectAsState().value
val isFollowing = (state as? com.vitorpamplona.amethyst.commons.state.LoadingState.Success)?.data?.isFollowing ?: false
val isLoading = state is com.vitorpamplona.amethyst.commons.state.LoadingState.Loading
if (isLoading) {
androidx.compose.material3.CircularProgressIndicator(
modifier = Modifier.size(16.dp),
strokeWidth = 2.dp,
@@ -330,7 +332,12 @@ fun UserProfileScreen(
}
}
followError?.let { error ->
val errorMessage =
followState.state
.collectAsState()
.value
.errorOrNull()
errorMessage?.let { error ->
Spacer(Modifier.height(4.dp))
Text(
error,
@@ -526,7 +533,7 @@ private suspend fun followUser(
account: AccountState.LoggedIn,
relayManager: DesktopRelayConnectionManager,
currentContactList: ContactListEvent?,
) {
): ContactListEvent =
withContext(Dispatchers.IO) {
println("[UserProfile] Starting followUser: target=${pubKeyHex.take(8)}...")
@@ -536,8 +543,9 @@ private suspend fun followUser(
println("[UserProfile] ContactListEvent created, broadcasting...")
relayManager.broadcastToAll(updatedEvent)
println("[UserProfile] Follow broadcast complete")
updatedEvent
}
}
/**
* Unfollows a user by publishing an updated contact list event without them.
@@ -547,7 +555,7 @@ private suspend fun unfollowUser(
account: AccountState.LoggedIn,
relayManager: DesktopRelayConnectionManager,
currentContactList: ContactListEvent?,
) {
): ContactListEvent =
withContext(Dispatchers.IO) {
println("[UserProfile] Starting unfollowUser: target=${pubKeyHex.take(8)}...")
@@ -560,8 +568,10 @@ private suspend fun unfollowUser(
println("[UserProfile] ContactListEvent updated, broadcasting...")
relayManager.broadcastToAll(updatedEvent)
println("[UserProfile] Unfollow broadcast complete")
updatedEvent
} else {
println("[UserProfile] Error: No contact list to unfollow from")
throw IllegalStateException("Cannot unfollow: No contact list available")
}
}
}