From 3846ae2af5b62251a1381af966223a7908b2a4ba Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 18 Jan 2023 16:50:03 -0500 Subject: [PATCH] Search Bar --- .../amethyst/model/LocalCache.kt | 7 +- .../amethyst/ui/actions/NewPostView.kt | 74 +++----- .../amethyst/ui/note/ChatroomCompose.kt | 5 +- .../amethyst/ui/note/NoteCompose.kt | 2 +- .../amethyst/ui/note/UserCompose.kt | 2 +- .../{UserDisplay.kt => UsernameDisplay.kt} | 2 +- .../amethyst/ui/screen/ThreadFeedView.kt | 4 +- .../ui/screen/loggedIn/ChannelScreen.kt | 5 - .../ui/screen/loggedIn/ChatroomScreen.kt | 5 +- .../ui/screen/loggedIn/SearchScreen.kt | 176 ++++++++++++++++++ 10 files changed, 220 insertions(+), 62 deletions(-) rename app/src/main/java/com/vitorpamplona/amethyst/ui/note/{UserDisplay.kt => UsernameDisplay.kt} (96%) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index ced9cccc2..c205f426c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -24,6 +24,7 @@ import nostr.postr.events.PrivateDmEvent import nostr.postr.events.RecommendRelayEvent import nostr.postr.events.TextNoteEvent import nostr.postr.toHex +import nostr.postr.toNpub object LocalCache { @@ -309,7 +310,11 @@ object LocalCache { } fun findUsersStartingWith(username: String): List { - return users.values.filter { it.info.anyNameStartsWith(username) } + return users.values.filter { + it.info.anyNameStartsWith(username) + || it.pubkeyHex.startsWith(username, true) + || it.pubkey.toNpub().startsWith(username, true) + } } // Observers line up here. diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index 046fe32c1..96d6a2d86 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -1,24 +1,21 @@ package com.vitorpamplona.amethyst.ui.actions import androidx.compose.foundation.border -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row 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.width +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Divider +import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedTextField import androidx.compose.material.Surface @@ -37,16 +34,14 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.TextRange +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.input.TransformedText -import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage +import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.UrlPreview @@ -57,7 +52,7 @@ import com.vitorpamplona.amethyst.ui.components.noProtocolUrlValidator import com.vitorpamplona.amethyst.ui.components.videoExtension import com.vitorpamplona.amethyst.ui.navigation.UploadFromGallery import com.vitorpamplona.amethyst.ui.note.ReplyInformation -import com.vitorpamplona.amethyst.ui.note.UserDisplay +import com.vitorpamplona.amethyst.ui.screen.UserLine import kotlinx.coroutines.delay import nostr.postr.events.TextNoteEvent @@ -169,41 +164,8 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, account: Account ) ) { itemsIndexed(userSuggestions, key = { _, item -> item.pubkeyHex }) { index, item -> - Column(modifier = Modifier.fillMaxWidth().clickable(onClick = { + UserLine(item) { postViewModel.autocompleteWithUser(item) - })) { - Row( - modifier = Modifier - .padding( - start = 12.dp, - end = 12.dp, - top = 10.dp) - ) { - - AsyncImage( - model = item.profilePicture(), - contentDescription = "Profile Image", - modifier = Modifier - .width(55.dp).height(55.dp) - .clip(shape = CircleShape) - ) - - Column(modifier = Modifier.padding(start = 10.dp).weight(1f)) { - Row(verticalAlignment = Alignment.CenterVertically) { - UserDisplay(item) - } - - Text( - item.info.about?.take(100) ?: "", - color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) - ) - } - } - - Divider( - modifier = Modifier.padding(top = 10.dp), - thickness = 0.25.dp - ) } } } @@ -278,3 +240,27 @@ fun PostButton(onPost: () -> Unit = {}, isActive: Boolean, modifier: Modifier = Text(text = "Post", color = Color.White) } } + +@Composable +fun SearchButton(onPost: () -> Unit = {}, isActive: Boolean, modifier: Modifier = Modifier) { + Button( + modifier = modifier, + onClick = { + if (isActive) { + onPost() + } + }, + shape = RoundedCornerShape(20.dp), + colors = ButtonDefaults + .buttonColors( + backgroundColor = if (isActive) MaterialTheme.colors.primary else Color.Gray + ) + ) { + Icon( + painter = painterResource(R.drawable.ic_search), + null, + modifier = Modifier.size(26.dp), + tint = Color.White + ) + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt index c46eaa3f7..1cb39f345 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt @@ -23,10 +23,7 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavController import coil.compose.AsyncImage import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.components.RichTextViewer import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import nostr.postr.events.TextNoteEvent @Composable fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navController: NavController) { @@ -79,7 +76,7 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr userToComposeOn?.let { ChannelName( channelPicture = it.profilePicture(), - channelTitle = { UserDisplay(it) }, + channelTitle = { UsernameDisplay(it) }, channelLastTime = note.event?.createdAt, channelLastContent = accountViewModel.decrypt(note), onClick = { navController.navigate("Room/${it.pubkeyHex}") }) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index b0e662d0c..80f5f4558 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -110,7 +110,7 @@ fun NoteCompose(baseNote: Note, modifier: Modifier = Modifier, isInnerNote: Bool Column(modifier = Modifier.padding(start = if (!isInnerNote) 10.dp else 0.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { if (author != null) - UserDisplay(author) + UsernameDisplay(author) if (note.event !is RepostEvent) { Text( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt index 609ef53d4..ac8a7ad19 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt @@ -54,7 +54,7 @@ fun UserCompose(baseUser: User, accountViewModel: AccountViewModel, navControlle Column(modifier = Modifier.padding(start = 10.dp).weight(1f)) { Row(verticalAlignment = Alignment.CenterVertically) { - UserDisplay(user) + UsernameDisplay(user) } Text( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserDisplay.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt similarity index 96% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserDisplay.kt rename to app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt index 13bf498ff..c133aec8e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserDisplay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt @@ -7,7 +7,7 @@ import androidx.compose.ui.text.font.FontWeight import com.vitorpamplona.amethyst.model.User @Composable -fun UserDisplay(user: User) { +fun UsernameDisplay(user: User) { if (user.bestUsername() != null || user.bestDisplayName() != null) { if (user.bestDisplayName().isNullOrBlank()) { Text( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index 05638b069..31e88ebf9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -40,7 +40,7 @@ import com.vitorpamplona.amethyst.ui.components.RichTextViewer import com.vitorpamplona.amethyst.ui.note.BlankNote import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.ReactionsRowState -import com.vitorpamplona.amethyst.ui.note.UserDisplay +import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.timeAgoLong import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -185,7 +185,7 @@ fun NoteMaster(baseNote: Note, accountViewModel: AccountViewModel, navController Column(modifier = Modifier.padding(start = 10.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { if (author != null) - UserDisplay(author) + UsernameDisplay(author) } Row(verticalAlignment = Alignment.CenterVertically) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index 969b4140f..359687c19 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -1,6 +1,5 @@ package com.vitorpamplona.amethyst.ui.screen -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -13,7 +12,6 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.Divider import androidx.compose.material.MaterialTheme -import androidx.compose.material.OutlinedTextField import androidx.compose.material.Text import androidx.compose.material.TextField import androidx.compose.runtime.Composable @@ -35,11 +33,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import coil.compose.AsyncImage import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.NostrChannelDataSource -import com.vitorpamplona.amethyst.service.NostrChatRoomDataSource import com.vitorpamplona.amethyst.ui.actions.PostButton -import com.vitorpamplona.amethyst.ui.note.UserDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt index 6a47ffd46..369e6f475 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt @@ -13,7 +13,6 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.Divider import androidx.compose.material.MaterialTheme -import androidx.compose.material.OutlinedTextField import androidx.compose.material.Text import androidx.compose.material.TextField import androidx.compose.runtime.Composable @@ -34,7 +33,7 @@ import coil.compose.AsyncImage import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.NostrChatRoomDataSource import com.vitorpamplona.amethyst.ui.actions.PostButton -import com.vitorpamplona.amethyst.ui.note.UserDisplay +import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable @@ -126,7 +125,7 @@ fun ChatroomHeader(baseUser: User, accountViewModel: AccountViewModel, navContro Column(modifier = Modifier.padding(start = 10.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { if (author != null) - UserDisplay(author) + UsernameDisplay(author) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt index 1f1f91708..614137b48 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt @@ -1,15 +1,57 @@ package com.vitorpamplona.amethyst.ui.screen +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize 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.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.ClickableText +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.LocalTextStyle +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController +import coil.compose.AsyncImage +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.NostrGlobalDataSource +import com.vitorpamplona.amethyst.ui.actions.CloseButton +import com.vitorpamplona.amethyst.ui.actions.SearchButton +import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable @@ -24,7 +66,141 @@ fun SearchScreen(accountViewModel: AccountViewModel, navController: NavControlle Column( modifier = Modifier.padding(vertical = 0.dp) ) { + SearchBar(navController) FeedView(feedViewModel, accountViewModel, navController) } } } + +@Composable +private fun SearchBar(navController: NavController) { + val searchValue = remember { mutableStateOf(TextFieldValue("")) } + val searchResults = remember { mutableStateOf>(emptyList()) } + + val isTrailingIconVisible by remember { + derivedStateOf { + searchValue.value.text.isNotBlank() + } + } + + //LAST ROW + Row( + modifier = Modifier + .padding(10.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + TextField( + value = searchValue.value, + onValueChange = { + searchValue.value = it + searchResults.value = LocalCache.findUsersStartingWith(it.text) + }, + keyboardOptions = KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.Sentences + ), + leadingIcon = { + Icon( + painter = painterResource(R.drawable.ic_search), + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = Color.Unspecified + ) + }, + modifier = Modifier + .weight(1f, true) + .defaultMinSize(minHeight = 20.dp), + placeholder = { + Text( + text = "npub, hex, username ", + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + }, + trailingIcon = { + if (isTrailingIconVisible) { + IconButton( + onClick = { + searchValue.value = TextFieldValue("") + searchResults.value = emptyList() + } + ) { + Icon( + imageVector = Icons.Default.Clear, + contentDescription = "Clear" + ) + } + } + } + ) + } + + if (searchValue.value.text.isNotBlank()) { + LazyColumn( + modifier = Modifier.fillMaxHeight(), + contentPadding = PaddingValues( + top = 10.dp, + bottom = 10.dp + ) + ) { + itemsIndexed(searchResults.value, key = { _, item -> item.pubkeyHex }) { index, item -> + UserLine(item) { + navController.navigate("User/${item.pubkeyHex}") + } + } + } + } +} + +@Composable +fun UserLine( + baseUser: User, + onClick: () -> Unit +) { + val userState by baseUser.live.observeAsState() + val user = userState?.user ?: return + + Column(modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + ) { + Row( + modifier = Modifier + .padding( + start = 12.dp, + end = 12.dp, + top = 10.dp + ) + ) { + + AsyncImage( + model = user.profilePicture(), + contentDescription = "Profile Image", + modifier = Modifier + .width(55.dp) + .height(55.dp) + .clip(shape = CircleShape) + ) + + Column( + modifier = Modifier + .padding(start = 10.dp) + .weight(1f) + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + UsernameDisplay(user) + } + + Text( + user.info.about?.take(100) ?: "", + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + } + } + + Divider( + modifier = Modifier.padding(top = 10.dp), + thickness = 0.25.dp + ) + } +}