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 7241cc474..fe88ddfe1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -2,10 +2,14 @@ package com.vitorpamplona.amethyst.model import androidx.lifecycle.LiveData import com.vitorpamplona.amethyst.service.Constants +import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent +import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.relays.Client +import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import nostr.postr.Contact import nostr.postr.Persona import nostr.postr.Utils @@ -15,7 +19,8 @@ import nostr.postr.events.TextNoteEvent import nostr.postr.toHex val DefaultChannels = setOf( - "25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb" // -> Anigma's Nostr + "25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", // -> Anigma's Nostr + "42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5" // -> Amethyst's Group ) class Account(val loggedIn: Persona, val followingChannels: MutableSet = DefaultChannels.toMutableSet()) { @@ -149,6 +154,44 @@ class Account(val loggedIn: Persona, val followingChannels: MutableSet = LocalCache.consume(signedEvent) } + fun sendCreateNewChannel(name: String, about: String, picture: String, accountStateViewModel: AccountStateViewModel) { + if (!isWriteable()) return + + val metadata = ChannelCreateEvent.ChannelData( + name, about, picture + ) + + val event = ChannelCreateEvent.create( + channelInfo = metadata, + privateKey = loggedIn.privKey!! + ) + + Client.send(event) + LocalCache.consume(event) + + followingChannels.add(event.id.toHex()) + accountStateViewModel.saveToEncryptedStorage(this) + } + + fun sendChangeChannel(name: String, about: String, picture: String, channel: Channel) { + if (!isWriteable()) return + + val metadata = ChannelCreateEvent.ChannelData( + name, about, picture + ) + + val event = ChannelMetadataEvent.create( + newChannelInfo = metadata, + originalChannelIdHex = channel.idHex, + privateKey = loggedIn.privKey!! + ) + + Client.send(event) + LocalCache.consume(event) + + followingChannels.add(event.id.toHex()) + } + fun decryptContent(note: Note): String? { val event = note.event return if (event is PrivateDmEvent && loggedIn.privKey != null) { 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 bb8408e29..a023e222c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt @@ -13,6 +13,7 @@ class Channel(val id: ByteArray) { val idHex = id.toHexKey() val idDisplayHex = id.toShortenHex() + var creator: User? = null var info = ChannelCreateEvent.ChannelData(null, null, null) var updatedMetadataAt: Long = 0; @@ -28,9 +29,10 @@ class Channel(val id: ByteArray) { } } - fun updateChannelInfo(channelInfo: ChannelCreateEvent.ChannelData, updatedAt: Long) { - info = channelInfo - updatedMetadataAt = updatedAt + fun updateChannelInfo(creator: User, channelInfo: ChannelCreateEvent.ChannelData, updatedAt: Long) { + this.creator = creator + this.info = channelInfo + this.updatedMetadataAt = updatedAt live.refresh() } 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 c205f426c..eb58a7b0a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -245,10 +245,18 @@ object LocalCache { } fun consume(event: ChannelCreateEvent) { + Log.d("MT", "New Event ${event.content} ${event.id.toHex()}") // new event val oldChannel = getOrCreateChannel(event.id.toHex()) + val author = getOrCreateUser(event.pubKey) if (event.createdAt > oldChannel.updatedMetadataAt) { - oldChannel.updateChannelInfo(event.channelInfo, event.createdAt) + if (oldChannel.creator == null || oldChannel.creator == author) { + oldChannel.updateChannelInfo(author, event.channelInfo, event.createdAt) + + val note = oldChannel.getOrCreateNote(event.id.toHex()) + note.channel = oldChannel + note.loadEvent(event, author, emptyList(), mutableListOf()) + } } else { // older data, does nothing } @@ -259,8 +267,15 @@ object LocalCache { // new event val oldChannel = getOrCreateChannel(event.channel) + val author = getOrCreateUser(event.pubKey) if (event.createdAt > oldChannel.updatedMetadataAt) { - oldChannel.updateChannelInfo(event.channelInfo, event.createdAt) + if (oldChannel.creator == null || oldChannel.creator == author) { + oldChannel.updateChannelInfo(author, event.channelInfo, event.createdAt) + + val note = oldChannel.getOrCreateNote(event.id.toHex()) + note.channel = oldChannel + note.loadEvent(event, author, emptyList(), mutableListOf()) + } } else { //Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}") } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt index 05fce8b5e..347f228b0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt @@ -1,6 +1,7 @@ package com.vitorpamplona.amethyst.service import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent @@ -21,6 +22,11 @@ object NostrChatroomListDataSource: NostrDataSource("MailBoxFeed") { authors = listOf(account.userProfile().pubkeyHex) ) + fun createChannelsCreatedbyMeFilter() = JsonFilter( + kinds = listOf(ChannelCreateEvent.kind, ChannelMetadataEvent.kind), + authors = listOf(account.userProfile().pubkeyHex) + ) + fun createMyChannelsFilter() = JsonFilter( kinds = listOf(ChannelCreateEvent.kind), ids = account.followingChannels.toList() @@ -53,6 +59,8 @@ object NostrChatroomListDataSource: NostrDataSource("MailBoxFeed") { val myChannelsInfoChannel = requestNewChannel() val myChannelsMessagesChannel = requestNewChannel() + val myChannelsCreatedbyMeChannel = requestNewChannel() + // returns the last Note of each user. override fun feed(): List { val messages = account.userProfile().messages @@ -66,6 +74,12 @@ object NostrChatroomListDataSource: NostrDataSource("MailBoxFeed") { it.notes.values.sortedBy { it.event?.createdAt }.lastOrNull { it.event != null } } + val channelsCreatedByMe = LocalCache.channels.values.filter { + it.creator == account.userProfile() + }.map { + it.notes.values.sortedBy { it.event?.createdAt }.lastOrNull { it.event != null } + } + return (privateMessages + publicChannels).filterNotNull().sortedBy { it.event?.createdAt }.reversed() } @@ -75,5 +89,7 @@ object NostrChatroomListDataSource: NostrDataSource("MailBoxFeed") { myChannelsChannel.filter = listOf(createMyChannelsFilter()).ifEmpty { null } myChannelsInfoChannel.filter = createLastChannelInfoFilter().ifEmpty { null } myChannelsMessagesChannel.filter = createLastMessageOfEachChannelFilter().ifEmpty { null } + + //myChannelsCreatedbyMeChannel.filter = listOf(createChannelsCreatedbyMeFilter()).ifEmpty { null } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMetadataEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMetadataEvent.kt index 433088563..372b1684a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMetadataEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMetadataEvent.kt @@ -30,14 +30,14 @@ class ChannelMetadataEvent ( companion object { const val kind = 41 - fun create(newChannelInfo: ChannelCreateEvent.ChannelData?, originalChannel: ChannelCreateEvent, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMetadataEvent { + fun create(newChannelInfo: ChannelCreateEvent.ChannelData?, originalChannelIdHex: String, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMetadataEvent { val content = if (newChannelInfo != null) gson.toJson(newChannelInfo) else "" val pubKey = Utils.pubkeyCreate(privateKey) - val tags = listOf( listOf("e", originalChannel.id.toHex(), "", "root") ) + val tags = listOf( listOf("e", originalChannelIdHex, "", "root") ) val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) return ChannelMetadataEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelView.kt new file mode 100644 index 000000000..edd2ddfe6 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelView.kt @@ -0,0 +1,123 @@ +package com.vitorpamplona.amethyst.ui.actions + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.KeyboardCapitalization +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 com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel + +@Composable +fun NewChannelView(onClose: () -> Unit, account: Account, accountStateViewModel: AccountStateViewModel, channel: Channel? = null) { + val postViewModel: NewChannelViewModel = viewModel() + + postViewModel.load(account, channel, accountStateViewModel) + + Dialog( + onDismissRequest = { onClose() }, + properties = DialogProperties( + dismissOnClickOutside = false + ) + ) { + Surface( + ) { + Column( + modifier = Modifier.padding(10.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + CloseButton(onCancel = { + postViewModel.clear() + onClose() + }) + + PostButton( + onPost = { + postViewModel.create() + onClose() + }, + postViewModel.channelName.value.text.isNotBlank() + ) + } + + Spacer(modifier = Modifier.height(15.dp)) + + OutlinedTextField( + label = { Text(text = "Channel Name") }, + modifier = Modifier.fillMaxWidth(), + value = postViewModel.channelName.value, + onValueChange = { postViewModel.channelName.value = it }, + placeholder = { + Text( + text = "My Awesome Group", + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + }, + keyboardOptions = KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.Sentences + ) + ) + + Spacer(modifier = Modifier.height(15.dp)) + + OutlinedTextField( + label = { Text(text = "Picture Url") }, + modifier = Modifier.fillMaxWidth(), + value = postViewModel.channelPicture.value, + onValueChange = { postViewModel.channelPicture.value = it }, + placeholder = { + Text( + text = "http://mygroup.com/logo.jpg", + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + } + ) + + Spacer(modifier = Modifier.height(15.dp)) + + OutlinedTextField( + label = { Text(text = "Description") }, + modifier = Modifier.fillMaxWidth().height(100.dp), + value = postViewModel.channelDescription.value, + onValueChange = { postViewModel.channelDescription.value = it }, + placeholder = { + Text( + text = "About us.. ", + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + }, + keyboardOptions = KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.Sentences + ), + maxLines = 10 + + ) + } + } + } + + + +} 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 new file mode 100644 index 000000000..7ff0e1f71 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewChannelViewModel.kt @@ -0,0 +1,56 @@ +package com.vitorpamplona.amethyst.ui.actions + +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.text.input.TextFieldValue +import androidx.lifecycle.ViewModel +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel + +class NewChannelViewModel: ViewModel() { + private var account: Account? = null + private var originalChannel: Channel? = null + private var accountStateViewModel: AccountStateViewModel? = null + + val channelName = mutableStateOf(TextFieldValue()) + val channelPicture = mutableStateOf(TextFieldValue()) + val channelDescription = mutableStateOf(TextFieldValue()) + + + fun load(account: Account, channel: Channel?, accountStateViewModel: AccountStateViewModel) { + this.accountStateViewModel = accountStateViewModel + this.account = account + if (channel != null) { + originalChannel = channel + channelName.value = TextFieldValue() + channelPicture.value = TextFieldValue() + channelDescription.value = TextFieldValue() + } + } + + fun create() { + if (originalChannel == null) + this.account?.sendCreateNewChannel( + channelName.value.text, + channelDescription.value.text, + channelPicture.value.text, + accountStateViewModel!! + ) + else + this.account?.sendChangeChannel( + channelName.value.text, + channelDescription.value.text, + channelPicture.value.text, + originalChannel!! + ) + + clear() + } + + fun clear() { + channelName.value = TextFieldValue() + channelPicture.value = TextFieldValue() + channelDescription.value = TextFieldValue() + } +} \ No newline at end of file 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 96d6a2d86..f590ef121 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 @@ -241,6 +241,25 @@ fun PostButton(onPost: () -> Unit = {}, isActive: Boolean, modifier: Modifier = } } +@Composable +fun CreateButton(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 + ) + ) { + Text(text = "Create", color = Color.White) + } +} + @Composable fun SearchButton(onPost: () -> Unit = {}, isActive: Boolean, modifier: Modifier = Modifier) { Button( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewChannelButton.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewChannelButton.kt new file mode 100644 index 000000000..f22df4563 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewChannelButton.kt @@ -0,0 +1,53 @@ +package com.vitorpamplona.amethyst.buttons + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Add +import androidx.compose.material.icons.outlined.Visibility +import androidx.compose.material.icons.outlined.VisibilityOff +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.ui.actions.NewChannelView +import com.vitorpamplona.amethyst.ui.actions.NewPostView +import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel + +@Composable +fun NewChannelButton(account: Account, accountStateViewModel: AccountStateViewModel) { + var wantsToPost by remember { + mutableStateOf(false) + } + + if (wantsToPost) + NewChannelView({ wantsToPost = false }, account = account, accountStateViewModel) + + OutlinedButton( + onClick = { wantsToPost = true }, + modifier = Modifier.size(55.dp), + shape = CircleShape, + colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary), + contentPadding = PaddingValues(0.dp), + ) { + Icon( + imageVector = Icons.Outlined.Add, + contentDescription = "New Channel", + modifier = Modifier.size(26.dp), + tint = Color.White + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index ca1bf80ff..8db00572c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -4,16 +4,18 @@ import androidx.compose.runtime.Composable import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable fun AppNavigation( navController: NavHostController, - accountViewModel: AccountViewModel + accountViewModel: AccountViewModel, + accountStateViewModel: AccountStateViewModel ) { NavHost(navController, startDestination = Route.Home.route) { Routes.forEach { - composable(it.route, it.arguments, content = it.buildScreen(accountViewModel, navController)) + composable(it.route, it.arguments, content = it.buildScreen(accountViewModel, accountStateViewModel, navController)) } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index cd5db1a2e..f4e6d9ebd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -10,6 +10,7 @@ import androidx.navigation.NavType import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.navArgument import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.ChannelScreen import com.vitorpamplona.amethyst.ui.screen.ChatroomListScreen import com.vitorpamplona.amethyst.ui.screen.ChatroomScreen @@ -25,31 +26,31 @@ sealed class Route( val route: String, val icon: Int, val arguments: List = emptyList(), - val buildScreen: (AccountViewModel, NavController) -> @Composable (NavBackStackEntry) -> Unit + val buildScreen: (AccountViewModel, AccountStateViewModel, NavController) -> @Composable (NavBackStackEntry) -> Unit ) { - object Home : Route("Home", R.drawable.ic_home, buildScreen = { acc, nav -> { _ -> HomeScreen(acc, nav) } }) - object Search : Route("Search", R.drawable.ic_search, buildScreen = { acc, nav -> { _ -> SearchScreen(acc, nav) }}) - object Notification : Route("Notification", R.drawable.ic_notifications,buildScreen = { acc, nav -> { _ -> NotificationScreen(acc, nav) }}) - object Message : Route("Message", R.drawable.ic_dm, buildScreen = { acc, nav -> { _ -> ChatroomListScreen(acc, nav) }}) + object Home : Route("Home", R.drawable.ic_home, buildScreen = { acc, accSt, nav -> { _ -> HomeScreen(acc, nav) } }) + object Search : Route("Search", R.drawable.ic_search, buildScreen = { acc, accSt, nav -> { _ -> SearchScreen(acc, nav) }}) + object Notification : Route("Notification", R.drawable.ic_notifications,buildScreen = { acc, accSt, nav -> { _ -> NotificationScreen(acc, nav) }}) + object Message : Route("Message", R.drawable.ic_dm, buildScreen = { acc, accSt, nav -> { _ -> ChatroomListScreen(acc, nav) }}) object Profile : Route("User/{id}", R.drawable.ic_profile, arguments = listOf(navArgument("id") { type = NavType.StringType } ), - buildScreen = { acc, nav -> { ProfileScreen(it.arguments?.getString("id"), acc, nav) }} + buildScreen = { acc, accSt, nav -> { ProfileScreen(it.arguments?.getString("id"), acc, nav) }} ) object Note : Route("Note/{id}", R.drawable.ic_moments, arguments = listOf(navArgument("id") { type = NavType.StringType } ), - buildScreen = { acc, nav -> { ThreadScreen(it.arguments?.getString("id"), acc, nav) }} + buildScreen = { acc, accSt, nav -> { ThreadScreen(it.arguments?.getString("id"), acc, nav) }} ) object Room : Route("Room/{id}", R.drawable.ic_moments, arguments = listOf(navArgument("id") { type = NavType.StringType } ), - buildScreen = { acc, nav -> { ChatroomScreen(it.arguments?.getString("id"), acc, nav) }} + buildScreen = { acc, accSt, nav -> { ChatroomScreen(it.arguments?.getString("id"), acc, nav) }} ) object Channel : Route("Channel/{id}", R.drawable.ic_moments, arguments = listOf(navArgument("id") { type = NavType.StringType } ), - buildScreen = { acc, nav -> { ChannelScreen(it.arguments?.getString("id"), acc, nav) }} + buildScreen = { acc, accSt, nav -> { ChannelScreen(it.arguments?.getString("id"), acc, accSt, nav) }} ) } 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 1cb39f345..eb3edb24f 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,6 +23,8 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavController import coil.compose.AsyncImage import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent +import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable @@ -42,6 +44,13 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr val channelState by note.channel!!.live.observeAsState() val channel = channelState?.channel + val description = if (note.event is ChannelCreateEvent) { + "Channel created" + } else if (note.event is ChannelMetadataEvent) { + "Channel Information changed to " + } else { + note.event?.content + } channel?.let { ChannelName( channelPicture = it.profilePicture(), @@ -52,7 +61,7 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr ) }, channelLastTime = note.event?.createdAt, - channelLastContent = "${author?.toBestDisplayName()}: " + note.event?.content, + channelLastContent = "${author?.toBestDisplayName()}: " + description, onClick = { navController.navigate("Channel/${it.idHex}") }) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index cb7bfa911..802707424 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -35,7 +35,9 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavController import coil.compose.AsyncImage import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent +import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.ui.components.RichTextViewer import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -134,26 +136,41 @@ fun ChatroomMessageCompose(baseNote: Note, accountViewModel: AccountViewModel, n } Row(verticalAlignment = Alignment.CenterVertically) { - val eventContent = accountViewModel.decrypt(note) + val event = note.event + if (event is ChannelCreateEvent) { + Text(text = "${note.author?.toBestDisplayName()} created " + + "${event.channelInfo.name ?: ""} with " + + "description of '${event.channelInfo.about ?: ""}', " + + "and picture '${event.channelInfo.picture ?: ""}'") + } else if (event is ChannelMetadataEvent) { + Text(text = "${note.author?.toBestDisplayName()} changed " + + "chat name to '${event.channelInfo.name ?: ""}', " + + "description to '${event.channelInfo.about ?: ""}', " + + "and picture to '${event.channelInfo.picture ?: ""}'") + } else { + val eventContent = accountViewModel.decrypt(note) - if (eventContent != null) - RichTextViewer( - eventContent, - note.event?.tags, - note, - accountViewModel, - navController - ) - else - RichTextViewer( - "Could Not decrypt the message", - note.event?.tags, - note, - accountViewModel, - navController - ) + if (eventContent != null) + RichTextViewer( + eventContent, + note.event?.tags, + note, + accountViewModel, + navController + ) + else + RichTextViewer( + "Could Not decrypt the message", + note.event?.tags, + note, + accountViewModel, + navController + ) + + } } + Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.End, 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 b5ee89abd..334a9422f 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 @@ -9,7 +9,10 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width 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.MaterialTheme import androidx.compose.material.Text @@ -20,9 +23,14 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue 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.platform.ClipboardManager +import androidx.compose.ui.platform.LocalClipboardManager +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 @@ -32,13 +40,18 @@ import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import coil.compose.AsyncImage +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.toNote 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.screen.loggedIn.AccountViewModel @Composable -fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, navController: NavController) { +fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accountStateViewModel: AccountStateViewModel, navController: NavController) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account @@ -58,9 +71,8 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, navCon Column(Modifier.fillMaxHeight()) { ChannelHeader( - channel, - accountViewModel = accountViewModel, - navController = navController + channel, account, + accountStateViewModel = accountStateViewModel ) Column( @@ -104,7 +116,7 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, navCon } @Composable -fun ChannelHeader(baseChannel: Channel, accountViewModel: AccountViewModel, navController: NavController) { +fun ChannelHeader(baseChannel: Channel, account: Account, accountStateViewModel: AccountStateViewModel) { val channelState by baseChannel.live.observeAsState() val channel = channelState?.channel @@ -120,7 +132,7 @@ fun ChannelHeader(baseChannel: Channel, accountViewModel: AccountViewModel, navC .clip(shape = CircleShape) ) - Column(modifier = Modifier.padding(start = 10.dp)) { + Column(modifier = Modifier.padding(start = 10.dp).weight(1f)) { Row(verticalAlignment = Alignment.CenterVertically) { Text( "${channel?.info?.name}", @@ -132,12 +144,16 @@ fun ChannelHeader(baseChannel: Channel, accountViewModel: AccountViewModel, navC Text( "${channel?.info?.about}", color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), - maxLines = 1, + maxLines = 2, overflow = TextOverflow.Ellipsis, fontSize = 12.sp ) } } + + channel?.let { NoteCopyButton(it) } + + channel?.let { EditButton(account, it, accountStateViewModel) } } } @@ -146,4 +162,46 @@ fun ChannelHeader(baseChannel: Channel, accountViewModel: AccountViewModel, navC thickness = 0.25.dp ) } +} + + +@Composable +private fun NoteCopyButton( + note: Channel +) { + val clipboardManager = LocalClipboardManager.current + + Button( + modifier = Modifier.padding(horizontal = 3.dp), + onClick = { clipboardManager.setText(AnnotatedString(note.id.toNote())) }, + shape = RoundedCornerShape(20.dp), + colors = ButtonDefaults + .buttonColors( + backgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ), + ) { + Text(text = "npub", color = Color.White) + } +} + +@Composable +private fun EditButton(account: Account, channel: Channel, accountStateViewModel: AccountStateViewModel) { + var wantsToPost by remember { + mutableStateOf(false) + } + + if (wantsToPost) + NewChannelView({ wantsToPost = false }, account = account, accountStateViewModel, channel) + + Button( + modifier = Modifier.padding(horizontal = 3.dp), + onClick = { wantsToPost = true }, + shape = RoundedCornerShape(20.dp), + colors = ButtonDefaults + .buttonColors( + backgroundColor = MaterialTheme.colors.primary + ) + ) { + Text(text = "Edit", color = Color.White) + } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt index 398d3063c..d3a4ab634 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController +import com.vitorpamplona.amethyst.buttons.NewChannelButton import com.vitorpamplona.amethyst.buttons.NewNoteButton import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar import com.vitorpamplona.amethyst.ui.navigation.AppNavigation @@ -49,7 +50,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun scaffoldState = scaffoldState ) { Column(modifier = Modifier.padding(bottom = it.calculateBottomPadding())) { - AppNavigation(navController, accountViewModel) + AppNavigation(navController, accountViewModel, accountStateViewModel) } } } @@ -73,4 +74,20 @@ fun FloatingButton(navController: NavHostController, accountViewModel: AccountSt } } } + + if (currentRoute(navController) == Route.Message.route) { + Crossfade(targetState = accountState) { state -> + when (state) { + is AccountState.LoggedInViewOnly -> { + // Does nothing. + } + is AccountState.LoggedOff -> { + // Does nothing. + } + is AccountState.LoggedIn -> { + NewChannelButton(state.account, accountViewModel) + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index 65bf853af..8f7d36302 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -51,7 +51,9 @@ import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.pagerTabIndicatorOffset import com.google.accompanist.pager.rememberPagerState import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.toNote import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource import com.vitorpamplona.amethyst.service.NostrUserProfileFollowersDataSource import com.vitorpamplona.amethyst.service.NostrUserProfileFollowsDataSource @@ -73,8 +75,6 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro val accountUserState by accountViewModel.userLiveData.observeAsState() val accountUser = accountUserState?.user - val clipboardManager = LocalClipboardManager.current - if (userId != null && account != null && accountUser != null) { DisposableEffect(account) { NostrUserProfileDataSource.loadUserProfile(userId) @@ -138,7 +138,7 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro MessageButton(user, navController) - NPubCopyButton(clipboardManager, user) + NPubCopyButton(user) if (accountUser == user) { EditButton() @@ -262,9 +262,10 @@ fun TabFollowers(user: User, accountViewModel: AccountViewModel, navController: @Composable private fun NPubCopyButton( - clipboardManager: ClipboardManager, user: User ) { + val clipboardManager = LocalClipboardManager.current + Button( modifier = Modifier.padding(horizontal = 3.dp), onClick = { clipboardManager.setText(AnnotatedString(user.pubkey.toNpub())) },