chore: confirm before delete contact #35

Merged
reya merged 2 commits from improve-contact-screen into master 2026-07-03 09:09:58 +00:00
2 changed files with 80 additions and 37 deletions

View File

@@ -3,11 +3,15 @@ package su.reya.coop.screens
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
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.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -20,6 +24,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.SegmentedListItem import androidx.compose.material3.SegmentedListItem
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TooltipAnchorPosition import androidx.compose.material3.TooltipAnchorPosition
import androidx.compose.material3.TooltipBox import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults import androidx.compose.material3.TooltipDefaults
@@ -54,6 +59,7 @@ import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
import rust.nostr.sdk.Nip05Address import rust.nostr.sdk.Nip05Address
import rust.nostr.sdk.PublicKey import rust.nostr.sdk.PublicKey
import su.reya.coop.LocalChatViewModel
import su.reya.coop.LocalNavigator import su.reya.coop.LocalNavigator
import su.reya.coop.LocalNostrViewModel import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalSnackbarHostState import su.reya.coop.LocalSnackbarHostState
@@ -67,9 +73,11 @@ fun ContactListScreen() {
val navigator = LocalNavigator.current val navigator = LocalNavigator.current
val snackbarHostState = LocalSnackbarHostState.current val snackbarHostState = LocalSnackbarHostState.current
val nostrViewModel = LocalNostrViewModel.current val nostrViewModel = LocalNostrViewModel.current
val chatViewModel = LocalChatViewModel.current
val contactList by nostrViewModel.contactList.collectAsStateWithLifecycle() val contactList by nostrViewModel.contactList.collectAsStateWithLifecycle()
var openAddContactDialog by remember { mutableStateOf(false) } var openAddContactDialog by remember { mutableStateOf(false) }
var contactToDelete by remember { mutableStateOf<PublicKey?>(null) }
Scaffold( Scaffold(
containerColor = MaterialTheme.colorScheme.surfaceContainer, containerColor = MaterialTheme.colorScheme.surfaceContainer,
@@ -128,43 +136,54 @@ fun ContactListScreen() {
} }
}, },
content = { innerPadding -> content = { innerPadding ->
Column( if (contactList.isNotEmpty()) {
modifier = Modifier val contacts = remember(contactList) { contactList.toList() }
.fillMaxWidth()
.padding(16.dp) LazyColumn(
.padding(innerPadding), modifier = Modifier
verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap), .fillMaxSize()
) { .padding(top = innerPadding.calculateTopPadding()),
if (contactList.isNotEmpty()) { contentPadding = PaddingValues(16.dp),
contactList.forEachIndexed { index, pubkey -> verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap),
) {
itemsIndexed(contacts) { index, pubkey ->
ContactListItem( ContactListItem(
pubkey = pubkey, pubkey = pubkey,
index = index, index = index,
total = contactList.size, total = contacts.size,
onClick = {}) onClick = {
val room = chatViewModel.createChatRoom(listOf(pubkey))
navigator.navigate(Screen.Chat(room))
},
onLongClick = {
contactToDelete = pubkey
}
)
} }
} else { }
Box( } else {
modifier = Modifier.fillMaxSize(), Box(
contentAlignment = Alignment.Center modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp),
) { ) {
Column( Text(
horizontalAlignment = Alignment.CenterHorizontally, text = "No contacts yet",
verticalArrangement = Arrangement.spacedBy(4.dp), style = MaterialTheme.typography.titleLargeEmphasized.copy(
) { fontWeight = FontWeight.SemiBold
Text( ),
text = "No contacts yet", color = MaterialTheme.colorScheme.onSurface
style = MaterialTheme.typography.titleLargeEmphasized.copy( )
fontWeight = FontWeight.SemiBold Text(
), text = "Your contacts will appear here",
color = MaterialTheme.colorScheme.onSurface style = MaterialTheme.typography.bodyMedium,
) color = MaterialTheme.colorScheme.outline
Text( )
text = "Your contacts will appear here",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline
)
}
} }
} }
} }
@@ -174,6 +193,29 @@ fun ContactListScreen() {
if (openAddContactDialog) { if (openAddContactDialog) {
AddContactDialog(onDismissRequest = { openAddContactDialog = false }) AddContactDialog(onDismissRequest = { openAddContactDialog = false })
} }
if (contactToDelete != null) {
AlertDialog(
onDismissRequest = { contactToDelete = null },
title = { Text("Delete Contact") },
text = { Text("Are you sure you want to remove this contact?") },
confirmButton = {
TextButton(
onClick = {
contactToDelete?.let { nostrViewModel.removeContact(it) }
contactToDelete = null
}
) {
Text("Delete")
}
},
dismissButton = {
TextButton(onClick = { contactToDelete = null }) {
Text("Cancel")
}
}
)
}
} }
@OptIn(ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@@ -282,6 +324,7 @@ fun ContactListItem(
index: Int, index: Int,
total: Int = 0, total: Int = 0,
onClick: () -> Unit, onClick: () -> Unit,
onLongClick: () -> Unit,
) { ) {
val nostrViewModel = LocalNostrViewModel.current val nostrViewModel = LocalNostrViewModel.current
val profileFlow = remember(pubkey) { nostrViewModel.getMetadata(pubkey) } val profileFlow = remember(pubkey) { nostrViewModel.getMetadata(pubkey) }
@@ -289,7 +332,7 @@ fun ContactListItem(
SegmentedListItem( SegmentedListItem(
onClick = onClick, onClick = onClick,
onLongClick = { nostrViewModel.removeContact(pubkey) }, onLongClick = onLongClick,
shapes = ListItemDefaults.segmentedShapes( shapes = ListItemDefaults.segmentedShapes(
index = index, index = index,
count = total count = total
@@ -301,11 +344,11 @@ fun ContactListItem(
size = 36.dp size = 36.dp
) )
}, },
supportingContent = { Text(text = pubkey.short()) }, supportingContent = { Text(pubkey.short()) },
content = { content = {
Text( Text(
text = profile?.name ?: "No name", text = profile?.name ?: "No name",
style = MaterialTheme.typography.titleMediumEmphasized, style = MaterialTheme.typography.titleMedium,
) )
} }
) )

View File

@@ -52,7 +52,7 @@ class MessageManager(private val nostr: Nostr) {
try { try {
val author = val author =
signer.getPublicKeyAsync() ?: throw IllegalStateException("User not signed in") signer.getPublicKeyAsync() ?: throw IllegalStateException("User not signed in")
val relays = nip17ExtractRelayList(msgRelayList) val relays = nip17ExtractRelayList(msgRelayList)
// Ensure relay connections // Ensure relay connections
@@ -130,7 +130,7 @@ class MessageManager(private val nostr: Nostr) {
val filter = Filter().identifier(giftId.toHex()) val filter = Filter().identifier(giftId.toHex())
val event = client?.database()?.query(filter)?.first() val event = client?.database()?.query(filter)?.first()
return event?.content()?.let { UnsignedEvent.fromJson(it) } return event?.content()?.let { UnsignedEvent.fromJson(it).ensureId() }
} catch (e: Throwable) { } catch (e: Throwable) {
throw IllegalStateException("Failed to get cached rumor: ${e.message}", e) throw IllegalStateException("Failed to get cached rumor: ${e.message}", e)
} }