fix
This commit is contained in:
@@ -111,6 +111,7 @@ fun App(dbPath: String) {
|
|||||||
|
|
||||||
ImportScreen(
|
ImportScreen(
|
||||||
isLoading = isCreating,
|
isLoading = isCreating,
|
||||||
|
onBack = { navController.popBackStack() },
|
||||||
onSave = { secret ->
|
onSave = { secret ->
|
||||||
viewModel.importIdentity(secret)
|
viewModel.importIdentity(secret)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,25 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LoadingIndicator
|
import androidx.compose.material3.LoadingIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -21,19 +32,52 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coop.composeapp.generated.resources.Res
|
||||||
|
import coop.composeapp.generated.resources.ic_arrow_back
|
||||||
|
import org.jetbrains.compose.resources.painterResource
|
||||||
|
import su.reya.coop.LocalSnackbarHostState
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ImportScreen(
|
fun ImportScreen(
|
||||||
isLoading: Boolean,
|
isLoading: Boolean,
|
||||||
|
onBack: () -> Unit,
|
||||||
onSave: (secret: String) -> Unit
|
onSave: (secret: String) -> Unit
|
||||||
) {
|
) {
|
||||||
|
val snackbarHostState = LocalSnackbarHostState.current
|
||||||
var secret by remember { mutableStateOf("") }
|
var secret by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("Import") },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(Res.drawable.ic_arrow_back),
|
||||||
|
contentDescription = "Back"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
content = { innerPadding ->
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = innerPadding.calculateTopPadding()),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp),
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(16.dp)
|
.padding(24.dp)
|
||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
@@ -50,14 +94,22 @@ fun ImportScreen(
|
|||||||
onClick = {
|
onClick = {
|
||||||
onSave(secret)
|
onSave(secret)
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(ButtonDefaults.LargeContainerHeight),
|
||||||
enabled = secret.isNotBlank() && !isLoading,
|
enabled = secret.isNotBlank() && !isLoading,
|
||||||
) {
|
) {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
LoadingIndicator()
|
LoadingIndicator()
|
||||||
} else {
|
} else {
|
||||||
Text("Save & Continue")
|
Text(
|
||||||
|
text = "Save & Continue",
|
||||||
|
style = MaterialTheme.typography.titleLargeEmphasized,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ fun NewIdentityScreen(
|
|||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onBack) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(Res.drawable.ic_arrow_back),
|
painter = painterResource(Res.drawable.ic_arrow_back),
|
||||||
contentDescription = "User"
|
contentDescription = "Back"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ junit = "4.13.2"
|
|||||||
kotlin = "2.3.20"
|
kotlin = "2.3.20"
|
||||||
kotlinx-serialization = "1.8.0"
|
kotlinx-serialization = "1.8.0"
|
||||||
material3 = "1.10.0-alpha05"
|
material3 = "1.10.0-alpha05"
|
||||||
|
ktor = "3.4.3"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
||||||
@@ -36,6 +37,10 @@ compose-material3 = { module = "org.jetbrains.compose.material3:material3", vers
|
|||||||
compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMultiplatform" }
|
compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMultiplatform" }
|
||||||
compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "composeMultiplatform" }
|
compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "composeMultiplatform" }
|
||||||
compose-uiToolingPreview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "composeMultiplatform" }
|
compose-uiToolingPreview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "composeMultiplatform" }
|
||||||
|
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
||||||
|
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
|
||||||
|
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
|
||||||
|
ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|||||||
@@ -26,7 +26,15 @@ kotlin {
|
|||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
|
implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
|
||||||
implementation("su.reya:nostr-sdk-kmp:0.1.2")
|
implementation("su.reya:nostr-sdk-kmp:0.1.5")
|
||||||
|
implementation(libs.ktor.client.core)
|
||||||
|
implementation(libs.ktor.client.websockets)
|
||||||
|
}
|
||||||
|
androidMain.dependencies {
|
||||||
|
implementation(libs.ktor.client.okhttp)
|
||||||
|
}
|
||||||
|
iosMain.dependencies {
|
||||||
|
implementation(libs.ktor.client.darwin)
|
||||||
}
|
}
|
||||||
commonTest.dependencies {
|
commonTest.dependencies {
|
||||||
implementation(libs.kotlin.test)
|
implementation(libs.kotlin.test)
|
||||||
|
|||||||
75
shared/src/commonMain/kotlin/su/reya/coop/CoopWebSocket.kt
Normal file
75
shared/src/commonMain/kotlin/su/reya/coop/CoopWebSocket.kt
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package su.reya.coop
|
||||||
|
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.plugins.websocket.DefaultClientWebSocketSession
|
||||||
|
import io.ktor.client.plugins.websocket.webSocketSession
|
||||||
|
import io.ktor.client.request.url
|
||||||
|
import io.ktor.websocket.Frame
|
||||||
|
import io.ktor.websocket.close
|
||||||
|
import io.ktor.websocket.readBytes
|
||||||
|
import io.ktor.websocket.readText
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||||
|
import rust.nostr.sdk.ConnectionMode
|
||||||
|
import rust.nostr.sdk.CustomWebSocketTransport
|
||||||
|
import rust.nostr.sdk.WebSocketAdapter
|
||||||
|
import rust.nostr.sdk.WebSocketAdapterWrapper
|
||||||
|
import rust.nostr.sdk.WebSocketMessage
|
||||||
|
|
||||||
|
class KtorWebSocketAdapter(
|
||||||
|
private val client: HttpClient,
|
||||||
|
private val session: DefaultClientWebSocketSession
|
||||||
|
) : WebSocketAdapter {
|
||||||
|
|
||||||
|
override suspend fun send(msg: WebSocketMessage) {
|
||||||
|
try {
|
||||||
|
when (msg) {
|
||||||
|
is WebSocketMessage.Text -> session.send(Frame.Text(msg.text))
|
||||||
|
is WebSocketMessage.Binary -> session.send(Frame.Binary(true, msg.bytes))
|
||||||
|
is WebSocketMessage.Ping -> session.send(Frame.Ping(msg.bytes))
|
||||||
|
is WebSocketMessage.Pong -> session.send(Frame.Pong(msg.bytes))
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("Attempted to send on a closed WebSocket: ${e.message}")
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun recv(): WebSocketMessage? {
|
||||||
|
return try {
|
||||||
|
when (val frame = session.incoming.receive()) {
|
||||||
|
is Frame.Text -> WebSocketMessage.Text(frame.readText())
|
||||||
|
is Frame.Binary -> WebSocketMessage.Binary(frame.readBytes())
|
||||||
|
is Frame.Ping -> WebSocketMessage.Ping(frame.readBytes())
|
||||||
|
is Frame.Pong -> WebSocketMessage.Pong(frame.readBytes())
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
} catch (e: ClosedReceiveChannelException) {
|
||||||
|
null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun closeConnection() {
|
||||||
|
session.cancel()
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CoopWebSocketClient(private val httpClient: HttpClient) : CustomWebSocketTransport {
|
||||||
|
override fun supportPing(): Boolean = false
|
||||||
|
|
||||||
|
override suspend fun connect(url: String, mode: ConnectionMode): WebSocketAdapterWrapper {
|
||||||
|
try {
|
||||||
|
val session = httpClient.webSocketSession {
|
||||||
|
url(url)
|
||||||
|
}
|
||||||
|
val adapter = KtorWebSocketAdapter(httpClient, session)
|
||||||
|
return WebSocketAdapterWrapper(adapter)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package su.reya.coop
|
package su.reya.coop
|
||||||
|
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.plugins.websocket.WebSockets
|
||||||
import rust.nostr.sdk.Client
|
import rust.nostr.sdk.Client
|
||||||
import rust.nostr.sdk.ClientBuilder
|
import rust.nostr.sdk.ClientBuilder
|
||||||
import rust.nostr.sdk.ClientNotification
|
import rust.nostr.sdk.ClientNotification
|
||||||
@@ -12,6 +14,7 @@ import rust.nostr.sdk.GossipConfig
|
|||||||
import rust.nostr.sdk.Keys
|
import rust.nostr.sdk.Keys
|
||||||
import rust.nostr.sdk.Kind
|
import rust.nostr.sdk.Kind
|
||||||
import rust.nostr.sdk.KindStandard
|
import rust.nostr.sdk.KindStandard
|
||||||
|
import rust.nostr.sdk.LogLevel
|
||||||
import rust.nostr.sdk.Metadata
|
import rust.nostr.sdk.Metadata
|
||||||
import rust.nostr.sdk.MetadataRecord
|
import rust.nostr.sdk.MetadataRecord
|
||||||
import rust.nostr.sdk.NostrConnect
|
import rust.nostr.sdk.NostrConnect
|
||||||
@@ -32,6 +35,7 @@ import rust.nostr.sdk.Timestamp
|
|||||||
import rust.nostr.sdk.UnsignedEvent
|
import rust.nostr.sdk.UnsignedEvent
|
||||||
import rust.nostr.sdk.UnwrappedGift
|
import rust.nostr.sdk.UnwrappedGift
|
||||||
import rust.nostr.sdk.extractMessagingRelayList
|
import rust.nostr.sdk.extractMessagingRelayList
|
||||||
|
import rust.nostr.sdk.initLogger
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
class Nostr {
|
class Nostr {
|
||||||
@@ -47,39 +51,37 @@ class Nostr {
|
|||||||
private set
|
private set
|
||||||
|
|
||||||
suspend fun init(dbPath: String) {
|
suspend fun init(dbPath: String) {
|
||||||
|
try {
|
||||||
|
// Initialize the logger for nostr client
|
||||||
|
initLogger(LogLevel.DEBUG)
|
||||||
|
|
||||||
val lmdb = NostrDatabase.lmdb(dbPath)
|
val lmdb = NostrDatabase.lmdb(dbPath)
|
||||||
val gossip = NostrGossip.inMemory()
|
val gossip = NostrGossip.inMemory()
|
||||||
val idleTimeout = Duration.parse("5m")
|
val idleTimeout = Duration.parse("5m")
|
||||||
|
val httpClient = HttpClient {
|
||||||
|
install(WebSockets)
|
||||||
|
}
|
||||||
|
|
||||||
client =
|
client =
|
||||||
ClientBuilder()
|
ClientBuilder()
|
||||||
|
.websocketTransport(CoopWebSocketClient(httpClient))
|
||||||
.database(lmdb)
|
.database(lmdb)
|
||||||
.gossip(gossip)
|
.gossip(gossip)
|
||||||
.gossipConfig(GossipConfig().noBackgroundRefresh())
|
.gossipConfig(GossipConfig().noBackgroundRefresh())
|
||||||
.maxRelays(20u)
|
|
||||||
.verifySubscriptions(false)
|
.verifySubscriptions(false)
|
||||||
.automaticAuthentication(false)
|
.automaticAuthentication(false)
|
||||||
.sleepWhenIdle(SleepWhenIdle.Enabled(idleTimeout))
|
.sleepWhenIdle(SleepWhenIdle.Enabled(idleTimeout))
|
||||||
.build()
|
.build()
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun connect() {
|
client?.addRelay(RelayUrl.parse("wss://relay.primal.net"))
|
||||||
try {
|
client?.addRelay(RelayUrl.parse("wss://user.kindpag.es"))
|
||||||
client?.addRelay(
|
|
||||||
url = RelayUrl.parse("wss://relay.primal.net"),
|
|
||||||
capabilities = RelayCapabilities.none()
|
|
||||||
)
|
|
||||||
client?.addRelay(
|
|
||||||
url = RelayUrl.parse("wss://user.kindpag.es"),
|
|
||||||
capabilities = RelayCapabilities.none()
|
|
||||||
)
|
|
||||||
client?.addRelay(
|
client?.addRelay(
|
||||||
url = RelayUrl.parse("wss://indexer.coracle.social"),
|
url = RelayUrl.parse("wss://indexer.coracle.social"),
|
||||||
capabilities = RelayCapabilities.gossip()
|
capabilities = RelayCapabilities.gossip()
|
||||||
)
|
)
|
||||||
client?.connect()
|
client?.connect(Duration.parse("10s"))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("Failed to connect to relays: ${e.message}")
|
println("Failed to initialize client: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +100,8 @@ class Nostr {
|
|||||||
try {
|
try {
|
||||||
signer = NostrSigner.keys(keys)
|
signer = NostrSigner.keys(keys)
|
||||||
userPubkey = signer?.getPublicKey()
|
userPubkey = signer?.getPublicKey()
|
||||||
|
|
||||||
|
// Fetch metadata for current user
|
||||||
getUserMetadata()
|
getUserMetadata()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("Failed to set signer: ${e.message}")
|
println("Failed to set signer: ${e.message}")
|
||||||
@@ -108,6 +112,8 @@ class Nostr {
|
|||||||
try {
|
try {
|
||||||
signer = NostrSigner.nostrConnect(remote)
|
signer = NostrSigner.nostrConnect(remote)
|
||||||
userPubkey = signer?.getPublicKey()
|
userPubkey = signer?.getPublicKey()
|
||||||
|
|
||||||
|
// Fetch metadata for current user
|
||||||
getUserMetadata()
|
getUserMetadata()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("Failed to set remote signer: ${e.message}")
|
println("Failed to set remote signer: ${e.message}")
|
||||||
@@ -123,19 +129,17 @@ class Nostr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getUserMetadata() {
|
suspend fun getUserMetadata() {
|
||||||
val userPubkey = signer?.getPublicKey() ?: return
|
|
||||||
|
|
||||||
// Get the latest metadata event
|
// Get the latest metadata event
|
||||||
val metadataFilter =
|
val metadataFilter =
|
||||||
Filter().author(userPubkey).limit(1u).kind(Kind.fromStd(KindStandard.METADATA))
|
Filter().author(userPubkey!!).limit(1u).kind(Kind.fromStd(KindStandard.METADATA))
|
||||||
|
|
||||||
// Get the latest contact list event
|
// Get the latest contact list event
|
||||||
val contactFilter =
|
val contactFilter =
|
||||||
Filter().author(userPubkey).limit(1u).kind(Kind.fromStd(KindStandard.CONTACT_LIST))
|
Filter().author(userPubkey!!).limit(1u).kind(Kind.fromStd(KindStandard.CONTACT_LIST))
|
||||||
|
|
||||||
// Get the latest messaging relay list event
|
// Get the latest messaging relay list event
|
||||||
val msgRelayFilter =
|
val msgRelayFilter =
|
||||||
Filter().author(userPubkey).limit(1u).kind(Kind.fromStd(KindStandard.INBOX_RELAYS))
|
Filter().author(userPubkey!!).limit(1u).kind(Kind.fromStd(KindStandard.INBOX_RELAYS))
|
||||||
|
|
||||||
// Construct a target that includes all filters
|
// Construct a target that includes all filters
|
||||||
val target = ReqTarget.auto(listOf(metadataFilter, contactFilter, msgRelayFilter))
|
val target = ReqTarget.auto(listOf(metadataFilter, contactFilter, msgRelayFilter))
|
||||||
@@ -170,11 +174,11 @@ class Nostr {
|
|||||||
|
|
||||||
suspend fun handleNotifications(onMetadataUpdate: (PublicKey, Metadata) -> Unit) {
|
suspend fun handleNotifications(onMetadataUpdate: (PublicKey, Metadata) -> Unit) {
|
||||||
val now = Timestamp.now()
|
val now = Timestamp.now()
|
||||||
val notifications = client?.notifications()
|
|
||||||
val processedEvent = mutableSetOf<EventId>()
|
val processedEvent = mutableSetOf<EventId>()
|
||||||
|
val notifications = client?.notifications() ?: return
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
val notification = notifications?.next() ?: break
|
val notification = notifications.next() ?: continue
|
||||||
|
|
||||||
when (notification) {
|
when (notification) {
|
||||||
is ClientNotification.Message -> {
|
is ClientNotification.Message -> {
|
||||||
@@ -189,7 +193,7 @@ class Nostr {
|
|||||||
if (processedEvent.contains(event.id())) continue
|
if (processedEvent.contains(event.id())) continue
|
||||||
processedEvent.add(event.id())
|
processedEvent.add(event.id())
|
||||||
|
|
||||||
if (event.kind().asStd() == KindStandard.METADATA) {
|
if (event.kind().asStd()?.equals(KindStandard.METADATA) == true) {
|
||||||
try {
|
try {
|
||||||
val metadata = Metadata.fromJson(event.content())
|
val metadata = Metadata.fromJson(event.content())
|
||||||
onMetadataUpdate(event.author(), metadata)
|
onMetadataUpdate(event.author(), metadata)
|
||||||
@@ -198,13 +202,13 @@ class Nostr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.kind().asStd() == KindStandard.INBOX_RELAYS) {
|
if (event.kind().asStd()?.equals(KindStandard.INBOX_RELAYS) == true) {
|
||||||
if (isSignedByUser(event = event)) {
|
if (isSignedByUser(event = event)) {
|
||||||
getUserMessages(msgRelayList = event)
|
getUserMessages(msgRelayList = event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.kind().asStd() == KindStandard.GIFT_WRAP) {
|
if (event.kind().asStd()?.equals(KindStandard.GIFT_WRAP) == true) {
|
||||||
try {
|
try {
|
||||||
val rumor = extractRumor(event)
|
val rumor = extractRumor(event)
|
||||||
// TODO: Handle rumor
|
// TODO: Handle rumor
|
||||||
|
|||||||
@@ -101,23 +101,19 @@ class NostrViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getUserProfile(): StateFlow<Metadata?> {
|
fun getUserProfile(): StateFlow<Metadata?> {
|
||||||
return getMetadata(nostr.userPubkey!!)
|
return nostr.userPubkey?.let { getMetadata(it) } ?: MutableStateFlow(null).asStateFlow()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun initAndConnect(dbPath: String) {
|
suspend fun initAndConnect(dbPath: String) {
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
try {
|
||||||
// Initialize nostr client
|
// Initialize nostr client
|
||||||
nostr.init(dbPath)
|
nostr.init(dbPath)
|
||||||
// Connect to bootstrap relays
|
|
||||||
nostr.connect()
|
|
||||||
// Get user's secret
|
// Get user's secret
|
||||||
getUserSecret()
|
getUserSecret()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
showError("Failed to initialize Nostr: ${e.message}")
|
showError("Failed to initialize Nostr: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun startNotificationHandler() {
|
fun startNotificationHandler() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
|||||||
Reference in New Issue
Block a user