update new chat screen
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M647,520L160,520L160,440L647,440L423,216L480,160L800,480L480,800L423,744L647,520Z" />
|
||||
</vector>
|
||||
@@ -1,10 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M256,760L200,704L424,480L200,256L256,200L480,424L704,200L760,256L536,480L760,704L704,760L480,536L256,760Z" />
|
||||
</vector>
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M336,680L280,624L424,480L280,337L336,281L480,425L623,281L679,337L535,480L679,624L623,680L480,536L336,680Z" />
|
||||
</vector>
|
||||
|
||||
@@ -3,29 +3,36 @@ 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.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.InputChip
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.PlainTooltip
|
||||
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.TooltipAnchorPosition
|
||||
import androidx.compose.material3.TooltipBox
|
||||
import androidx.compose.material3.TooltipDefaults
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTooltipState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -37,9 +44,11 @@ import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coop.composeapp.generated.resources.Res
|
||||
import coop.composeapp.generated.resources.ic_arrow_back
|
||||
import coop.composeapp.generated.resources.ic_arrow_next
|
||||
import coop.composeapp.generated.resources.ic_close_small
|
||||
import coop.composeapp.generated.resources.ic_scanner
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -60,7 +69,10 @@ fun NewChatScreen(
|
||||
val snackbarHostState = LocalSnackbarHostState.current
|
||||
val navController = LocalNavController.current
|
||||
val viewModel = LocalNostrViewModel.current
|
||||
val contactList by viewModel.contactList.collectAsState(initial = emptySet())
|
||||
|
||||
val createGroup = remember { mutableStateOf(false) }
|
||||
val searchResults = remember { mutableStateListOf<PublicKey>() }
|
||||
val selectedReceivers = remember { mutableStateListOf<PublicKey>() }
|
||||
var query by remember { mutableStateOf("") }
|
||||
|
||||
@@ -71,7 +83,28 @@ fun NewChatScreen(
|
||||
LaunchedEffect(query) {
|
||||
if (query.length >= 3) {
|
||||
delay(500) // 500ms debounce
|
||||
// TODO: Implement search
|
||||
|
||||
if (query.startsWith("npub1")) {
|
||||
val pubkey = try {
|
||||
PublicKey.parse(query)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
if (pubkey != null) {
|
||||
selectedReceivers.add(pubkey)
|
||||
}
|
||||
} else if (query.contains("@")) {
|
||||
val pubkey = viewModel.searchByAddress(query)
|
||||
if (pubkey != null) {
|
||||
selectedReceivers.add(pubkey)
|
||||
}
|
||||
} else {
|
||||
val results = viewModel.searchByNostr(query)
|
||||
searchResults.clear()
|
||||
searchResults.addAll(results)
|
||||
}
|
||||
|
||||
query = ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +120,12 @@ fun NewChatScreen(
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("New Chat") },
|
||||
title = {
|
||||
Text(
|
||||
text = if (createGroup.value) "New group chat" else "New chat",
|
||||
style = MaterialTheme.typography.titleMediumEmphasized
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
),
|
||||
@@ -109,6 +147,37 @@ fun NewChatScreen(
|
||||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
if (selectedReceivers.isNotEmpty()) {
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(
|
||||
TooltipAnchorPosition.Above,
|
||||
spacingBetweenTooltipAndAnchor = 8.dp,
|
||||
),
|
||||
tooltip = {
|
||||
PlainTooltip { Text("Next") }
|
||||
},
|
||||
state = rememberTooltipState(),
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = {
|
||||
val roomId = viewModel.createChatRoom(selectedReceivers.toList())
|
||||
navController.navigate(Screen.Chat(roomId))
|
||||
},
|
||||
expanded = false,
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(Res.drawable.ic_arrow_next),
|
||||
contentDescription = "Next"
|
||||
)
|
||||
},
|
||||
text = { Text("Next") },
|
||||
containerColor = MaterialTheme.colorScheme.tertiary,
|
||||
contentColor = MaterialTheme.colorScheme.onTertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
content = { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -119,21 +188,27 @@ fun NewChatScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
) {
|
||||
FlowRow(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.Top,
|
||||
) {
|
||||
Text(
|
||||
text = "To:",
|
||||
modifier = Modifier.align(Alignment.Top),
|
||||
style = MaterialTheme.typography.labelMediumEmphasized,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
selectedReceivers.forEach { receiver ->
|
||||
ReceiverChip(
|
||||
pubkey = receiver,
|
||||
@@ -143,16 +218,14 @@ fun NewChatScreen(
|
||||
BasicTextField(
|
||||
value = query,
|
||||
onValueChange = { query = it },
|
||||
modifier = Modifier
|
||||
.widthIn(min = 50.dp)
|
||||
.align(Alignment.CenterVertically),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textStyle = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
),
|
||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||
decorationBox = { innerTextField ->
|
||||
Box(contentAlignment = Alignment.CenterStart) {
|
||||
if (query.isEmpty() && selectedReceivers.isEmpty()) {
|
||||
if (query.isEmpty()) {
|
||||
Text(
|
||||
"Type a npub or address",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
@@ -167,14 +240,27 @@ fun NewChatScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
) {
|
||||
// TODO: add result list
|
||||
if (searchResults.isNotEmpty()) {
|
||||
ContactList(
|
||||
items = searchResults,
|
||||
selectedReceivers = selectedReceivers,
|
||||
onContactClick = { pubkey ->
|
||||
val roomId = viewModel.createChatRoom(listOf(pubkey))
|
||||
navController.navigate(Screen.Chat(roomId))
|
||||
},
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
} else {
|
||||
ContactList(
|
||||
title = "Contacts",
|
||||
items = contactList.toList(),
|
||||
selectedReceivers = selectedReceivers,
|
||||
onContactClick = { pubkey ->
|
||||
val roomId = viewModel.createChatRoom(listOf(pubkey))
|
||||
@@ -185,6 +271,7 @@ fun NewChatScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -201,10 +288,18 @@ fun ReceiverChip(
|
||||
val displayName = profile?.name ?: profile?.displayName ?: pubkey.short()
|
||||
val picture = profile?.picture
|
||||
|
||||
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
|
||||
InputChip(
|
||||
selected = true,
|
||||
onClick = onRemove,
|
||||
label = { Text(displayName) },
|
||||
label = {
|
||||
Text(
|
||||
text = displayName,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
)
|
||||
},
|
||||
avatar = {
|
||||
Avatar(
|
||||
picture = picture,
|
||||
@@ -217,33 +312,38 @@ fun ReceiverChip(
|
||||
painter = painterResource(Res.drawable.ic_close_small),
|
||||
contentDescription = "Close"
|
||||
)
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun ContactList(
|
||||
title: String? = null,
|
||||
items: List<PublicKey>,
|
||||
selectedReceivers: SnapshotStateList<PublicKey>,
|
||||
onContactClick: (PublicKey) -> Unit
|
||||
) {
|
||||
val viewModel = LocalNostrViewModel.current
|
||||
val contactList by viewModel.contactList.collectAsState(initial = emptySet())
|
||||
if (items.isEmpty()) return
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap),
|
||||
) {
|
||||
if (title != null) {
|
||||
Text(
|
||||
text = "Contacts",
|
||||
style = MaterialTheme.typography.titleLargeEmphasized,
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMediumEmphasized,
|
||||
)
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
contactList.forEachIndexed { index, item ->
|
||||
}
|
||||
items.forEachIndexed { index, item ->
|
||||
ContactListItem(
|
||||
pubkey = item,
|
||||
index = index,
|
||||
total = contactList.size,
|
||||
total = items.size,
|
||||
isSelected = selectedReceivers.contains(item),
|
||||
onClick = { onContactClick(item) },
|
||||
onLongClick = {
|
||||
|
||||
@@ -89,6 +89,7 @@ class Nostr {
|
||||
// Bootstrap relays
|
||||
client?.addRelay(RelayUrl.parse("wss://relay.primal.net"))
|
||||
client?.addRelay(RelayUrl.parse("wss://user.kindpag.es"))
|
||||
client?.addRelay(RelayUrl.parse("wss://purplepag.es"))
|
||||
|
||||
// Add search relay
|
||||
client?.addRelay(
|
||||
@@ -638,18 +639,18 @@ class Nostr {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun searchByAddress(query: String): List<PublicKey> {
|
||||
suspend fun searchByAddress(query: String): PublicKey {
|
||||
try {
|
||||
val address = Nip05Address.parse(query)
|
||||
val profile = profileFromAddress(HttpClient(), address)
|
||||
|
||||
return listOf(profile.publicKey())
|
||||
return profile.publicKey()
|
||||
} catch (e: Exception) {
|
||||
throw IllegalStateException("Failed to search address: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun searchByNostr(query: String) {
|
||||
suspend fun searchByNostr(query: String): List<PublicKey> {
|
||||
try {
|
||||
val kinds = listOf(Kind.fromStd(KindStandard.METADATA))
|
||||
val filter = Filter().kinds(kinds).search(query).limit(10u)
|
||||
@@ -672,6 +673,8 @@ class Nostr {
|
||||
results.add(event.event!!.author())
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
} catch (e: Exception) {
|
||||
throw IllegalStateException("Failed to search nostr: ${e.message}", e)
|
||||
}
|
||||
|
||||
@@ -380,6 +380,24 @@ class NostrViewModel(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun searchByAddress(query: String): PublicKey? {
|
||||
try {
|
||||
return nostr.searchByAddress(query)
|
||||
} catch (e: Exception) {
|
||||
showError("Error: ${e.message}")
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun searchByNostr(query: String): List<PublicKey> {
|
||||
try {
|
||||
return nostr.searchByNostr(query)
|
||||
} catch (e: Exception) {
|
||||
showError("Error: ${e.message}")
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun PublicKey.short(): String {
|
||||
|
||||
Reference in New Issue
Block a user