new chat screen

This commit is contained in:
2026-05-15 17:09:59 +07:00
parent d56847f5d4
commit 6b448a56f8
11 changed files with 571 additions and 31 deletions
@@ -1,7 +1,10 @@
package su.reya.coop
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
@@ -24,6 +27,8 @@ import rust.nostr.sdk.KindStandard
import rust.nostr.sdk.LogLevel
import rust.nostr.sdk.Metadata
import rust.nostr.sdk.MetadataRecord
import rust.nostr.sdk.Nip05Address
import rust.nostr.sdk.Nip05Profile
import rust.nostr.sdk.NostrDatabase
import rust.nostr.sdk.NostrGossip
import rust.nostr.sdk.PublicKey
@@ -56,8 +61,6 @@ class Nostr {
private set
var msgRelayList: Map<PublicKey, List<RelayUrl>> = emptyMap()
private set
var contactList: List<PublicKey> = emptyList()
private set
suspend fun init(dbPath: String) {
try {
@@ -87,6 +90,12 @@ class Nostr {
client?.addRelay(RelayUrl.parse("wss://relay.primal.net"))
client?.addRelay(RelayUrl.parse("wss://user.kindpag.es"))
// Add search relay
client?.addRelay(
url = RelayUrl.parse("wss://antiprimal.net"),
capabilities = RelayCapabilities.read()
)
// Indexer relay for NIP-65 discovery
client?.addRelay(
url = RelayUrl.parse("wss://indexer.coracle.social"),
@@ -107,7 +116,6 @@ class Nostr {
suspend fun exit() {
signer.switch(Keys.generate())
deviceSigner = null
contactList = emptyList()
}
suspend fun setSigner(keys: AsyncNostrSigner) {
@@ -184,6 +192,7 @@ class Nostr {
suspend fun handleNotifications(
onMetadataUpdate: (PublicKey, Metadata) -> Unit,
onContactListUpdate: (List<PublicKey>) -> Unit,
onNewMessage: (UnsignedEvent) -> Unit,
onEose: () -> Unit,
) = coroutineScope {
@@ -217,6 +226,12 @@ class Nostr {
}
}
if (event.kind().asStd()?.equals(KindStandard.CONTACT_LIST) == true) {
if (isSignedByUser(event = event)) {
onContactListUpdate(event.tags().publicKeys())
}
}
if (event.kind().asStd()?.equals(KindStandard.INBOX_RELAYS) == true) {
if (isSignedByUser(event = event)) {
getUserMessages(msgRelayList = event)
@@ -457,7 +472,7 @@ class Nostr {
)
)
client?.subscribe(target = target, id = "metadata-reqs", closeOn = opts)
client?.subscribe(target = target, closeOn = opts)
} catch (e: Exception) {
throw IllegalStateException("Failed to fetch metadata batch: ${e.message}", e)
}
@@ -494,13 +509,10 @@ class Nostr {
Filter().kind(kind).author(userPubkey).pubkeys(room.members.toList());
// Check if the user is interacting with the room's members
val isInteracting = client?.database()?.query(filter)?.isEmpty() == false;
// Check if the room's members are in the contact list
val isContact = contactList.containsAll(room.members)
val isOngoing = client?.database()?.query(filter)?.isEmpty() == false;
// Set the room kind based on interaction status
if (isInteracting || isContact) {
if (isOngoing) {
room.setKind(RoomKind.Ongoing)
}
@@ -614,4 +626,54 @@ class Nostr {
throw IllegalStateException("Failed to send message: ${e.message}", e)
}
}
suspend fun profileFromAddress(client: HttpClient, address: Nip05Address): Nip05Profile {
try {
val response: HttpResponse = client.get(address.url())
val bodyString: String = response.body()
return Nip05Profile.fromJson(address, bodyString)
} catch (e: Exception) {
throw IllegalStateException("Failed to fetch profile from address: ${e.message}", e)
}
}
suspend fun searchByAddress(query: String): List<PublicKey> {
try {
val address = Nip05Address.parse(query)
val profile = profileFromAddress(HttpClient(), address)
return listOf(profile.publicKey())
} catch (e: Exception) {
throw IllegalStateException("Failed to search address: ${e.message}", e)
}
}
suspend fun searchByNostr(query: String) {
try {
val kinds = listOf(Kind.fromStd(KindStandard.METADATA))
val filter = Filter().kinds(kinds).search(query).limit(10u)
val target =
ReqTarget.manual(mapOf(RelayUrl.parse("wss://antiprimal.net") to listOf(filter)))
val stream = client?.streamEvents(
target = target,
id = "search",
timeout = Duration.parse("4s"),
policy = ReqExitPolicy.ExitOnEose
)
// Collect the results
val results = mutableListOf<PublicKey>()
// Keep searching until the stream is closed or timeout
stream?.next()?.let { event ->
if (event.event != null) {
results.add(event.event!!.author())
}
}
} catch (e: Exception) {
throw IllegalStateException("Failed to search nostr: ${e.message}", e)
}
}
}