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 ef66142..6c16207 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt @@ -82,6 +82,7 @@ import androidx.core.app.NotificationManagerCompat import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle 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_scanner @@ -97,6 +98,7 @@ import su.reya.coop.Screen import su.reya.coop.ago import su.reya.coop.shared.Avatar import su.reya.coop.shared.displayNameFlow +import su.reya.coop.shared.getExpressiveFontFamily import su.reya.coop.shared.pictureFlow import su.reya.coop.short @@ -120,13 +122,14 @@ fun HomeScreen() { val isBannerDismissed by viewModel.isNotificationBannerDismissed.collectAsState() val scope = rememberCoroutineScope() - val sheetState = rememberModalBottomSheetState() + 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) } + var isBusy by remember { mutableStateOf(false) } var isNotificationEnabled by remember { mutableStateOf(NotificationManagerCompat.from(context).areNotificationsEnabled()) @@ -465,60 +468,138 @@ fun HomeScreen() { modifier = Modifier .fillMaxWidth() .fillMaxHeight(0.5f) - .padding(24.dp), + .padding(horizontal = 24.dp) + .navigationBarsPadding(), horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), ) { Text( - text = "Messaging Relays are required", - style = MaterialTheme.typography.headlineSmallEmphasized.copy( + text = "Messaging Relays are missing", + style = MaterialTheme.typography.titleLargeEmphasized.copy( fontWeight = FontWeight.SemiBold, + fontFamily = getExpressiveFontFamily() ), 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)) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Surface( + modifier = Modifier.size(24.dp), + shape = MaterialShapes.Circle.toShape(), + color = MaterialTheme.colorScheme.error, + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(Res.drawable.ic_close), + contentDescription = "X", + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.onError + ) + } + } + Text( + text = "Other people won't be able to send you messages.", + style = MaterialTheme.typography.titleSmallEmphasized, + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Surface( + modifier = Modifier.size(24.dp), + shape = MaterialShapes.Circle.toShape(), + color = MaterialTheme.colorScheme.error, + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(Res.drawable.ic_close), + contentDescription = "X", + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.onError + ) + } + } + Text( + text = "You cannot store your messages.", + style = MaterialTheme.typography.titleSmallEmphasized, + ) + } 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( + style = MaterialTheme.typography.bodySmall.copy( + fontStyle = FontStyle.Italic, + ), + ) + Text( + text = "If you believe this is a mistake, please click the Retry button to check again.", + style = MaterialTheme.typography.bodySmall.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), + if (isBusy) { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center ) { - Text( - text = "Retry", - style = MaterialTheme.typography.titleMediumEmphasized, - ) + LoadingIndicator() } - Button( - onClick = { - scope.launch { - viewModel.useDefaultMsgRelayList() - sheetState.hide() - } - }, - modifier = Modifier - .weight(1f) - .height(ButtonDefaults.MediumContainerHeight), + } else { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - Text( - text = "Use Default", - style = MaterialTheme.typography.titleMediumEmphasized, - ) + TextButton( + enabled = !isBusy, + onClick = { + scope.launch { + isBusy = true + try { + viewModel.refetchMsgRelays(currentUser) + } catch (e: Exception) { + snackbarHostState.showSnackbar("Failed to refresh metadata: ${e.message}") + } + isBusy = false + } + }, + modifier = Modifier + .weight(1f) + .height(ButtonDefaults.MediumContainerHeight), + ) { + Text( + text = "Retry", + style = MaterialTheme.typography.titleMediumEmphasized, + ) + } + Button( + enabled = !isBusy, + onClick = { + scope.launch { + viewModel.useDefaultMsgRelayList() + sheetState.hide() + } + }, + modifier = Modifier + .weight(1f) + .height(ButtonDefaults.MediumContainerHeight), + ) { + Text( + text = "Use Default", + style = MaterialTheme.typography.titleMediumEmphasized, + ) + } } } } diff --git a/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt b/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt index 6f8d1a8..746c9b9 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt @@ -190,11 +190,6 @@ class Nostr { } } - suspend fun exit() { - signer.switch(Keys.generate()) - deviceSigner = null - } - suspend fun setSigner(new: AsyncNostrSigner) { try { signer.switch(new) @@ -650,6 +645,19 @@ class Nostr { } } + suspend fun fetchMsgRelays(publicKey: PublicKey): List { + try { + val kind = Kind.fromStd(KindStandard.INBOX_RELAYS) + val filter = Filter().kind(kind).author(publicKey).limit(1u) + val target = ReqTarget.auto(listOf(filter)) + val events = client?.fetchEvents(target, timeout = Duration.parse("3s")) + + return nip17ExtractRelayList(events?.toVec()?.firstOrNull() ?: return emptyList()) + } catch (e: Exception) { + throw IllegalStateException("Failed to fetch msg relays: ${e.message}", e) + } + } + suspend fun getRelayList(publicKey: PublicKey): Map { try { val kind = Kind.fromStd(KindStandard.RELAY_LIST) diff --git a/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt b/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt index 077d8f6..bc1f7b6 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt @@ -146,20 +146,6 @@ class NostrViewModel( } } - private fun processIncomingEvent(event: UnsignedEvent) { - val roomId = event.roomId() - val existingRoom = _chatRooms.value.firstOrNull { it.id == roomId } - - if (existingRoom == null) { - nostr.signer.currentUser?.let { user -> - val newRoom = Room.new(event, user) - _chatRooms.update { (it + newRoom).sortedDescending().toSet() } - } - } else { - updateRoomList(roomId, event) - } - } - private suspend fun runObserver() = coroutineScope { // Observe new messages launch { @@ -298,13 +284,11 @@ class NostrViewModel( nostr.getUserMetadata() // Small delay to ensure all relays are connected - delay(3000.milliseconds) + delay(2.seconds) // Check if the relay list is empty val relays = nostr.getMsgRelays(pubkey) - if (relays.isEmpty()) { - _isRelayListEmpty.value = true - } + if (relays.isEmpty()) _isRelayListEmpty.value = true break } @@ -540,6 +524,11 @@ class NostrViewModel( return externalSignerHandler?.isAvailable() == true } + suspend fun refetchMsgRelays(pubkey: PublicKey) { + val relays = nostr.fetchMsgRelays(pubkey) + if (relays.isNotEmpty()) dismissRelayWarning() + } + suspend fun useDefaultMsgRelayList() { try { val defaultRelays = nostr.getDefaultMsgRelayList()