update desktop with refactored classes
This commit is contained in:
@@ -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(
|
||||
|
||||
+47
-45
@@ -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(
|
||||
|
||||
+50
-40
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user