From 9d521805505dc0950335198a6d0a309f457cdbd3 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 19 Jan 2023 08:03:01 -0500 Subject: [PATCH] Join/Leave Channels, Channel search. --- .../vitorpamplona/amethyst/model/Account.kt | 11 ++- .../vitorpamplona/amethyst/model/Channel.kt | 5 ++ .../amethyst/model/LocalCache.kt | 22 +++++- .../com/vitorpamplona/amethyst/model/User.kt | 1 + .../ui/actions/NewChannelViewModel.kt | 6 +- .../amethyst/ui/note/ChatroomCompose.kt | 13 ++-- .../ui/screen/loggedIn/ChannelScreen.kt | 76 ++++++++++++++++--- .../ui/screen/loggedIn/SearchScreen.kt | 33 +++++++- 8 files changed, 146 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index fe88ddfe1..d832cfaf6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -169,7 +169,16 @@ class Account(val loggedIn: Persona, val followingChannels: MutableSet = Client.send(event) LocalCache.consume(event) - followingChannels.add(event.id.toHex()) + joinChannel(event.id.toHex(), accountStateViewModel) + } + + fun joinChannel(idHex: String, accountStateViewModel: AccountStateViewModel) { + followingChannels.add(idHex) + accountStateViewModel.saveToEncryptedStorage(this) + } + + fun leaveChannel(idHex: String, accountStateViewModel: AccountStateViewModel) { + followingChannels.remove(idHex) accountStateViewModel.saveToEncryptedStorage(this) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt index a023e222c..a8d9757b7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt @@ -42,6 +42,11 @@ class Channel(val id: ByteArray) { return info.picture ?: "https://robohash.org/${idHex}.png" } + fun anyNameStartsWith(prefix: String): Boolean { + return listOfNotNull(info.name, info.about) + .filter { it.startsWith(prefix, true) }.isNotEmpty() + } + // Observers line up here. val live: ChannelLiveData = ChannelLiveData(this) 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 eb58a7b0a..7e0164a60 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -260,6 +260,8 @@ object LocalCache { } else { // older data, does nothing } + + refreshObservers() } fun consume(event: ChannelMetadataEvent) { //Log.d("MT", "New User ${users.size} ${event.contactMetaData.name}") @@ -279,6 +281,8 @@ object LocalCache { } else { //Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}") } + + refreshObservers() } fun consume(event: ChannelMessageEvent) { @@ -293,7 +297,7 @@ object LocalCache { val author = getOrCreateUser(event.pubKey) val mentions = Collections.synchronizedList(event.mentions.map { getOrCreateUser(decodePublicKey(it)) }) - val replyTo = Collections.synchronizedList(event.replyTos.map { getOrCreateNote(it) }.toMutableList()) + val replyTo = Collections.synchronizedList(event.replyTos.map { channel.getOrCreateNote(it) }.toMutableList()) note.channel = channel note.loadEvent(event, author, mentions, replyTo) @@ -332,6 +336,22 @@ object LocalCache { } } + fun findNotesStartingWith(text: String): List { + return notes.values.filter { + (it.event is TextNoteEvent && it.event?.content?.contains(text) ?: false) + || it.idHex.startsWith(text, true) + || it.id.toNote().startsWith(text, true) + } + } + + fun findChannelsStartingWith(text: String): List { + return channels.values.filter { + it.anyNameStartsWith(text) + || it.idHex.startsWith(text, true) + || it.id.toNote().startsWith(text, true) + } + } + // Observers line up here. val live: LocalCacheLiveData = LocalCacheLiveData(this) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt index 5e85e934a..60f557711 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -120,6 +120,7 @@ class User(val pubkey: ByteArray) { handlerWaiting = true filterHandler.postDelayed({ + println("User Refresh") live.refresh() handlerWaiting = false }, 100) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelViewModel.kt index 7ff0e1f71..6b472474c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelViewModel.kt @@ -23,9 +23,9 @@ class NewChannelViewModel: ViewModel() { this.account = account if (channel != null) { originalChannel = channel - channelName.value = TextFieldValue() - channelPicture.value = TextFieldValue() - channelDescription.value = TextFieldValue() + channelName.value = TextFieldValue(channel.info.name ?: "") + channelPicture.value = TextFieldValue(channel.info.picture ?: "") + channelDescription.value = TextFieldValue(channel.info.about ?: "") } } 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 eb3edb24f..99317e6df 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 @@ -95,7 +95,7 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr } @Composable -private fun ChannelName( +fun ChannelName( channelPicture: String, channelTitle: @Composable () -> Unit, channelLastTime: Long?, @@ -124,10 +124,13 @@ private fun ChannelName( ) { channelTitle() - Text( - timeAgo(channelLastTime), - color = MaterialTheme.colors.onSurface.copy(alpha = 0.52f) - ) + channelLastTime?.let { + Text( + timeAgo(channelLastTime), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.52f) + ) + } + } if (channelLastContent != null) 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 334a9422f..b6ce28550 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 @@ -48,6 +48,7 @@ import com.vitorpamplona.amethyst.service.NostrChannelDataSource import com.vitorpamplona.amethyst.ui.actions.NewChannelView import com.vitorpamplona.amethyst.ui.actions.NewPostView import com.vitorpamplona.amethyst.ui.actions.PostButton +import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable @@ -72,17 +73,23 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accoun Column(Modifier.fillMaxHeight()) { ChannelHeader( channel, account, - accountStateViewModel = accountStateViewModel + accountStateViewModel = accountStateViewModel, + navController = navController ) Column( - modifier = Modifier.fillMaxHeight().padding(vertical = 0.dp).weight(1f, true) + modifier = Modifier + .fillMaxHeight() + .padding(vertical = 0.dp) + .weight(1f, true) ) { ChatroomFeedView(feedViewModel, accountViewModel, navController) } //LAST ROW - Row(modifier = Modifier.padding(10.dp).fillMaxWidth(), + Row(modifier = Modifier + .padding(10.dp) + .fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { @@ -92,7 +99,9 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accoun keyboardOptions = KeyboardOptions.Default.copy( capitalization = KeyboardCapitalization.Sentences ), - modifier = Modifier.weight(1f, true).padding(end = 10.dp), + modifier = Modifier + .weight(1f, true) + .padding(end = 10.dp), placeholder = { Text( text = "reply here.. ", @@ -116,7 +125,7 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accoun } @Composable -fun ChannelHeader(baseChannel: Channel, account: Account, accountStateViewModel: AccountStateViewModel) { +fun ChannelHeader(baseChannel: Channel, account: Account, accountStateViewModel: AccountStateViewModel, navController: NavController) { val channelState by baseChannel.live.observeAsState() val channel = channelState?.channel @@ -128,11 +137,14 @@ fun ChannelHeader(baseChannel: Channel, account: Account, accountStateViewModel: model = channel?.profilePicture(), contentDescription = "Profile Image", modifier = Modifier - .width(35.dp).height(35.dp) + .width(35.dp) + .height(35.dp) .clip(shape = CircleShape) ) - Column(modifier = Modifier.padding(start = 10.dp).weight(1f)) { + Column(modifier = Modifier + .padding(start = 10.dp) + .weight(1f)) { Row(verticalAlignment = Alignment.CenterVertically) { Text( "${channel?.info?.name}", @@ -153,7 +165,17 @@ fun ChannelHeader(baseChannel: Channel, account: Account, accountStateViewModel: channel?.let { NoteCopyButton(it) } - channel?.let { EditButton(account, it, accountStateViewModel) } + channel?.let { + if (channel.creator == account.userProfile()) { + EditButton(account, it, accountStateViewModel) + } else { + if (account.followingChannels.contains(channel.idHex)) { + LeaveButton(account,channel,accountStateViewModel, navController) + } else { + JoinButton(account,channel,accountStateViewModel, navController) + } + } + } } } @@ -180,7 +202,7 @@ private fun NoteCopyButton( backgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ), ) { - Text(text = "npub", color = Color.White) + Text(text = "note", color = Color.White) } } @@ -204,4 +226,40 @@ private fun EditButton(account: Account, channel: Channel, accountStateViewModel ) { Text(text = "Edit", color = Color.White) } +} + +@Composable +private fun JoinButton(account: Account, channel: Channel, accountStateViewModel: AccountStateViewModel, navController: NavController) { + Button( + modifier = Modifier.padding(horizontal = 3.dp), + onClick = { + account.joinChannel(channel.idHex, accountStateViewModel) + navController.navigate(Route.Message.route) + }, + shape = RoundedCornerShape(20.dp), + colors = ButtonDefaults + .buttonColors( + backgroundColor = MaterialTheme.colors.primary + ) + ) { + Text(text = "Join", color = Color.White) + } +} + +@Composable +private fun LeaveButton(account: Account, channel: Channel, accountStateViewModel: AccountStateViewModel, navController: NavController) { + Button( + modifier = Modifier.padding(horizontal = 3.dp), + onClick = { + account.leaveChannel(channel.idHex, accountStateViewModel) + navController.navigate(Route.Message.route) + }, + shape = RoundedCornerShape(20.dp), + colors = ButtonDefaults + .buttonColors( + backgroundColor = MaterialTheme.colors.primary + ) + ) { + Text(text = "Leave", color = Color.White) + } } \ No newline at end of file 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 1d4cd7e15..137f9d894 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 @@ -39,6 +39,7 @@ 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.font.FontWeight import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp @@ -46,11 +47,15 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import coil.compose.AsyncImage import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note 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.ChannelName +import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -66,16 +71,18 @@ fun SearchScreen(accountViewModel: AccountViewModel, navController: NavControlle Column( modifier = Modifier.padding(vertical = 0.dp) ) { - SearchBar(navController) + SearchBar(accountViewModel, navController) FeedView(feedViewModel, accountViewModel, navController) } } } @Composable -private fun SearchBar(navController: NavController) { +private fun SearchBar(accountViewModel: AccountViewModel, navController: NavController) { val searchValue = remember { mutableStateOf(TextFieldValue("")) } val searchResults = remember { mutableStateOf>(emptyList()) } + val searchResultsNotes = remember { mutableStateOf>(emptyList()) } + val searchResultsChannels = remember { mutableStateOf>(emptyList()) } val isTrailingIconVisible by remember { derivedStateOf { @@ -96,6 +103,8 @@ private fun SearchBar(navController: NavController) { onValueChange = { searchValue.value = it searchResults.value = LocalCache.findUsersStartingWith(it.text) + searchResultsNotes.value = LocalCache.findNotesStartingWith(it.text) + searchResultsChannels.value = LocalCache.findChannelsStartingWith(it.text) }, keyboardOptions = KeyboardOptions.Default.copy( capitalization = KeyboardCapitalization.Sentences @@ -123,6 +132,8 @@ private fun SearchBar(navController: NavController) { onClick = { searchValue.value = TextFieldValue("") searchResults.value = emptyList() + searchResultsChannels.value = emptyList() + searchResultsNotes.value = emptyList() } ) { Icon( @@ -148,6 +159,24 @@ private fun SearchBar(navController: NavController) { navController.navigate("User/${item.pubkeyHex}") } } + + itemsIndexed(searchResultsChannels.value, key = { _, item -> item.idHex }) { index, item -> + ChannelName( + channelPicture = item.profilePicture(), + channelTitle = { + Text( + "${item.info.name}", + fontWeight = FontWeight.Bold + ) + }, + channelLastTime = null, + channelLastContent = item.info.about, + onClick = { navController.navigate("Channel/${item.idHex}") }) + } + + itemsIndexed(searchResultsNotes.value, key = { _, item -> item.idHex }) { index, item -> + NoteCompose(item, accountViewModel = accountViewModel, navController = navController) + } } } }