diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts
index 7f3b884..2a3dcc3 100644
--- a/composeApp/build.gradle.kts
+++ b/composeApp/build.gradle.kts
@@ -24,7 +24,7 @@ kotlin {
implementation(libs.jetbrains.navigation3.ui)
implementation(libs.jetbrains.lifecycle.viewmodelNavigation3)
implementation(libs.androidx.core.splashscreen)
- implementation("su.reya:nostr-sdk-kmp:0.2.7")
+ implementation("su.reya:nostr-sdk-kmp:0.3")
implementation("io.coil-kt.coil3:coil-compose:3.4.0")
implementation("io.coil-kt.coil3:coil-network-okhttp:3.4.0")
implementation("io.github.kalinjul.easyqrscan:scanner:0.7.0")
diff --git a/composeApp/src/androidMain/composeResources/drawable/ic_report.xml b/composeApp/src/androidMain/composeResources/drawable/ic_report.xml
new file mode 100644
index 0000000..0b85c04
--- /dev/null
+++ b/composeApp/src/androidMain/composeResources/drawable/ic_report.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/composeApp/src/androidMain/composeResources/drawable/ic_request.xml b/composeApp/src/androidMain/composeResources/drawable/ic_request.xml
new file mode 100644
index 0000000..38af524
--- /dev/null
+++ b/composeApp/src/androidMain/composeResources/drawable/ic_request.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/App.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/App.kt
index 1b246d3..ed281a6 100644
--- a/composeApp/src/androidMain/kotlin/su/reya/coop/App.kt
+++ b/composeApp/src/androidMain/kotlin/su/reya/coop/App.kt
@@ -44,6 +44,7 @@ import su.reya.coop.screens.NewIdentityScreen
import su.reya.coop.screens.OnboardingScreen
import su.reya.coop.screens.ProfileScreen
import su.reya.coop.screens.RelayScreen
+import su.reya.coop.screens.RequestListScreen
import su.reya.coop.screens.ScanScreen
import su.reya.coop.screens.UpdateProfileScreen
@@ -164,6 +165,9 @@ fun App(viewModel: NostrViewModel) {
entry {
HomeScreen()
}
+ entry {
+ RequestListScreen()
+ }
entry {
OnboardingScreen()
}
diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/Navigation.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/Navigation.kt
index 50aca39..6a53f67 100644
--- a/composeApp/src/androidMain/kotlin/su/reya/coop/Navigation.kt
+++ b/composeApp/src/androidMain/kotlin/su/reya/coop/Navigation.kt
@@ -23,6 +23,9 @@ sealed interface Screen : NavKey {
@Serializable
data object Home : Screen
+ @Serializable
+ data object RequestList : Screen
+
@Serializable
data class Chat(val id: Long) : Screen
diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt
index d29813d..01b8d72 100644
--- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt
+++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt
@@ -76,6 +76,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.core.app.NotificationManagerCompat
import androidx.lifecycle.compose.LifecycleResumeEffect
@@ -84,7 +85,9 @@ import coop.composeapp.generated.resources.Res
import coop.composeapp.generated.resources.ic_close
import coop.composeapp.generated.resources.ic_new_chat
import coop.composeapp.generated.resources.ic_qr
+import coop.composeapp.generated.resources.ic_request
import coop.composeapp.generated.resources.ic_scanner
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.painterResource
import rust.nostr.sdk.PublicKey
@@ -93,6 +96,7 @@ import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalScanResult
import su.reya.coop.LocalSnackbarHostState
import su.reya.coop.Room
+import su.reya.coop.RoomKind
import su.reya.coop.Screen
import su.reya.coop.ago
import su.reya.coop.shared.Avatar
@@ -111,8 +115,14 @@ fun HomeScreen() {
val clipboardManager = LocalClipboard.current
val viewModel = LocalNostrViewModel.current
+
+ val scope = rememberCoroutineScope()
+ val sheetState = rememberModalBottomSheetState(true)
+ val listState = rememberLazyListState()
+ val pullToRefreshState = rememberPullToRefreshState()
+
val currentUser = viewModel.currentUser() ?: return
- val currentUserProfile = viewModel.getMetadata(currentUser) ?: return
+ val currentUserProfile = viewModel.getMetadata(currentUser)
val userProfile by currentUserProfile.collectAsStateWithLifecycle()
val chatRooms by viewModel.chatRooms.collectAsStateWithLifecycle()
@@ -120,11 +130,6 @@ fun HomeScreen() {
val isPartialProcessedGiftWrap by viewModel.isPartialProcessedGiftWrap.collectAsState(initial = false)
val isBannerDismissed by viewModel.isNotificationBannerDismissed.collectAsState()
- val scope = rememberCoroutineScope()
- val sheetState = rememberModalBottomSheetState(true)
- val listState = rememberLazyListState()
- val pullToRefreshState = rememberPullToRefreshState()
-
val expandedFab by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } }
var showBottomSheet by remember { mutableStateOf(false) }
var isRefreshing by remember { mutableStateOf(false) }
@@ -140,6 +145,11 @@ fun HomeScreen() {
// State will be updated by LifecycleResumeEffect
}
+ // Partition chat rooms into requests and ongoing
+ val (requests, ongoing) = remember(chatRooms) {
+ chatRooms.partition { it.kind == RoomKind.Request }
+ }
+
LifecycleResumeEffect(context) {
isNotificationEnabled = NotificationManagerCompat.from(context).areNotificationsEnabled()
onPauseOrDispose { }
@@ -350,7 +360,11 @@ fun HomeScreen() {
state = listState,
modifier = Modifier.fillMaxSize()
) {
- items(chatRooms.toList(), key = { it.id }) { room ->
+ if (requests.isNotEmpty()) {
+ item { NewRequests(requests) }
+ }
+
+ items(ongoing, key = { it.id }) { room ->
ChatRoom(
room = room,
onClick = { navigator.navigate(Screen.Chat(room.id)) }
@@ -603,6 +617,89 @@ fun HomeScreen() {
}
}
+@Composable
+fun NewRequests(requests: List) {
+ val navigator = LocalNavigator.current
+ val viewModel = LocalNostrViewModel.current
+
+ val total = requests.size
+ val firstRoom = requests.getOrNull(0)
+ val secondRoom = requests.getOrNull(1)
+
+ val firstName by remember(firstRoom) {
+ firstRoom?.displayNameFlow(viewModel) ?: flowOf("")
+ }.collectAsStateWithLifecycle("Loading...")
+
+ val secondName by remember(secondRoom) {
+ secondRoom?.displayNameFlow(viewModel) ?: flowOf("")
+ }.collectAsStateWithLifecycle("")
+
+ val supportingText = when {
+ total == 1 && firstRoom != null -> {
+ val message = firstRoom.lastMessage ?: ""
+ "$firstName: $message"
+ }
+
+ total == 2 -> {
+ "$firstName and $secondName"
+ }
+
+ total > 2 -> {
+ val othersCount = total - 2
+ val othersText = if (othersCount == 1) "1 other" else "$othersCount others"
+ "$firstName, $secondName and $othersText"
+ }
+
+ else -> ""
+ }
+
+ ListItem(
+ modifier = Modifier.clickable {
+ navigator.navigate(Screen.RequestList)
+ },
+ leadingContent = {
+ Box(
+ modifier = Modifier
+ .size(48.dp)
+ .clip(MaterialShapes.Clover4Leaf.toShape()),
+ contentAlignment = Alignment.Center
+ ) {
+ Surface(
+ modifier = Modifier.size(48.dp),
+ color = MaterialTheme.colorScheme.tertiaryContainer,
+ ) {
+ Box(contentAlignment = Alignment.Center) {
+ Icon(
+ painter = painterResource(Res.drawable.ic_request),
+ contentDescription = "Requests",
+ tint = MaterialTheme.colorScheme.onTertiaryFixed
+ )
+ }
+ }
+ }
+ },
+ headlineContent = {
+ Text(
+ text = "Requests",
+ style = MaterialTheme.typography.titleMediumEmphasized
+ )
+ },
+ supportingContent = {
+ if (supportingText.isNotEmpty()) {
+ Text(
+ text = supportingText,
+ style = MaterialTheme.typography.bodyMedium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ },
+ colors = ListItemDefaults.colors(
+ containerColor = MaterialTheme.colorScheme.surface
+ )
+ )
+}
+
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ChatRoom(room: Room, onClick: () -> Unit) {
@@ -636,7 +733,8 @@ fun ChatRoom(room: Room, onClick: () -> Unit) {
if (!room.lastMessage.isNullOrBlank()) {
Text(
text = room.lastMessage!!,
- style = MaterialTheme.typography.bodyMedium
+ style = MaterialTheme.typography.bodyMedium,
+ overflow = TextOverflow.Ellipsis
)
}
},
diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/RequestListScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/RequestListScreen.kt
new file mode 100644
index 0000000..75ee8da
--- /dev/null
+++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/RequestListScreen.kt
@@ -0,0 +1,155 @@
+package su.reya.coop.screens
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.pulltorefresh.PullToRefreshBox
+import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
+import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+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.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import coop.composeapp.generated.resources.Res
+import coop.composeapp.generated.resources.ic_arrow_back
+import kotlinx.coroutines.launch
+import su.reya.coop.LocalNavigator
+import su.reya.coop.LocalNostrViewModel
+import su.reya.coop.LocalSnackbarHostState
+import su.reya.coop.RoomKind
+import su.reya.coop.Screen
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
+@Composable
+fun RequestListScreen() {
+ val navigator = LocalNavigator.current
+ val snackbarHostState = LocalSnackbarHostState.current
+ val viewModel = LocalNostrViewModel.current
+
+ val scope = rememberCoroutineScope()
+ val listState = rememberLazyListState()
+ val pullToRefreshState = rememberPullToRefreshState()
+
+ var isRefreshing by remember { mutableStateOf(false) }
+ val chatRooms by viewModel.chatRooms.collectAsStateWithLifecycle()
+
+ // Get all request rooms
+ val requests = remember(chatRooms) {
+ chatRooms.filter { it.kind == RoomKind.Request }
+ }
+
+ Scaffold(
+ snackbarHost = { SnackbarHost(snackbarHostState) },
+ containerColor = MaterialTheme.colorScheme.surfaceContainer,
+ topBar = {
+ TopAppBar(
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainer,
+ ),
+ title = {
+ Text("New Requests", style = MaterialTheme.typography.titleMediumEmphasized)
+ },
+ navigationIcon = {
+ IconButton(onClick = { navigator.goBack() }) {
+ Icon(
+ painter = org.jetbrains.compose.resources.painterResource(Res.drawable.ic_arrow_back),
+ contentDescription = "Back"
+ )
+ }
+ },
+ )
+ },
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier.padding(top = innerPadding.calculateTopPadding()),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.surface,
+ shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp),
+ ) {
+ PullToRefreshBox(
+ modifier = Modifier.fillMaxSize(),
+ isRefreshing = isRefreshing,
+ state = pullToRefreshState,
+ onRefresh = {
+ scope.launch {
+ isRefreshing = true
+ viewModel.refreshChatRooms()
+ isRefreshing = false
+ }
+ },
+ indicator = {
+ PullToRefreshDefaults.LoadingIndicator(
+ state = pullToRefreshState,
+ isRefreshing = isRefreshing,
+ modifier = Modifier.align(Alignment.TopCenter),
+ )
+ }
+ ) {
+ if (requests.isEmpty()) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ ) {
+ Text(
+ text = "No requests yet",
+ style = MaterialTheme.typography.titleLargeEmphasized.copy(
+ fontWeight = FontWeight.SemiBold
+ ),
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ Text(
+ text = "New chat requests will appear here.",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.outline
+ )
+ }
+ }
+ } else {
+ LazyColumn(
+ state = listState,
+ modifier = Modifier.fillMaxSize()
+ ) {
+ items(requests.toList(), key = { it.id }) { room ->
+ ChatRoom(
+ room = room,
+ onClick = { navigator.navigate(Screen.Chat(room.id)) }
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts
index f8e160e..ed524c4 100644
--- a/shared/build.gradle.kts
+++ b/shared/build.gradle.kts
@@ -33,7 +33,7 @@ kotlin {
implementation(libs.androidx.lifecycle.runtimeCompose)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.8.0")
- implementation("su.reya:nostr-sdk-kmp:0.2.7")
+ implementation("su.reya:nostr-sdk-kmp:0.3")
implementation("com.squareup.okio:okio:3.16.2")
}
androidMain.dependencies {
diff --git a/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt b/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt
index 6c88227..5baa07e 100644
--- a/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt
+++ b/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt
@@ -50,11 +50,11 @@ import rust.nostr.sdk.SubscribeAutoCloseOptions
import rust.nostr.sdk.Tag
import rust.nostr.sdk.Timestamp
import rust.nostr.sdk.UnsignedEvent
-import rust.nostr.sdk.UnwrappedGift
import rust.nostr.sdk.extractRelayList
import rust.nostr.sdk.initLogger
import rust.nostr.sdk.nip17ExtractRelayList
import rust.nostr.sdk.nip59MakeGiftWrapAsync
+import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
@@ -78,8 +78,6 @@ class Nostr {
private set
var signer: UniversalSigner = UniversalSigner(Keys.generate())
private set
- var deviceSigner: AsyncNostrSigner? = null
- private set
var sentEvents: MutableMap> = mutableMapOf()
private set
var rumorMap: MutableMap = mutableMapOf()
@@ -310,26 +308,22 @@ class Nostr {
}
if (event.kind().asStd()?.equals(KindStandard.GIFT_WRAP) == true) {
- try {
- val rumor = extractRumor(event)
+ val rumor = extractRumor(event)
- // Logic to notify UI after processing
- // Cancel previous tracker if it exists
- eoseTrackerJob?.cancel()
- // Start a new tracker
- eoseTrackerJob = launch {
- delay(10000.milliseconds) // Wait for 10 seconds
- onSubscriptionClose()
- }
+ // Logic to notify UI after processing
+ // Cancel previous tracker if it exists
+ eoseTrackerJob?.cancel()
+ // Start a new tracker
+ eoseTrackerJob = launch {
+ delay(10000.milliseconds) // Wait for 10 seconds
+ onSubscriptionClose()
+ }
- // Handle new message
- rumor?.createdAt()?.asSecs()?.let {
- if (it >= now.asSecs()) {
- onNewMessage(rumor)
- }
+ // Handle new message
+ rumor?.createdAt()?.asSecs()?.let {
+ if (it >= now.asSecs()) {
+ onNewMessage(rumor)
}
- } catch (e: Exception) {
- println("Failed to extract rumor: $e")
}
}
}
@@ -372,7 +366,7 @@ class Nostr {
val event = client?.database()?.query(filter)?.first()
return event?.content()?.let { UnsignedEvent.fromJson(it) }
- } catch (e: Exception) {
+ } catch (e: Throwable) {
throw IllegalStateException("Failed to get cached rumor: ${e.message}", e)
}
}
@@ -392,7 +386,7 @@ class Nostr {
)
// Set event kind
- val kind = Kind.fromStd(KindStandard.APPLICATION_SPECIFIC_DATA);
+ val kind = Kind.fromStd(KindStandard.APPLICATION_SPECIFIC_DATA)
// Construct event
val event = EventBuilder(kind, rumor.asJson())
@@ -400,36 +394,64 @@ class Nostr {
.finalizeAsync(Keys.generate())
client?.database()?.saveEvent(event)
- } catch (e: Exception) {
+ } catch (e: Throwable) {
println("Failed to set cached rumor: ${e.message}")
}
}
private suspend fun extractRumor(event: Event): UnsignedEvent? {
try {
+ // Gift wrap must have at least one 'p' tag
+ if (event.tags().publicKeys().isEmpty()) {
+ println("No recipient tags found.")
+ return null
+ }
+
+ // Event must be a gift wrap
+ if (event.kind().asStd().let { it != KindStandard.GIFT_WRAP }) {
+ println("Event is not a gift wrap.")
+ return null
+ }
+
// Check if the rumor is already cached
val cachedRumor = getCachedRumor(event.id())
if (cachedRumor != null) return cachedRumor
- // Unwrap the gift with current signer
- val gift = UnwrappedGift.fromGiftWrapAsync(signer = signer, giftWrap = event)
- val rumor = gift.rumor()
+ // Decrypt the gift wrap event
+ val seal = signer.nip44DecryptAsync(event.author(), event.content())
+ val sealEvent = Event.fromJson(seal)
- // Save the rumor to the database
- setCachedRumor(event.id(), rumor)
+ // Verify seal event
+ if (!sealEvent.verify()) {
+ println("Failed to verify seal event.")
+ return null
+ }
- // Return the rumor
- return rumor
- } catch (e: Exception) {
- println("Failed to unwrap gift: ${e.message}")
+ // Decrypt the rumor
+ val rumor = signer.nip44DecryptAsync(sealEvent.author(), sealEvent.content())
+ val unsignedEvent = UnsignedEvent.fromJson(rumor)
+
+ // Ensure the rumor author matches the seal
+ if (unsignedEvent.author() != sealEvent.author()) {
+ println("Author mismatch.")
+ return null
+ }
+
+ // Cache the rumor for later use
+ setCachedRumor(event.id(), unsignedEvent)
+
+ return unsignedEvent
+ } catch (e: CancellationException) {
+ throw e
+ } catch (e: Throwable) {
+ println("Failed to unwrap gift ${event.id().toHex()}: ${e.message}")
+ return null
}
-
- return null
}
private suspend fun getDefaultRelayList(): Map {
// Construct a list of relays
- val relayList = mapOf(
+ val relayList = mapOf(
RelayUrl.parse("wss://relay.damus.io") to RelayMetadata.READ,
RelayUrl.parse("wss://relay.primal.net") to RelayMetadata.READ,
RelayUrl.parse("wss://relay.nostr.net") to RelayMetadata.WRITE,
@@ -471,7 +493,7 @@ class Nostr {
suspend fun createIdentity(keys: Keys, name: String, bio: String?, picture: String?) {
// Send relay list event
val relayList = getDefaultRelayList()
- val relayListEvent = EventBuilder.relayList(relayList).finalizeAsync(keys);
+ val relayListEvent = EventBuilder.relayList(relayList).finalizeAsync(keys)
client?.sendEvent(
event = relayListEvent,
@@ -546,7 +568,7 @@ class Nostr {
private suspend fun getLatestMetadata(pubkey: PublicKey): Metadata? {
return try {
- val kind = Kind.fromStd(KindStandard.METADATA);
+ val kind = Kind.fromStd(KindStandard.METADATA)
val filter = Filter().kind(kind).author(pubkey).limit(1u)
val event = client?.database()?.query(filter)?.first() ?: return null
@@ -581,7 +603,7 @@ class Nostr {
suspend fun fetchMetadataBatch(keys: List) {
try {
- val limit = keys.size.toULong() * 4u;
+ val limit = keys.size.toULong() * 4u
val opts = SubscribeAutoCloseOptions().exitPolicy(ReqExitPolicy.ExitOnEose)
// Construct a filter for metadata events
@@ -615,7 +637,7 @@ class Nostr {
ackPolicy = AckPolicy.none(),
)
- val kind = Kind.fromStd(KindStandard.INBOX_RELAYS);
+ val kind = Kind.fromStd(KindStandard.INBOX_RELAYS)
val filter = Filter().kind(kind).author(signer.currentUser!!).limit(1u)
val target = ReqTarget.auto(listOf(filter))
val opts = SubscribeAutoCloseOptions().exitPolicy(ReqExitPolicy.ExitOnEose)
@@ -722,7 +744,7 @@ class Nostr {
val filter = Filter().kind(kind).author(userPubkey).pubkeys(pubkeys)
// Determine if it's an ongoing room
- val isOngoing = client?.database()?.query(filter)?.isEmpty() == false
+ val isOngoing = client?.database()?.query(filter)?.isEmpty() ?: false
// Append room to map
roomsMap[newRoom.id] =
@@ -781,7 +803,7 @@ class Nostr {
suspend fun connectMsgRelays(event: Event) {
try {
- val urls = nip17ExtractRelayList(event);
+ val urls = nip17ExtractRelayList(event)
for (url in urls) {
client?.addRelay(url, RelayCapabilities.gossip())
client?.connectRelay(url)