reuse some filtering logic
This commit is contained in:
@@ -11,6 +11,10 @@ sourceSets {
|
||||
kotlin.srcDir("src/jvmMain/kotlin")
|
||||
resources.srcDir("src/jvmMain/resources")
|
||||
}
|
||||
test {
|
||||
kotlin.srcDir("src/jvmTest/kotlin")
|
||||
resources.srcDir("src/jvmTest/resources")
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
@@ -40,6 +44,11 @@ dependencies {
|
||||
|
||||
// Collections
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
|
||||
// Testing
|
||||
testImplementation(libs.kotlin.test)
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
testImplementation(libs.okhttp)
|
||||
}
|
||||
|
||||
compose.desktop {
|
||||
|
||||
+33
-104
@@ -42,7 +42,6 @@ import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -54,16 +53,17 @@ 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.subscriptions.FeedMode
|
||||
import com.vitorpamplona.amethyst.commons.subscriptions.createContactListSubscription
|
||||
import com.vitorpamplona.amethyst.commons.subscriptions.createFollowingFeedSubscription
|
||||
import com.vitorpamplona.amethyst.commons.subscriptions.createGlobalFeedSubscription
|
||||
import com.vitorpamplona.amethyst.commons.subscriptions.rememberSubscription
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.LoadingState
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.NoteCard
|
||||
import com.vitorpamplona.amethyst.commons.util.toNoteDisplayData
|
||||
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
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.nip02FollowList.ContactListEvent
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
|
||||
/**
|
||||
* Note card with action buttons.
|
||||
@@ -95,11 +95,6 @@ fun FeedNoteCard(
|
||||
}
|
||||
}
|
||||
|
||||
enum class FeedMode {
|
||||
GLOBAL,
|
||||
FOLLOWING,
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FeedScreen(
|
||||
relayManager: DesktopRelayConnectionManager,
|
||||
@@ -125,113 +120,47 @@ fun FeedScreen(
|
||||
var followedUsers by remember { mutableStateOf<Set<String>>(emptySet()) }
|
||||
|
||||
// Load followed users for Following feed mode
|
||||
DisposableEffect(relayStatuses, account, feedMode) {
|
||||
rememberSubscription(relayStatuses, account, feedMode, relayManager = relayManager) {
|
||||
val configuredRelays = relayStatuses.keys
|
||||
if (configuredRelays.isNotEmpty() && account != null && feedMode == FeedMode.FOLLOWING) {
|
||||
val contactListSubId = "feed-contacts-${account.pubKeyHex}-${System.currentTimeMillis()}"
|
||||
relayManager.subscribe(
|
||||
subId = contactListSubId,
|
||||
filters =
|
||||
listOf(
|
||||
Filter(
|
||||
kinds = listOf(ContactListEvent.KIND),
|
||||
authors = listOf(account.pubKeyHex),
|
||||
limit = 1,
|
||||
),
|
||||
),
|
||||
createContactListSubscription(
|
||||
relays = configuredRelays,
|
||||
listener =
|
||||
object : IRequestListener {
|
||||
override fun onEvent(
|
||||
event: Event,
|
||||
isLive: Boolean,
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
if (event is ContactListEvent) {
|
||||
followedUsers = event.verifiedFollowKeySet()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEose(
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {}
|
||||
},
|
||||
pubKeyHex = account.pubKeyHex,
|
||||
onEvent = { event, _, _, _ ->
|
||||
if (event is ContactListEvent) {
|
||||
followedUsers = event.verifiedFollowKeySet()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onDispose {
|
||||
relayManager.unsubscribe(contactListSubId)
|
||||
}
|
||||
} else {
|
||||
onDispose {}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(relayStatuses, feedMode, followedUsers) {
|
||||
// Clear events when feed mode changes
|
||||
remember(feedMode) { eventState.clear() }
|
||||
|
||||
// Subscribe to feed based on mode
|
||||
rememberSubscription(relayStatuses, feedMode, followedUsers, relayManager = relayManager) {
|
||||
val configuredRelays = relayStatuses.keys
|
||||
if (configuredRelays.isNotEmpty()) {
|
||||
// Clear previous events when switching modes
|
||||
eventState.clear()
|
||||
if (configuredRelays.isEmpty()) return@rememberSubscription null
|
||||
|
||||
val subId = "${feedMode.name.lowercase()}-feed-${System.currentTimeMillis()}"
|
||||
val filters =
|
||||
when (feedMode) {
|
||||
FeedMode.GLOBAL ->
|
||||
listOf(
|
||||
Filter(
|
||||
kinds = listOf(TextNoteEvent.KIND),
|
||||
limit = 50,
|
||||
),
|
||||
)
|
||||
FeedMode.FOLLOWING ->
|
||||
if (followedUsers.isNotEmpty()) {
|
||||
listOf(
|
||||
Filter(
|
||||
kinds = listOf(TextNoteEvent.KIND),
|
||||
authors = followedUsers.toList(),
|
||||
limit = 50,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
// No followed users yet, return empty filter
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
if (filters.isNotEmpty()) {
|
||||
relayManager.subscribe(
|
||||
subId = subId,
|
||||
filters = filters,
|
||||
when (feedMode) {
|
||||
FeedMode.GLOBAL ->
|
||||
createGlobalFeedSubscription(
|
||||
relays = configuredRelays,
|
||||
listener =
|
||||
object : IRequestListener {
|
||||
override fun onEvent(
|
||||
event: Event,
|
||||
isLive: Boolean,
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
eventState.addItem(event)
|
||||
}
|
||||
|
||||
override fun onEose(
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
// End of stored events
|
||||
}
|
||||
},
|
||||
onEvent = { event, _, _, _ -> eventState.addItem(event) },
|
||||
)
|
||||
|
||||
onDispose {
|
||||
relayManager.unsubscribe(subId)
|
||||
FeedMode.FOLLOWING ->
|
||||
if (followedUsers.isNotEmpty()) {
|
||||
createFollowingFeedSubscription(
|
||||
relays = configuredRelays,
|
||||
followedUsers = followedUsers.toList(),
|
||||
onEvent = { event, _, _, _ -> eventState.addItem(event) },
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
onDispose {}
|
||||
}
|
||||
} else {
|
||||
onDispose {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+44
-85
@@ -39,7 +39,6 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -52,15 +51,14 @@ 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.subscriptions.createNotificationsSubscription
|
||||
import com.vitorpamplona.amethyst.commons.subscriptions.rememberSubscription
|
||||
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.quartz.nip01Core.core.Event
|
||||
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.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||
@@ -122,96 +120,57 @@ fun NotificationsScreen(
|
||||
}
|
||||
val notifications by notificationState.items.collectAsState()
|
||||
|
||||
DisposableEffect(relayStatuses, account.pubKeyHex) {
|
||||
// Subscribe to notifications
|
||||
rememberSubscription(relayStatuses, account.pubKeyHex, relayManager = relayManager) {
|
||||
val configuredRelays = relayStatuses.keys
|
||||
if (configuredRelays.isNotEmpty()) {
|
||||
val subId = "notifications-${account.pubKeyHex}-${System.currentTimeMillis()}"
|
||||
val filters =
|
||||
listOf(
|
||||
// Mentions, replies, reactions, reposts, zaps
|
||||
Filter(
|
||||
kinds =
|
||||
listOf(
|
||||
TextNoteEvent.KIND, // 1 - mentions/replies
|
||||
ReactionEvent.KIND, // 7 - reactions
|
||||
RepostEvent.KIND, // 6 - reposts
|
||||
GenericRepostEvent.KIND, // 16 - generic reposts
|
||||
LnZapEvent.KIND, // 9735 - zaps
|
||||
),
|
||||
tags = mapOf("p" to listOf(account.pubKeyHex)), // Events mentioning user
|
||||
limit = 100,
|
||||
),
|
||||
)
|
||||
|
||||
relayManager.subscribe(
|
||||
subId = subId,
|
||||
filters = filters,
|
||||
createNotificationsSubscription(
|
||||
relays = configuredRelays,
|
||||
listener =
|
||||
object : IRequestListener {
|
||||
override fun onEvent(
|
||||
event: Event,
|
||||
isLive: Boolean,
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
// Skip events from the user themselves (except zaps)
|
||||
if (event.pubKey == account.pubKeyHex && event !is LnZapEvent) {
|
||||
return
|
||||
pubKeyHex = account.pubKeyHex,
|
||||
onEvent = { event, _, _, _ ->
|
||||
// Skip events from the user themselves (except zaps)
|
||||
if (event.pubKey == account.pubKeyHex && event !is LnZapEvent) {
|
||||
return@createNotificationsSubscription
|
||||
}
|
||||
|
||||
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 -> {
|
||||
val amount = event.amount?.toLong()
|
||||
NotificationItem.Zap(
|
||||
event = event,
|
||||
timestamp = event.createdAt,
|
||||
amount = amount,
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
is TextNoteEvent -> {
|
||||
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)
|
||||
}
|
||||
|
||||
notificationState.addItem(notification)
|
||||
}
|
||||
else -> NotificationItem.Mention(event, event.createdAt)
|
||||
}
|
||||
|
||||
override fun onEose(
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
// End of stored events
|
||||
}
|
||||
},
|
||||
notificationState.addItem(notification)
|
||||
},
|
||||
)
|
||||
|
||||
onDispose {
|
||||
relayManager.unsubscribe(subId)
|
||||
}
|
||||
} else {
|
||||
onDispose { }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+53
-131
@@ -48,7 +48,6 @@ import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -64,15 +63,15 @@ 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.subscriptions.createContactListSubscription
|
||||
import com.vitorpamplona.amethyst.commons.subscriptions.createMetadataSubscription
|
||||
import com.vitorpamplona.amethyst.commons.subscriptions.createUserPostsSubscription
|
||||
import com.vitorpamplona.amethyst.commons.subscriptions.rememberSubscription
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.LoadingState
|
||||
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
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.nip02FollowList.ContactListEvent
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.toNpub
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -124,148 +123,71 @@ fun UserProfileScreen(
|
||||
}
|
||||
|
||||
// Load current user's contact list (for follow state)
|
||||
DisposableEffect(relayStatuses, account) {
|
||||
rememberSubscription(relayStatuses, account, relayManager = relayManager) {
|
||||
val configuredRelays = relayStatuses.keys
|
||||
if (configuredRelays.isNotEmpty() && account != null) {
|
||||
val contactListSubId = "my-contacts-${account.pubKeyHex}-${System.currentTimeMillis()}"
|
||||
relayManager.subscribe(
|
||||
subId = contactListSubId,
|
||||
filters =
|
||||
listOf(
|
||||
Filter(
|
||||
kinds = listOf(ContactListEvent.KIND), // Kind 3
|
||||
authors = listOf(account.pubKeyHex),
|
||||
limit = 1,
|
||||
),
|
||||
),
|
||||
createContactListSubscription(
|
||||
relays = configuredRelays,
|
||||
listener =
|
||||
object : IRequestListener {
|
||||
override fun onEvent(
|
||||
event: Event,
|
||||
isLive: Boolean,
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
if (event is ContactListEvent) {
|
||||
followState.updateContactList(event, pubKeyHex)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEose(
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {}
|
||||
},
|
||||
pubKeyHex = account.pubKeyHex,
|
||||
onEvent = { event, _, _, _ ->
|
||||
if (event is ContactListEvent) {
|
||||
followState.updateContactList(event, pubKeyHex)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onDispose {
|
||||
relayManager.unsubscribe(contactListSubId)
|
||||
}
|
||||
} else {
|
||||
onDispose {}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to user metadata and posts
|
||||
DisposableEffect(relayStatuses, pubKeyHex, retryTrigger) {
|
||||
// Clear posts when profile changes
|
||||
remember(pubKeyHex, retryTrigger) {
|
||||
eventState.clear()
|
||||
postsLoading = true
|
||||
postsError = null
|
||||
}
|
||||
|
||||
// Subscribe to user metadata
|
||||
rememberSubscription(relayStatuses, pubKeyHex, retryTrigger, relayManager = relayManager) {
|
||||
val configuredRelays = relayStatuses.keys
|
||||
if (configuredRelays.isNotEmpty()) {
|
||||
postsLoading = true
|
||||
postsError = null
|
||||
eventState.clear()
|
||||
// Metadata subscription (kind 0)
|
||||
val metadataSubId = "profile-metadata-$pubKeyHex-${System.currentTimeMillis()}"
|
||||
relayManager.subscribe(
|
||||
subId = metadataSubId,
|
||||
filters =
|
||||
listOf(
|
||||
Filter(
|
||||
kinds = listOf(0), // Metadata
|
||||
authors = listOf(pubKeyHex),
|
||||
limit = 1,
|
||||
),
|
||||
),
|
||||
createMetadataSubscription(
|
||||
relays = configuredRelays,
|
||||
listener =
|
||||
object : IRequestListener {
|
||||
override fun onEvent(
|
||||
event: Event,
|
||||
isLive: Boolean,
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
// Parse metadata JSON (simplified - full parsing in production)
|
||||
try {
|
||||
val content = event.content
|
||||
displayName = extractJsonField(content, "display_name") ?: extractJsonField(content, "name")
|
||||
about = extractJsonField(content, "about")
|
||||
picture = extractJsonField(content, "picture")
|
||||
} catch (e: Exception) {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEose(
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {}
|
||||
},
|
||||
)
|
||||
|
||||
// Posts subscription (kind 1)
|
||||
val postsSubId = "profile-posts-$pubKeyHex-${System.currentTimeMillis()}"
|
||||
relayManager.subscribe(
|
||||
subId = postsSubId,
|
||||
filters =
|
||||
listOf(
|
||||
Filter(
|
||||
kinds = listOf(TextNoteEvent.KIND),
|
||||
authors = listOf(pubKeyHex),
|
||||
limit = 50,
|
||||
),
|
||||
),
|
||||
relays = configuredRelays,
|
||||
listener =
|
||||
object : IRequestListener {
|
||||
override fun onEvent(
|
||||
event: Event,
|
||||
isLive: Boolean,
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
eventState.addItem(event)
|
||||
}
|
||||
|
||||
override fun onEose(
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
// At least one relay finished sending events
|
||||
postsLoading = false
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Set timeout for loading state
|
||||
val timeoutJob =
|
||||
kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.Default).launch {
|
||||
kotlinx.coroutines.delay(10000) // 10 second timeout
|
||||
if (postsLoading) {
|
||||
postsError = "Request timed out. Check relay connections."
|
||||
postsLoading = false
|
||||
pubKeyHex = pubKeyHex,
|
||||
onEvent = { event, _, _, _ ->
|
||||
try {
|
||||
val content = event.content
|
||||
displayName = extractJsonField(content, "display_name") ?: extractJsonField(content, "name")
|
||||
about = extractJsonField(content, "about")
|
||||
picture = extractJsonField(content, "picture")
|
||||
} catch (e: Exception) {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
onDispose {
|
||||
timeoutJob.cancel()
|
||||
relayManager.unsubscribe(metadataSubId)
|
||||
relayManager.unsubscribe(postsSubId)
|
||||
}
|
||||
// Subscribe to user posts
|
||||
rememberSubscription(relayStatuses, pubKeyHex, retryTrigger, relayManager = relayManager) {
|
||||
val configuredRelays = relayStatuses.keys
|
||||
if (configuredRelays.isNotEmpty()) {
|
||||
createUserPostsSubscription(
|
||||
relays = configuredRelays,
|
||||
pubKeyHex = pubKeyHex,
|
||||
onEvent = { event, _, _, _ ->
|
||||
eventState.addItem(event)
|
||||
},
|
||||
onEose = { _, _ ->
|
||||
postsLoading = false
|
||||
},
|
||||
)
|
||||
} else {
|
||||
postsLoading = false
|
||||
postsError = "No relays configured"
|
||||
onDispose {}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 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.network
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class DesktopHttpClientTest {
|
||||
@Test
|
||||
fun testGetHttpClientReturnsConfiguredClient() {
|
||||
val url = NormalizedRelayUrl("wss://relay.damus.io")
|
||||
val client = DesktopHttpClient.getHttpClient(url)
|
||||
|
||||
assertNotNull(client)
|
||||
assertEquals(30_000, client.connectTimeoutMillis)
|
||||
assertEquals(30_000, client.readTimeoutMillis)
|
||||
assertEquals(30_000, client.writeTimeoutMillis)
|
||||
assertEquals(30_000, client.pingIntervalMillis)
|
||||
assertTrue(client.retryOnConnectionFailure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetHttpClientReturnsSameInstance() {
|
||||
val url1 = NormalizedRelayUrl("wss://relay.damus.io")
|
||||
val url2 = NormalizedRelayUrl("wss://nos.lol")
|
||||
|
||||
val client1 = DesktopHttpClient.getHttpClient(url1)
|
||||
val client2 = DesktopHttpClient.getHttpClient(url2)
|
||||
|
||||
// Should return the same singleton instance
|
||||
assertEquals(client1, client2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testHttpClientHasExpectedTimeouts() {
|
||||
val url = NormalizedRelayUrl("wss://relay.nostr.band")
|
||||
val client = DesktopHttpClient.getHttpClient(url)
|
||||
|
||||
// Verify all timeouts are 30 seconds
|
||||
assertEquals(30_000, client.connectTimeoutMillis)
|
||||
assertEquals(30_000, client.readTimeoutMillis)
|
||||
assertEquals(30_000, client.writeTimeoutMillis)
|
||||
assertEquals(30_000, client.pingIntervalMillis)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testHttpClientHasRetryEnabled() {
|
||||
val url = NormalizedRelayUrl("wss://relay.snort.social")
|
||||
val client = DesktopHttpClient.getHttpClient(url)
|
||||
|
||||
assertTrue(client.retryOnConnectionFailure, "Retry on connection failure should be enabled")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testHttpClientIsLazyInitialized() {
|
||||
// This test verifies the lazy initialization pattern
|
||||
// The client should be created only once even with multiple calls
|
||||
val url1 = NormalizedRelayUrl("wss://relay1.example.com")
|
||||
val url2 = NormalizedRelayUrl("wss://relay2.example.com")
|
||||
val url3 = NormalizedRelayUrl("wss://relay3.example.com")
|
||||
|
||||
val client1 = DesktopHttpClient.getHttpClient(url1)
|
||||
val client2 = DesktopHttpClient.getHttpClient(url2)
|
||||
val client3 = DesktopHttpClient.getHttpClient(url3)
|
||||
|
||||
// All should be the same instance due to lazy singleton
|
||||
assertEquals(client1, client2)
|
||||
assertEquals(client2, client3)
|
||||
assertEquals(client1, client3)
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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.network
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class DesktopRelayConnectionManagerTest {
|
||||
@Test
|
||||
fun testRelayConnectionManagerCanBeInstantiated() {
|
||||
val manager = DesktopRelayConnectionManager()
|
||||
assertNotNull(manager)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRelayConnectionManagerHasNoActiveConnectionsInitially() {
|
||||
val manager = DesktopRelayConnectionManager()
|
||||
val connectedRelays = manager.connectedRelays.value
|
||||
val availableRelays = manager.availableRelays.value
|
||||
assertTrue(connectedRelays.isEmpty(), "Should have no connected relays on initialization")
|
||||
assertTrue(availableRelays.isEmpty(), "Should have no available relays on initialization")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRelayConnectionManagerInheritsFromBaseClass() {
|
||||
val manager = DesktopRelayConnectionManager()
|
||||
assertTrue(
|
||||
manager is com.vitorpamplona.amethyst.commons.network.RelayConnectionManager,
|
||||
"DesktopRelayConnectionManager should extend RelayConnectionManager",
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user