diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/App.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/App.kt index 6eefaf2..bd27100 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/App.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/App.kt @@ -6,23 +6,11 @@ import android.os.Build import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialExpressiveTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.MotionScheme import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text import androidx.compose.material3.Typography import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme @@ -38,13 +26,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.staticCompositionLocalOf -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier 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.unit.dp import androidx.core.util.Consumer import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator @@ -54,7 +36,6 @@ import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.ui.NavDisplay -import kotlinx.coroutines.launch import su.reya.coop.screens.ChatScreen import su.reya.coop.screens.HomeScreen import su.reya.coop.screens.ImportScreen @@ -95,7 +76,6 @@ fun App(viewModel: NostrViewModel) { val qrScanResult = remember { QrScanResult() } val signerRequired by viewModel.signerRequired.collectAsStateWithLifecycle() - val isRelayListEmpty by viewModel.isRelayListEmpty.collectAsStateWithLifecycle() // Snackbar val snackbarHostState = remember { SnackbarHostState() } @@ -219,61 +199,6 @@ fun App(viewModel: NostrViewModel) { } } ) - - // Show the relay setup dialog if the msg relay list is empty - if (isRelayListEmpty) { - ModalBottomSheet( - onDismissRequest = { viewModel.dismissRelayWarning() }, - sheetState = sheetState, - containerColor = MaterialTheme.colorScheme.surfaceContainer, - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(0.5f) - .padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - text = "Messaging Relays are required", - style = MaterialTheme.typography.headlineSmallEmphasized.copy( - fontWeight = FontWeight.SemiBold, - ), - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.primary, - ) - Spacer(modifier = Modifier.size(8.dp)) - Text( - text = "Coop cannot found your messaging relays. To send and receive messages on Coop, you need to set up at least one messaging relay.", - style = MaterialTheme.typography.bodyLarge - ) - Spacer(modifier = Modifier.size(8.dp)) - Text( - text = "Please click the button below to continue with the default set of relays. You can always change them later in the settings.", - style = MaterialTheme.typography.bodyLarge.copy( - fontStyle = FontStyle.Italic, - ), - ) - Spacer(modifier = Modifier.weight(1f)) - Button( - onClick = { - scope.launch { - viewModel.useDefaultMsgRelayList() - sheetState.hide() - } - }, - modifier = Modifier - .fillMaxWidth() - .height(ButtonDefaults.MediumContainerHeight), - ) { - Text( - text = "Continue", - style = MaterialTheme.typography.titleMediumEmphasized, - ) - } - } - } - } } } } diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt index 58b016e..6c306eb 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt @@ -55,7 +55,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import coop.composeapp.generated.resources.Res import coop.composeapp.generated.resources.ic_arrow_back import coop.composeapp.generated.resources.ic_send -import kotlinx.coroutines.flow.first import org.jetbrains.compose.resources.painterResource import rust.nostr.sdk.UnsignedEvent import su.reya.coop.LocalNavigator @@ -67,7 +66,6 @@ import su.reya.coop.roomId import su.reya.coop.shared.Avatar import su.reya.coop.shared.displayNameFlow import su.reya.coop.shared.pictureFlow -import su.reya.coop.short @Composable fun ChatScreen(id: Long) { @@ -77,9 +75,7 @@ fun ChatScreen(id: Long) { // Get chat room by ID val chatRooms by viewModel.chatRooms.collectAsStateWithLifecycle() - val room by remember(id) { - derivedStateOf { chatRooms.firstOrNull { it.id == id } } - } + val room by remember(id) { derivedStateOf { chatRooms.firstOrNull { it.id == id } } } // Show empty screen if (room == null) { @@ -88,7 +84,7 @@ fun ChatScreen(id: Long) { contentAlignment = Alignment.Center ) { Text( - text = "Chat room not found", + text = "Something went wrong.", style = MaterialTheme.typography.titleMediumEmphasized, color = MaterialTheme.colorScheme.onSurface ) @@ -114,23 +110,14 @@ fun ChatScreen(id: Long) { // Start loading spinner loading = true + // Get msg relays for each member + viewModel.chatRoomConnect(id) + // Get messages val initialMessages = viewModel.getChatRoomMessages(id) messages.clear() messages.addAll(initialMessages) - - // Get msg relays for each member - val results = viewModel.chatRoomConnect(id) - results.forEach { (member, relays) -> - if (relays.isNotEmpty()) { - val metadata = viewModel.getMetadata(member).first { it != null } - val profile = metadata?.asRecord() - val name = profile?.displayName ?: profile?.name ?: member.short() - - snackbarHostState.showSnackbar("Connected to messaging relays for $name") - } - } - + // Stop loading spinner loading = false 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 3c85ade..ef66142 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt @@ -13,8 +13,10 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding @@ -72,7 +74,9 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.LocalClipboard 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.unit.dp import androidx.core.app.NotificationManagerCompat import androidx.lifecycle.compose.LifecycleResumeEffect @@ -111,6 +115,7 @@ fun HomeScreen() { val userProfile by currentUserProfile.collectAsStateWithLifecycle() val chatRooms by viewModel.chatRooms.collectAsStateWithLifecycle() + val isRelayListEmpty by viewModel.isRelayListEmpty.collectAsStateWithLifecycle() val isPartialProcessedGiftWrap by viewModel.isPartialProcessedGiftWrap.collectAsState(initial = false) val isBannerDismissed by viewModel.isNotificationBannerDismissed.collectAsState() @@ -448,6 +453,77 @@ fun HomeScreen() { } }, ) + + // Show the relay setup dialog if the msg relay list is empty + if (isRelayListEmpty) { + ModalBottomSheet( + onDismissRequest = { viewModel.dismissRelayWarning() }, + sheetState = sheetState, + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(0.5f) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "Messaging Relays are required", + style = MaterialTheme.typography.headlineSmallEmphasized.copy( + fontWeight = FontWeight.SemiBold, + ), + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.primary, + ) + Spacer(modifier = Modifier.size(8.dp)) + Text( + text = "Coop cannot found your messaging relays. To send and receive messages on Coop, you need to set up at least one messaging relay.", + style = MaterialTheme.typography.bodyLarge + ) + Spacer(modifier = Modifier.size(8.dp)) + Text( + text = "Please click the button below to continue with the default set of relays. You can always change them later in the settings.", + style = MaterialTheme.typography.bodyLarge.copy( + fontStyle = FontStyle.Italic, + ), + ) + Spacer(modifier = Modifier.weight(1f)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + TextButton( + onClick = { }, + modifier = Modifier + .weight(1f) + .height(ButtonDefaults.MediumContainerHeight), + ) { + Text( + text = "Retry", + style = MaterialTheme.typography.titleMediumEmphasized, + ) + } + Button( + onClick = { + scope.launch { + viewModel.useDefaultMsgRelayList() + sheetState.hide() + } + }, + modifier = Modifier + .weight(1f) + .height(ButtonDefaults.MediumContainerHeight), + ) { + Text( + text = "Use Default", + style = MaterialTheme.typography.titleMediumEmphasized, + ) + } + } + } + } + } } @OptIn(ExperimentalMaterial3ExpressiveApi::class) diff --git a/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt b/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt index aad656c..6f8d1a8 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt @@ -453,9 +453,10 @@ class Nostr { client?.addRelay( url = relay, capabilities = - if (metadata == RelayMetadata.READ) RelayCapabilities.read() - else if (metadata == RelayMetadata.WRITE) RelayCapabilities.write() - else RelayCapabilities.none() + when (metadata) { + RelayMetadata.READ -> RelayCapabilities.read() + RelayMetadata.WRITE -> RelayCapabilities.write() + } ) client?.connectRelay(relay) } @@ -466,7 +467,7 @@ class Nostr { suspend fun getDefaultMsgRelayList(): List { // Construct a list of messaging relays val msgRelayList = listOf( - RelayUrl.parse("wss://relay.0xchat.com"), + RelayUrl.parse("wss://auth.nostr1.com"), RelayUrl.parse("wss://nip17.com"), ) @@ -721,33 +722,25 @@ class Nostr { } } - suspend fun chatRoomConnect(members: List): Map> { + suspend fun chatRoomConnect(members: List) { try { - val results = mutableMapOf>() - members.forEach { member -> - results[member] = mutableListOf() val kind = Kind.fromStd(KindStandard.INBOX_RELAYS) val filter = Filter().kind(kind).author(member).limit(1u) val stream = client?.streamEvents( target = ReqTarget.auto(listOf(filter)), - id = "room-${member.toBech32().substring(0, 10)}", + id = null, timeout = Duration.parse("3s"), policy = ReqExitPolicy.ExitOnEose ) stream?.next()?.let { res -> if (res.event != null) { - // Connect to the msg relays connectMsgRelays(res.event!!) - // Mark the member as connected - results[member]?.add(res.relayUrl) } } } - - return results } catch (e: Exception) { throw IllegalStateException("Failed to fetch relays: ${e.message}", e) } @@ -757,10 +750,8 @@ class Nostr { try { val urls = nip17ExtractRelayList(event); for (url in urls) { - if (client?.relay(url) == null) { - client?.addRelay(url) - client?.connectRelay(url) - } + client?.addRelay(url, RelayCapabilities.gossip()) + client?.connectRelay(url) } } catch (e: Exception) { throw IllegalStateException("Failed to connect to relays: ${e.message}", e) diff --git a/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt b/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt index 0088e66..077d8f6 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt @@ -644,20 +644,16 @@ class NostrViewModel( return emptyList() } - suspend fun chatRoomConnect(roomId: Long): Map> { - try { - val room = getChatRoom(roomId) ?: throw IllegalArgumentException("Room not found") - val members = room.members + fun chatRoomConnect(roomId: Long) { + viewModelScope.launch { + try { + val room = getChatRoom(roomId) ?: throw IllegalArgumentException("Room not found") + val members = room.members - return runCatching { nostr.chatRoomConnect(members.toList()) - }.getOrElse { e -> + } catch (e: Exception) { showError("Error: ${e.message}") - members.associateWith { emptyList() } } - } catch (e: Exception) { - showError("Error: ${e.message}") - return emptyMap() } }