Add Channel

This commit is contained in:
Vitor Pamplona
2023-01-18 22:00:32 -05:00
parent 0e3b007730
commit 75219f0f2d
16 changed files with 481 additions and 49 deletions

View File

@@ -2,10 +2,14 @@ package com.vitorpamplona.amethyst.model
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.Constants 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.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.relays.Client 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.Contact
import nostr.postr.Persona import nostr.postr.Persona
import nostr.postr.Utils import nostr.postr.Utils
@@ -15,7 +19,8 @@ import nostr.postr.events.TextNoteEvent
import nostr.postr.toHex import nostr.postr.toHex
val DefaultChannels = setOf( val DefaultChannels = setOf(
"25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb" // -> Anigma's Nostr "25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", // -> Anigma's Nostr
"42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5" // -> Amethyst's Group
) )
class Account(val loggedIn: Persona, val followingChannels: MutableSet<String> = DefaultChannels.toMutableSet()) { class Account(val loggedIn: Persona, val followingChannels: MutableSet<String> = DefaultChannels.toMutableSet()) {
@@ -149,6 +154,44 @@ class Account(val loggedIn: Persona, val followingChannels: MutableSet<String> =
LocalCache.consume(signedEvent) 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? { fun decryptContent(note: Note): String? {
val event = note.event val event = note.event
return if (event is PrivateDmEvent && loggedIn.privKey != null) { return if (event is PrivateDmEvent && loggedIn.privKey != null) {

View File

@@ -13,6 +13,7 @@ class Channel(val id: ByteArray) {
val idHex = id.toHexKey() val idHex = id.toHexKey()
val idDisplayHex = id.toShortenHex() val idDisplayHex = id.toShortenHex()
var creator: User? = null
var info = ChannelCreateEvent.ChannelData(null, null, null) var info = ChannelCreateEvent.ChannelData(null, null, null)
var updatedMetadataAt: Long = 0; var updatedMetadataAt: Long = 0;
@@ -28,9 +29,10 @@ class Channel(val id: ByteArray) {
} }
} }
fun updateChannelInfo(channelInfo: ChannelCreateEvent.ChannelData, updatedAt: Long) { fun updateChannelInfo(creator: User, channelInfo: ChannelCreateEvent.ChannelData, updatedAt: Long) {
info = channelInfo this.creator = creator
updatedMetadataAt = updatedAt this.info = channelInfo
this.updatedMetadataAt = updatedAt
live.refresh() live.refresh()
} }

View File

@@ -245,10 +245,18 @@ object LocalCache {
} }
fun consume(event: ChannelCreateEvent) { fun consume(event: ChannelCreateEvent) {
Log.d("MT", "New Event ${event.content} ${event.id.toHex()}")
// new event // new event
val oldChannel = getOrCreateChannel(event.id.toHex()) val oldChannel = getOrCreateChannel(event.id.toHex())
val author = getOrCreateUser(event.pubKey)
if (event.createdAt > oldChannel.updatedMetadataAt) { 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 { } else {
// older data, does nothing // older data, does nothing
} }
@@ -259,8 +267,15 @@ object LocalCache {
// new event // new event
val oldChannel = getOrCreateChannel(event.channel) val oldChannel = getOrCreateChannel(event.channel)
val author = getOrCreateUser(event.pubKey)
if (event.createdAt > oldChannel.updatedMetadataAt) { 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 { } else {
//Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}") //Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}")
} }

View File

@@ -1,6 +1,7 @@
package com.vitorpamplona.amethyst.service package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
@@ -21,6 +22,11 @@ object NostrChatroomListDataSource: NostrDataSource<Note>("MailBoxFeed") {
authors = listOf(account.userProfile().pubkeyHex) authors = listOf(account.userProfile().pubkeyHex)
) )
fun createChannelsCreatedbyMeFilter() = JsonFilter(
kinds = listOf(ChannelCreateEvent.kind, ChannelMetadataEvent.kind),
authors = listOf(account.userProfile().pubkeyHex)
)
fun createMyChannelsFilter() = JsonFilter( fun createMyChannelsFilter() = JsonFilter(
kinds = listOf(ChannelCreateEvent.kind), kinds = listOf(ChannelCreateEvent.kind),
ids = account.followingChannels.toList() ids = account.followingChannels.toList()
@@ -53,6 +59,8 @@ object NostrChatroomListDataSource: NostrDataSource<Note>("MailBoxFeed") {
val myChannelsInfoChannel = requestNewChannel() val myChannelsInfoChannel = requestNewChannel()
val myChannelsMessagesChannel = requestNewChannel() val myChannelsMessagesChannel = requestNewChannel()
val myChannelsCreatedbyMeChannel = requestNewChannel()
// returns the last Note of each user. // returns the last Note of each user.
override fun feed(): List<Note> { override fun feed(): List<Note> {
val messages = account.userProfile().messages val messages = account.userProfile().messages
@@ -66,6 +74,12 @@ object NostrChatroomListDataSource: NostrDataSource<Note>("MailBoxFeed") {
it.notes.values.sortedBy { it.event?.createdAt }.lastOrNull { it.event != null } 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() return (privateMessages + publicChannels).filterNotNull().sortedBy { it.event?.createdAt }.reversed()
} }
@@ -75,5 +89,7 @@ object NostrChatroomListDataSource: NostrDataSource<Note>("MailBoxFeed") {
myChannelsChannel.filter = listOf(createMyChannelsFilter()).ifEmpty { null } myChannelsChannel.filter = listOf(createMyChannelsFilter()).ifEmpty { null }
myChannelsInfoChannel.filter = createLastChannelInfoFilter().ifEmpty { null } myChannelsInfoChannel.filter = createLastChannelInfoFilter().ifEmpty { null }
myChannelsMessagesChannel.filter = createLastMessageOfEachChannelFilter().ifEmpty { null } myChannelsMessagesChannel.filter = createLastMessageOfEachChannelFilter().ifEmpty { null }
//myChannelsCreatedbyMeChannel.filter = listOf(createChannelsCreatedbyMeFilter()).ifEmpty { null }
} }
} }

View File

@@ -30,14 +30,14 @@ class ChannelMetadataEvent (
companion object { companion object {
const val kind = 41 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) val content = if (newChannelInfo != null)
gson.toJson(newChannelInfo) gson.toJson(newChannelInfo)
else else
"" ""
val pubKey = Utils.pubkeyCreate(privateKey) 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 id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey) val sig = Utils.sign(id, privateKey)
return ChannelMetadataEvent(id, pubKey, createdAt, tags, content, sig) return ChannelMetadataEvent(id, pubKey, createdAt, tags, content, sig)

View File

@@ -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
)
}
}
}
}

View File

@@ -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()
}
}

View File

@@ -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 @Composable
fun SearchButton(onPost: () -> Unit = {}, isActive: Boolean, modifier: Modifier = Modifier) { fun SearchButton(onPost: () -> Unit = {}, isActive: Boolean, modifier: Modifier = Modifier) {
Button( Button(

View File

@@ -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
)
}
}

View File

@@ -4,16 +4,18 @@ import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable @Composable
fun AppNavigation( fun AppNavigation(
navController: NavHostController, navController: NavHostController,
accountViewModel: AccountViewModel accountViewModel: AccountViewModel,
accountStateViewModel: AccountStateViewModel
) { ) {
NavHost(navController, startDestination = Route.Home.route) { NavHost(navController, startDestination = Route.Home.route) {
Routes.forEach { Routes.forEach {
composable(it.route, it.arguments, content = it.buildScreen(accountViewModel, navController)) composable(it.route, it.arguments, content = it.buildScreen(accountViewModel, accountStateViewModel, navController))
} }
} }
} }

View File

@@ -10,6 +10,7 @@ import androidx.navigation.NavType
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.vitorpamplona.amethyst.R 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.ChannelScreen
import com.vitorpamplona.amethyst.ui.screen.ChatroomListScreen import com.vitorpamplona.amethyst.ui.screen.ChatroomListScreen
import com.vitorpamplona.amethyst.ui.screen.ChatroomScreen import com.vitorpamplona.amethyst.ui.screen.ChatroomScreen
@@ -25,31 +26,31 @@ sealed class Route(
val route: String, val route: String,
val icon: Int, val icon: Int,
val arguments: List<NamedNavArgument> = emptyList(), val arguments: List<NamedNavArgument> = 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 Home : Route("Home", R.drawable.ic_home, buildScreen = { acc, accSt, nav -> { _ -> HomeScreen(acc, nav) } })
object Search : Route("Search", R.drawable.ic_search, buildScreen = { acc, nav -> { _ -> SearchScreen(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, nav -> { _ -> NotificationScreen(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, nav -> { _ -> ChatroomListScreen(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, object Profile : Route("User/{id}", R.drawable.ic_profile,
arguments = listOf(navArgument("id") { type = NavType.StringType } ), 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, object Note : Route("Note/{id}", R.drawable.ic_moments,
arguments = listOf(navArgument("id") { type = NavType.StringType } ), 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, object Room : Route("Room/{id}", R.drawable.ic_moments,
arguments = listOf(navArgument("id") { type = NavType.StringType } ), 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, object Channel : Route("Channel/{id}", R.drawable.ic_moments,
arguments = listOf(navArgument("id") { type = NavType.StringType } ), 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) }}
) )
} }

View File

@@ -23,6 +23,8 @@ import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.model.Note 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 import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable @Composable
@@ -42,6 +44,13 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
val channelState by note.channel!!.live.observeAsState() val channelState by note.channel!!.live.observeAsState()
val channel = channelState?.channel 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 { channel?.let {
ChannelName( ChannelName(
channelPicture = it.profilePicture(), channelPicture = it.profilePicture(),
@@ -52,7 +61,7 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
) )
}, },
channelLastTime = note.event?.createdAt, channelLastTime = note.event?.createdAt,
channelLastContent = "${author?.toBestDisplayName()}: " + note.event?.content, channelLastContent = "${author?.toBestDisplayName()}: " + description,
onClick = { navController.navigate("Channel/${it.idHex}") }) onClick = { navController.navigate("Channel/${it.idHex}") })
} }

View File

@@ -35,7 +35,9 @@ import androidx.compose.ui.unit.sp
import androidx.navigation.NavController import androidx.navigation.NavController
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.model.Note 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.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.ui.components.RichTextViewer import com.vitorpamplona.amethyst.ui.components.RichTextViewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -134,26 +136,41 @@ fun ChatroomMessageCompose(baseNote: Note, accountViewModel: AccountViewModel, n
} }
Row(verticalAlignment = Alignment.CenterVertically) { 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) if (eventContent != null)
RichTextViewer( RichTextViewer(
eventContent, eventContent,
note.event?.tags, note.event?.tags,
note, note,
accountViewModel, accountViewModel,
navController navController
) )
else else
RichTextViewer( RichTextViewer(
"Could Not decrypt the message", "Could Not decrypt the message",
note.event?.tags, note.event?.tags,
note, note,
accountViewModel, accountViewModel,
navController navController
) )
}
} }
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End, horizontalArrangement = Arrangement.End,

View File

@@ -9,7 +9,10 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions 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.Divider
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
@@ -20,9 +23,14 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip 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.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.TextFieldValue 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.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Channel 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.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.actions.PostButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable @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 accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account val account = accountState?.account
@@ -58,9 +71,8 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, navCon
Column(Modifier.fillMaxHeight()) { Column(Modifier.fillMaxHeight()) {
ChannelHeader( ChannelHeader(
channel, channel, account,
accountViewModel = accountViewModel, accountStateViewModel = accountStateViewModel
navController = navController
) )
Column( Column(
@@ -104,7 +116,7 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, navCon
} }
@Composable @Composable
fun ChannelHeader(baseChannel: Channel, accountViewModel: AccountViewModel, navController: NavController) { fun ChannelHeader(baseChannel: Channel, account: Account, accountStateViewModel: AccountStateViewModel) {
val channelState by baseChannel.live.observeAsState() val channelState by baseChannel.live.observeAsState()
val channel = channelState?.channel val channel = channelState?.channel
@@ -120,7 +132,7 @@ fun ChannelHeader(baseChannel: Channel, accountViewModel: AccountViewModel, navC
.clip(shape = CircleShape) .clip(shape = CircleShape)
) )
Column(modifier = Modifier.padding(start = 10.dp)) { Column(modifier = Modifier.padding(start = 10.dp).weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text( Text(
"${channel?.info?.name}", "${channel?.info?.name}",
@@ -132,12 +144,16 @@ fun ChannelHeader(baseChannel: Channel, accountViewModel: AccountViewModel, navC
Text( Text(
"${channel?.info?.about}", "${channel?.info?.about}",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
maxLines = 1, maxLines = 2,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
fontSize = 12.sp 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 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)
}
} }

View File

@@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.vitorpamplona.amethyst.buttons.NewChannelButton
import com.vitorpamplona.amethyst.buttons.NewNoteButton import com.vitorpamplona.amethyst.buttons.NewNoteButton
import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar
import com.vitorpamplona.amethyst.ui.navigation.AppNavigation import com.vitorpamplona.amethyst.ui.navigation.AppNavigation
@@ -49,7 +50,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
scaffoldState = scaffoldState scaffoldState = scaffoldState
) { ) {
Column(modifier = Modifier.padding(bottom = it.calculateBottomPadding())) { 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)
}
}
}
}
} }

View File

@@ -51,7 +51,9 @@ import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.pagerTabIndicatorOffset import com.google.accompanist.pager.pagerTabIndicatorOffset
import com.google.accompanist.pager.rememberPagerState import com.google.accompanist.pager.rememberPagerState
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.toNote
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowersDataSource import com.vitorpamplona.amethyst.service.NostrUserProfileFollowersDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowsDataSource import com.vitorpamplona.amethyst.service.NostrUserProfileFollowsDataSource
@@ -73,8 +75,6 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro
val accountUserState by accountViewModel.userLiveData.observeAsState() val accountUserState by accountViewModel.userLiveData.observeAsState()
val accountUser = accountUserState?.user val accountUser = accountUserState?.user
val clipboardManager = LocalClipboardManager.current
if (userId != null && account != null && accountUser != null) { if (userId != null && account != null && accountUser != null) {
DisposableEffect(account) { DisposableEffect(account) {
NostrUserProfileDataSource.loadUserProfile(userId) NostrUserProfileDataSource.loadUserProfile(userId)
@@ -138,7 +138,7 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro
MessageButton(user, navController) MessageButton(user, navController)
NPubCopyButton(clipboardManager, user) NPubCopyButton(user)
if (accountUser == user) { if (accountUser == user) {
EditButton() EditButton()
@@ -262,9 +262,10 @@ fun TabFollowers(user: User, accountViewModel: AccountViewModel, navController:
@Composable @Composable
private fun NPubCopyButton( private fun NPubCopyButton(
clipboardManager: ClipboardManager,
user: User user: User
) { ) {
val clipboardManager = LocalClipboardManager.current
Button( Button(
modifier = Modifier.padding(horizontal = 3.dp), modifier = Modifier.padding(horizontal = 3.dp),
onClick = { clipboardManager.setText(AnnotatedString(user.pubkey.toNpub())) }, onClick = { clipboardManager.setText(AnnotatedString(user.pubkey.toNpub())) },