add relay screen
This commit is contained in:
@@ -52,6 +52,7 @@ import su.reya.coop.screens.MyQrScreen
|
|||||||
import su.reya.coop.screens.NewChatScreen
|
import su.reya.coop.screens.NewChatScreen
|
||||||
import su.reya.coop.screens.NewIdentityScreen
|
import su.reya.coop.screens.NewIdentityScreen
|
||||||
import su.reya.coop.screens.OnboardingScreen
|
import su.reya.coop.screens.OnboardingScreen
|
||||||
|
import su.reya.coop.screens.RelayScreen
|
||||||
import su.reya.coop.screens.ScanScreen
|
import su.reya.coop.screens.ScanScreen
|
||||||
|
|
||||||
val LocalNostrViewModel = staticCompositionLocalOf<NostrViewModel> {
|
val LocalNostrViewModel = staticCompositionLocalOf<NostrViewModel> {
|
||||||
@@ -124,7 +125,7 @@ fun App() {
|
|||||||
// Show the relay setup dialog if the msg relay list is empty
|
// Show the relay setup dialog if the msg relay list is empty
|
||||||
if (isRelayListEmpty) {
|
if (isRelayListEmpty) {
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = { },
|
onDismissRequest = { viewModel.dismissRelayWarning() },
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
) {
|
) {
|
||||||
@@ -242,6 +243,11 @@ fun App() {
|
|||||||
onBack = { navController.popBackStack() },
|
onBack = { navController.popBackStack() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
composable<Screen.Relay> { backStackEntry ->
|
||||||
|
RelayScreen(
|
||||||
|
onBack = { navController.popBackStack() },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,4 +26,7 @@ sealed interface Screen {
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object MyQr : Screen
|
data object MyQr : Screen
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object Relay : Screen
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package su.reya.coop.screens
|
package su.reya.coop.screens
|
||||||
|
|
||||||
import android.content.ClipData
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -60,7 +59,6 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.platform.LocalClipboard
|
import androidx.compose.ui.platform.LocalClipboard
|
||||||
import androidx.compose.ui.platform.toClipEntry
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coop.composeapp.generated.resources.Res
|
import coop.composeapp.generated.resources.Res
|
||||||
@@ -269,11 +267,20 @@ fun HomeScreen(
|
|||||||
) {
|
) {
|
||||||
val pubkey = viewModel.currentUser()
|
val pubkey = viewModel.currentUser()
|
||||||
val shortPubkey = pubkey?.short() ?: "Not available"
|
val shortPubkey = pubkey?.short() ?: "Not available"
|
||||||
|
|
||||||
val userName =
|
val userName =
|
||||||
userProfile?.asRecord()?.displayName
|
userProfile?.asRecord()?.displayName
|
||||||
?: userProfile?.asRecord()?.name
|
?: userProfile?.asRecord()?.name
|
||||||
?: "No name"
|
?: "No name"
|
||||||
|
|
||||||
|
val dismissAndRun: (suspend () -> Unit) -> Unit = { action ->
|
||||||
|
scope.launch {
|
||||||
|
sheetState.hide()
|
||||||
|
showBottomSheet = false
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
@@ -311,13 +318,7 @@ fun HomeScreen(
|
|||||||
) {
|
) {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
dismissAndRun { navController.navigate(Screen.MyQr) }
|
||||||
if (pubkey != null) {
|
|
||||||
val text = pubkey.toBech32();
|
|
||||||
val entry = ClipData.newPlainText("text", text)
|
|
||||||
clipboard.setClipEntry(entry.toClipEntry())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Text(text = shortPubkey)
|
Text(text = shortPubkey)
|
||||||
@@ -340,7 +341,7 @@ fun HomeScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.size(16.dp))
|
Spacer(modifier = Modifier.size(16.dp))
|
||||||
BottomMenuList()
|
BottomMenuList(onDismiss = dismissAndRun)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -394,15 +395,17 @@ fun ChatRoom(room: Room, onClick: () -> Unit) {
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun BottomMenuList() {
|
fun BottomMenuList(
|
||||||
|
onDismiss: (suspend () -> Unit) -> Unit
|
||||||
|
) {
|
||||||
|
val navController = LocalNavController.current
|
||||||
val viewModel = LocalNostrViewModel.current
|
val viewModel = LocalNostrViewModel.current
|
||||||
|
|
||||||
val defaultMenuList = listOf(
|
val defaultMenuList = listOf(
|
||||||
"Messaging Relays" to { },
|
"Relay Management" to { navController.navigate(Screen.Relay) },
|
||||||
"Spam Filter" to { },
|
"Spams & Blocks" to { },
|
||||||
"Contacts" to { },
|
"Contacts" to { },
|
||||||
"Settings" to { },
|
"Settings" to { }
|
||||||
"About" to { }
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
@@ -415,7 +418,7 @@ fun BottomMenuList() {
|
|||||||
) {
|
) {
|
||||||
defaultMenuList.forEachIndexed { index, (title, action) ->
|
defaultMenuList.forEachIndexed { index, (title, action) ->
|
||||||
SegmentedListItem(
|
SegmentedListItem(
|
||||||
onClick = { action() },
|
onClick = { onDismiss { action() } },
|
||||||
shapes = ListItemDefaults.segmentedShapes(
|
shapes = ListItemDefaults.segmentedShapes(
|
||||||
index = index,
|
index = index,
|
||||||
count = defaultMenuList.size
|
count = defaultMenuList.size
|
||||||
|
|||||||
@@ -0,0 +1,236 @@
|
|||||||
|
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.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.ListItemDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SegmentedListItem
|
||||||
|
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.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
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 rust.nostr.sdk.RelayMetadata
|
||||||
|
import rust.nostr.sdk.RelayUrl
|
||||||
|
import su.reya.coop.LocalNostrViewModel
|
||||||
|
import su.reya.coop.LocalSnackbarHostState
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun RelayScreen(
|
||||||
|
onBack: () -> Unit
|
||||||
|
) {
|
||||||
|
val snackbarHostState = LocalSnackbarHostState.current
|
||||||
|
val viewModel = LocalNostrViewModel.current
|
||||||
|
|
||||||
|
val msgRelayList = remember { mutableStateListOf<RelayUrl>() }
|
||||||
|
val relayList = remember { mutableStateMapOf<RelayUrl, RelayMetadata?>() }
|
||||||
|
|
||||||
|
val inboxRelays by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
relayList.filter { it.value == RelayMetadata.READ || it.value == null }.keys.toList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val outboxRelays by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
relayList.filter { it.value == RelayMetadata.WRITE || it.value == null }.keys.toList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
relayList.putAll(viewModel.currentUserRelayList())
|
||||||
|
msgRelayList.addAll(viewModel.currentUserMsgRelayList())
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
),
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = "Relay Management",
|
||||||
|
style = MaterialTheme.typography.titleMediumEmphasized
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(Res.drawable.ic_arrow_back),
|
||||||
|
contentDescription = "Back"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
content = { innerPadding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(innerPadding)
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Messaging Relay List",
|
||||||
|
style = MaterialTheme.typography.titleMediumEmphasized,
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap),
|
||||||
|
) {
|
||||||
|
if (msgRelayList.isNotEmpty()) {
|
||||||
|
msgRelayList.forEachIndexed { index, relayUrl ->
|
||||||
|
SegmentedListItem(
|
||||||
|
onClick = { },
|
||||||
|
shapes = ListItemDefaults.segmentedShapes(
|
||||||
|
index = index,
|
||||||
|
count = msgRelayList.size
|
||||||
|
),
|
||||||
|
content = { Text(text = relayUrl.toString()) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Surface(
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "No relays configured",
|
||||||
|
style = MaterialTheme.typography.labelMediumEmphasized,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Inbox Relays",
|
||||||
|
style = MaterialTheme.typography.titleMediumEmphasized,
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap),
|
||||||
|
) {
|
||||||
|
if (inboxRelays.isNotEmpty()) {
|
||||||
|
inboxRelays.forEachIndexed { index, relayUrl ->
|
||||||
|
SegmentedListItem(
|
||||||
|
onClick = { },
|
||||||
|
shapes = ListItemDefaults.segmentedShapes(
|
||||||
|
index = index,
|
||||||
|
count = inboxRelays.size
|
||||||
|
),
|
||||||
|
content = { Text(text = relayUrl.toString()) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Surface(
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "No relays configured",
|
||||||
|
style = MaterialTheme.typography.labelMediumEmphasized,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Outbox Relays",
|
||||||
|
style = MaterialTheme.typography.titleMediumEmphasized,
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap),
|
||||||
|
) {
|
||||||
|
if (outboxRelays.isNotEmpty()) {
|
||||||
|
outboxRelays.forEachIndexed { index, relayUrl ->
|
||||||
|
SegmentedListItem(
|
||||||
|
onClick = { },
|
||||||
|
shapes = ListItemDefaults.segmentedShapes(
|
||||||
|
index = index,
|
||||||
|
count = outboxRelays.size
|
||||||
|
),
|
||||||
|
content = { Text(text = relayUrl.toString()) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Surface(
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "No relays configured",
|
||||||
|
style = MaterialTheme.typography.labelMediumEmphasized,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -51,6 +51,7 @@ import rust.nostr.sdk.TagKind
|
|||||||
import rust.nostr.sdk.Timestamp
|
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.extractRelayList
|
||||||
import rust.nostr.sdk.giftWrapAsync
|
import rust.nostr.sdk.giftWrapAsync
|
||||||
import rust.nostr.sdk.initLogger
|
import rust.nostr.sdk.initLogger
|
||||||
import rust.nostr.sdk.nip17ExtractRelayList
|
import rust.nostr.sdk.nip17ExtractRelayList
|
||||||
@@ -611,6 +612,18 @@ class Nostr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getRelayList(publicKey: PublicKey): Map<RelayUrl, RelayMetadata?> {
|
||||||
|
try {
|
||||||
|
val kind = Kind.fromStd(KindStandard.RELAY_LIST)
|
||||||
|
val filter = Filter().kind(kind).author(publicKey).limit(1u)
|
||||||
|
val events = client?.database()?.query(filter)
|
||||||
|
|
||||||
|
return extractRelayList(events?.toVec()?.firstOrNull() ?: return emptyMap())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw IllegalStateException("Failed to get relay list: ${e.message}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getChatRooms(): Set<Room>? {
|
suspend fun getChatRooms(): Set<Room>? {
|
||||||
try {
|
try {
|
||||||
val userPubkey = signer.currentUser ?: throw IllegalStateException("User not signed in")
|
val userPubkey = signer.currentUser ?: throw IllegalStateException("User not signed in")
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import rust.nostr.sdk.Metadata
|
|||||||
import rust.nostr.sdk.NostrConnect
|
import rust.nostr.sdk.NostrConnect
|
||||||
import rust.nostr.sdk.NostrConnectUri
|
import rust.nostr.sdk.NostrConnectUri
|
||||||
import rust.nostr.sdk.PublicKey
|
import rust.nostr.sdk.PublicKey
|
||||||
|
import rust.nostr.sdk.RelayMetadata
|
||||||
import rust.nostr.sdk.RelayUrl
|
import rust.nostr.sdk.RelayUrl
|
||||||
import rust.nostr.sdk.Tag
|
import rust.nostr.sdk.Tag
|
||||||
import rust.nostr.sdk.UnsignedEvent
|
import rust.nostr.sdk.UnsignedEvent
|
||||||
@@ -386,6 +387,24 @@ class NostrViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun currentUserRelayList(): Map<RelayUrl, RelayMetadata?> {
|
||||||
|
try {
|
||||||
|
return nostr.getRelayList(nostr.signer.currentUser!!)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Error: ${e.message}")
|
||||||
|
return emptyMap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun currentUserMsgRelayList(): List<RelayUrl> {
|
||||||
|
try {
|
||||||
|
return nostr.getMsgRelays(nostr.signer.currentUser!!)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Error: ${e.message}")
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun createChatRoom(to: List<PublicKey>): Long {
|
fun createChatRoom(to: List<PublicKey>): Long {
|
||||||
if (nostr.signer.currentUser == null) throw IllegalStateException("User not signed in")
|
if (nostr.signer.currentUser == null) throw IllegalStateException("User not signed in")
|
||||||
if (to.isEmpty()) throw IllegalArgumentException("At least one recipient is required")
|
if (to.isEmpty()) throw IllegalArgumentException("At least one recipient is required")
|
||||||
|
|||||||
Reference in New Issue
Block a user