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 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<String> = DefaultChannels.toMutableSet()) {
@ -149,6 +154,44 @@ class Account(val loggedIn: Persona, val followingChannels: MutableSet<String> =
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) {

View File

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

View File

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

View File

@ -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<Note>("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<Note>("MailBoxFeed") {
val myChannelsInfoChannel = requestNewChannel()
val myChannelsMessagesChannel = requestNewChannel()
val myChannelsCreatedbyMeChannel = requestNewChannel()
// returns the last Note of each user.
override fun feed(): List<Note> {
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 }
}
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<Note>("MailBoxFeed") {
myChannelsChannel.filter = listOf(createMyChannelsFilter()).ifEmpty { null }
myChannelsInfoChannel.filter = createLastChannelInfoFilter().ifEmpty { null }
myChannelsMessagesChannel.filter = createLastMessageOfEachChannelFilter().ifEmpty { null }
//myChannelsCreatedbyMeChannel.filter = listOf(createChannelsCreatedbyMeFilter()).ifEmpty { null }
}
}

View File

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

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

View File

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

View File

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

View File

@ -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,

View File

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

View File

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

View File

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