move msg relay sheet to home screen

This commit is contained in:
2026-06-10 15:26:20 +07:00
parent a759ad48e4
commit f7d2866517
5 changed files with 97 additions and 122 deletions

View File

@@ -6,23 +6,11 @@ import android.os.Build
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.isSystemInDarkTheme 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.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialExpressiveTheme import androidx.compose.material3.MaterialExpressiveTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.MotionScheme import androidx.compose.material3.MotionScheme
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.Typography import androidx.compose.material3.Typography
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicDarkColorScheme
@@ -38,13 +26,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf 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.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.core.util.Consumer
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
@@ -54,7 +36,6 @@ import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay import androidx.navigation3.ui.NavDisplay
import kotlinx.coroutines.launch
import su.reya.coop.screens.ChatScreen import su.reya.coop.screens.ChatScreen
import su.reya.coop.screens.HomeScreen import su.reya.coop.screens.HomeScreen
import su.reya.coop.screens.ImportScreen import su.reya.coop.screens.ImportScreen
@@ -95,7 +76,6 @@ fun App(viewModel: NostrViewModel) {
val qrScanResult = remember { QrScanResult() } val qrScanResult = remember { QrScanResult() }
val signerRequired by viewModel.signerRequired.collectAsStateWithLifecycle() val signerRequired by viewModel.signerRequired.collectAsStateWithLifecycle()
val isRelayListEmpty by viewModel.isRelayListEmpty.collectAsStateWithLifecycle()
// Snackbar // Snackbar
val snackbarHostState = remember { SnackbarHostState() } 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,
)
}
}
}
}
} }
} }
} }

View File

@@ -55,7 +55,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coop.composeapp.generated.resources.Res import coop.composeapp.generated.resources.Res
import coop.composeapp.generated.resources.ic_arrow_back import coop.composeapp.generated.resources.ic_arrow_back
import coop.composeapp.generated.resources.ic_send import coop.composeapp.generated.resources.ic_send
import kotlinx.coroutines.flow.first
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
import rust.nostr.sdk.UnsignedEvent import rust.nostr.sdk.UnsignedEvent
import su.reya.coop.LocalNavigator 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.Avatar
import su.reya.coop.shared.displayNameFlow import su.reya.coop.shared.displayNameFlow
import su.reya.coop.shared.pictureFlow import su.reya.coop.shared.pictureFlow
import su.reya.coop.short
@Composable @Composable
fun ChatScreen(id: Long) { fun ChatScreen(id: Long) {
@@ -77,9 +75,7 @@ fun ChatScreen(id: Long) {
// Get chat room by ID // Get chat room by ID
val chatRooms by viewModel.chatRooms.collectAsStateWithLifecycle() val chatRooms by viewModel.chatRooms.collectAsStateWithLifecycle()
val room by remember(id) { val room by remember(id) { derivedStateOf { chatRooms.firstOrNull { it.id == id } } }
derivedStateOf { chatRooms.firstOrNull { it.id == id } }
}
// Show empty screen // Show empty screen
if (room == null) { if (room == null) {
@@ -88,7 +84,7 @@ fun ChatScreen(id: Long) {
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
text = "Chat room not found", text = "Something went wrong.",
style = MaterialTheme.typography.titleMediumEmphasized, style = MaterialTheme.typography.titleMediumEmphasized,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface
) )
@@ -114,23 +110,14 @@ fun ChatScreen(id: Long) {
// Start loading spinner // Start loading spinner
loading = true loading = true
// Get msg relays for each member
viewModel.chatRoomConnect(id)
// Get messages // Get messages
val initialMessages = viewModel.getChatRoomMessages(id) val initialMessages = viewModel.getChatRoomMessages(id)
messages.clear() messages.clear()
messages.addAll(initialMessages) 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 // Stop loading spinner
loading = false loading = false

View File

@@ -13,8 +13,10 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
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.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding 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.ClipEntry
import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext 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.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.LifecycleResumeEffect
@@ -111,6 +115,7 @@ fun HomeScreen() {
val userProfile by currentUserProfile.collectAsStateWithLifecycle() val userProfile by currentUserProfile.collectAsStateWithLifecycle()
val chatRooms by viewModel.chatRooms.collectAsStateWithLifecycle() val chatRooms by viewModel.chatRooms.collectAsStateWithLifecycle()
val isRelayListEmpty by viewModel.isRelayListEmpty.collectAsStateWithLifecycle()
val isPartialProcessedGiftWrap by viewModel.isPartialProcessedGiftWrap.collectAsState(initial = false) val isPartialProcessedGiftWrap by viewModel.isPartialProcessedGiftWrap.collectAsState(initial = false)
val isBannerDismissed by viewModel.isNotificationBannerDismissed.collectAsState() 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) @OptIn(ExperimentalMaterial3ExpressiveApi::class)

View File

@@ -453,9 +453,10 @@ class Nostr {
client?.addRelay( client?.addRelay(
url = relay, url = relay,
capabilities = capabilities =
if (metadata == RelayMetadata.READ) RelayCapabilities.read() when (metadata) {
else if (metadata == RelayMetadata.WRITE) RelayCapabilities.write() RelayMetadata.READ -> RelayCapabilities.read()
else RelayCapabilities.none() RelayMetadata.WRITE -> RelayCapabilities.write()
}
) )
client?.connectRelay(relay) client?.connectRelay(relay)
} }
@@ -466,7 +467,7 @@ class Nostr {
suspend fun getDefaultMsgRelayList(): List<RelayUrl> { suspend fun getDefaultMsgRelayList(): List<RelayUrl> {
// Construct a list of messaging relays // Construct a list of messaging relays
val msgRelayList = listOf( val msgRelayList = listOf(
RelayUrl.parse("wss://relay.0xchat.com"), RelayUrl.parse("wss://auth.nostr1.com"),
RelayUrl.parse("wss://nip17.com"), RelayUrl.parse("wss://nip17.com"),
) )
@@ -721,33 +722,25 @@ class Nostr {
} }
} }
suspend fun chatRoomConnect(members: List<PublicKey>): Map<PublicKey, List<RelayUrl>> { suspend fun chatRoomConnect(members: List<PublicKey>) {
try { try {
val results = mutableMapOf<PublicKey, MutableList<RelayUrl>>()
members.forEach { member -> members.forEach { member ->
results[member] = mutableListOf<RelayUrl>()
val kind = Kind.fromStd(KindStandard.INBOX_RELAYS) val kind = Kind.fromStd(KindStandard.INBOX_RELAYS)
val filter = Filter().kind(kind).author(member).limit(1u) val filter = Filter().kind(kind).author(member).limit(1u)
val stream = client?.streamEvents( val stream = client?.streamEvents(
target = ReqTarget.auto(listOf(filter)), target = ReqTarget.auto(listOf(filter)),
id = "room-${member.toBech32().substring(0, 10)}", id = null,
timeout = Duration.parse("3s"), timeout = Duration.parse("3s"),
policy = ReqExitPolicy.ExitOnEose policy = ReqExitPolicy.ExitOnEose
) )
stream?.next()?.let { res -> stream?.next()?.let { res ->
if (res.event != null) { if (res.event != null) {
// Connect to the msg relays
connectMsgRelays(res.event!!) connectMsgRelays(res.event!!)
// Mark the member as connected
results[member]?.add(res.relayUrl)
} }
} }
} }
return results
} catch (e: Exception) { } catch (e: Exception) {
throw IllegalStateException("Failed to fetch relays: ${e.message}", e) throw IllegalStateException("Failed to fetch relays: ${e.message}", e)
} }
@@ -757,10 +750,8 @@ class Nostr {
try { try {
val urls = nip17ExtractRelayList(event); val urls = nip17ExtractRelayList(event);
for (url in urls) { for (url in urls) {
if (client?.relay(url) == null) { client?.addRelay(url, RelayCapabilities.gossip())
client?.addRelay(url) client?.connectRelay(url)
client?.connectRelay(url)
}
} }
} catch (e: Exception) { } catch (e: Exception) {
throw IllegalStateException("Failed to connect to relays: ${e.message}", e) throw IllegalStateException("Failed to connect to relays: ${e.message}", e)

View File

@@ -644,20 +644,16 @@ class NostrViewModel(
return emptyList() return emptyList()
} }
suspend fun chatRoomConnect(roomId: Long): Map<PublicKey, List<RelayUrl>> { fun chatRoomConnect(roomId: Long) {
try { viewModelScope.launch {
val room = getChatRoom(roomId) ?: throw IllegalArgumentException("Room not found") try {
val members = room.members val room = getChatRoom(roomId) ?: throw IllegalArgumentException("Room not found")
val members = room.members
return runCatching {
nostr.chatRoomConnect(members.toList()) nostr.chatRoomConnect(members.toList())
}.getOrElse { e -> } catch (e: Exception) {
showError("Error: ${e.message}") showError("Error: ${e.message}")
members.associateWith { emptyList() }
} }
} catch (e: Exception) {
showError("Error: ${e.message}")
return emptyMap()
} }
} }