Improvement to chat bubbles layout

This commit is contained in:
Vitor Pamplona
2023-01-17 10:18:30 -05:00
parent 0667a822f1
commit ee9422077e
6 changed files with 210 additions and 205 deletions

View File

@@ -199,8 +199,9 @@ fun CloseButton(onCancel: () -> Unit) {
} }
@Composable @Composable
fun PostButton(onPost: () -> Unit = {}, isActive: Boolean) { fun PostButton(onPost: () -> Unit = {}, isActive: Boolean, modifier: Modifier = Modifier) {
Button( Button(
modifier = modifier,
onClick = { onClick = {
if (isActive) { if (isActive) {
onPost() onPost()

View File

@@ -1,6 +1,7 @@
package com.vitorpamplona.amethyst.ui.note package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@@ -17,10 +18,12 @@ 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.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp 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.model.User
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
import nostr.postr.events.TextNoteEvent import nostr.postr.events.TextNoteEvent
@@ -42,52 +45,18 @@ 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
Column(modifier = channel?.let {
Modifier.clickable( ChannelName(
onClick = { navController.navigate("Channel/${channel?.idHex}") } channelPicture = it.profilePicture(),
) channelTitle = {
) { Text(
Row( "${it.info.name}",
modifier = Modifier fontWeight = FontWeight.Bold
.padding( )
start = 12.dp, },
end = 12.dp, channelLastTime = note.event?.createdAt,
top = 10.dp) channelLastContent = "${author?.toBestDisplayName()}: " + note.event?.content,
) { onClick = { navController.navigate("Channel/${it.idHex}") })
AsyncImage(
model = channel?.profilePicture(),
contentDescription = "Public Channel Image",
modifier = Modifier
.width(55.dp).height(55.dp)
.clip(shape = CircleShape)
)
Column(modifier = Modifier.padding(start = 10.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
"${channel?.info?.name}",
fontWeight = FontWeight.Bold,
)
Text(
timeAgo(note.event?.createdAt),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
}
val eventContent = accountViewModel.decrypt(note)
if (eventContent != null)
RichTextViewer("${author?.toBestDisplayName()}: " + eventContent.take(100), note.event?.tags, note, accountViewModel, navController)
else
RichTextViewer("Referenced event not found", note.event?.tags, note, accountViewModel, navController)
}
}
Divider(
modifier = Modifier.padding(top = 10.dp),
thickness = 0.25.dp
)
} }
} else { } else {
@@ -107,51 +76,74 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
} }
} }
Column(modifier = userToComposeOn?.let {
Modifier.clickable( ChannelName(
onClick = { navController.navigate("Room/${userToComposeOn?.pubkeyHex}") } channelPicture = it.profilePicture(),
) channelTitle = { UserDisplay(it) },
) { channelLastTime = note.event?.createdAt,
Row( channelLastContent = accountViewModel.decrypt(note),
modifier = Modifier onClick = { navController.navigate("Room/${it.pubkeyHex}") })
.padding(
start = 12.dp,
end = 12.dp,
top = 10.dp)
) {
AsyncImage(
model = userToComposeOn?.profilePicture(),
contentDescription = "Profile Image",
modifier = Modifier
.width(55.dp).height(55.dp)
.clip(shape = CircleShape)
)
Column(modifier = Modifier.padding(start = 10.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
if (userToComposeOn != null)
UserDisplay(userToComposeOn)
Text(
timeAgo(note.event?.createdAt),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
}
val eventContent = accountViewModel.decrypt(note)
if (eventContent != null)
RichTextViewer(eventContent.take(100), note.event?.tags, note, accountViewModel, navController)
else
RichTextViewer("Referenced event not found", note.event?.tags, note, accountViewModel, navController)
}
}
Divider(
modifier = Modifier.padding(top = 10.dp),
thickness = 0.25.dp
)
} }
} }
} }
@Composable
private fun ChannelName(
channelPicture: String,
channelTitle: @Composable () -> Unit,
channelLastTime: Long?,
channelLastContent: String?,
onClick: () -> Unit
) {
Column(modifier = Modifier.clickable(onClick = onClick) ) {
Row(
modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 10.dp)
) {
AsyncImage(
model = channelPicture,
contentDescription = "Profile Image",
modifier = Modifier
.width(55.dp)
.height(55.dp)
.clip(shape = CircleShape)
)
Column(modifier = Modifier.padding(start = 10.dp),
verticalArrangement = Arrangement.SpaceAround) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(bottom = 4.dp)
) {
channelTitle()
Text(
timeAgo(channelLastTime),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.52f)
)
}
if (channelLastContent != null)
Text(
channelLastContent,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.52f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
else
Text(
"Referenced event not found",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.52f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
Divider(
modifier = Modifier.padding(top = 10.dp),
thickness = 0.25.dp
)
}
}

View File

@@ -6,6 +6,7 @@ import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -30,6 +31,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
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
@@ -37,8 +39,8 @@ import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
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
val ChatBubbleShapeMe = RoundedCornerShape(20.dp, 20.dp, 3.dp, 20.dp) val ChatBubbleShapeMe = RoundedCornerShape(15.dp, 15.dp, 3.dp, 15.dp)
val ChatBubbleShapeThem = RoundedCornerShape(20.dp, 20.dp, 20.dp, 3.dp) val ChatBubbleShapeThem = RoundedCornerShape(3.dp, 15.dp, 15.dp, 15.dp)
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
@@ -57,56 +59,60 @@ fun ChatroomMessageCompose(baseNote: Note, accountViewModel: AccountViewModel, n
val authorState by note.author!!.live.observeAsState() val authorState by note.author!!.live.observeAsState()
val author = authorState?.user val author = authorState?.user
var backgroundBubbleColor: Color
var alignment: Arrangement.Horizontal
var shape: Shape
if (author == accountUser) {
backgroundBubbleColor = MaterialTheme.colors.primary.copy(alpha = 0.32f)
alignment = Arrangement.End
shape = ChatBubbleShapeMe
} else {
backgroundBubbleColor = MaterialTheme.colors.onSurface.copy(alpha = 0.12f)
alignment = Arrangement.Start
shape = ChatBubbleShapeThem
}
Column() { Column() {
var backgroundBubbleColor: Color
var alignment: Arrangement.Horizontal
var shape: Shape
if (author == accountUser) {
backgroundBubbleColor = MaterialTheme.colors.primary.copy(alpha = 0.32f)
alignment = Arrangement.End
shape = ChatBubbleShapeMe
} else {
backgroundBubbleColor = MaterialTheme.colors.onSurface.copy(alpha = 0.12f)
alignment = Arrangement.Start
shape = ChatBubbleShapeThem
}
Row( Row(
horizontalArrangement = alignment, modifier = Modifier.fillMaxWidth(1f).padding(
modifier = Modifier.fillMaxWidth() start = 12.dp,
.padding( end = 12.dp,
start = 12.dp, top = 5.dp,
end = 12.dp, bottom = 5.dp
top = 5.dp, ),
bottom = 5.dp horizontalArrangement = alignment
).combinedClickable(
onClick = { },
onLongClick = { popupExpanded = true }
)
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically horizontalArrangement = alignment,
modifier = Modifier.fillMaxWidth(0.85f)
) { ) {
Surface( Surface(
color = backgroundBubbleColor, color = backgroundBubbleColor,
shape = shape shape = shape,
modifier = Modifier
.combinedClickable(
onClick = { },
onLongClick = { popupExpanded = true }
)
) { ) {
Column( Column(
modifier = Modifier.padding(10.dp), modifier = Modifier.padding(start = 10.dp, end = 10.dp, bottom = 5.dp),
) { ) {
if (author != accountUser && note.event is ChannelMessageEvent) { if (author != accountUser && note.event is ChannelMessageEvent) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = alignment horizontalArrangement = alignment,
modifier = Modifier.padding(top = 5.dp)
) { ) {
AsyncImage( AsyncImage(
model = author?.profilePicture(), model = author?.profilePicture(),
contentDescription = "Profile Image", contentDescription = "Profile Image",
modifier = Modifier modifier = Modifier
.width(25.dp).height(25.dp) .width(25.dp)
.height(25.dp)
.clip(shape = CircleShape) .clip(shape = CircleShape)
.clickable(onClick = { .clickable(onClick = {
author?.let { author?.let {
@@ -127,9 +133,7 @@ fun ChatroomMessageCompose(baseNote: Note, accountViewModel: AccountViewModel, n
} }
} }
Row( Row(verticalAlignment = Alignment.CenterVertically) {
verticalAlignment = Alignment.CenterVertically
) {
val eventContent = accountViewModel.decrypt(note) val eventContent = accountViewModel.decrypt(note)
if (eventContent != null) if (eventContent != null)
@@ -148,24 +152,26 @@ fun ChatroomMessageCompose(baseNote: Note, accountViewModel: AccountViewModel, n
accountViewModel, accountViewModel,
navController navController
) )
} }
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = alignment horizontalArrangement = Arrangement.End,
modifier = Modifier.padding(top = 2.dp)
) { ) {
Text( Text(
timeAgoLong(note.event?.createdAt), timeAgoLong(note.event?.createdAt),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
fontSize = 12.sp
) )
} }
} }
} }
}
}
NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel) }
NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
}
} }
} }
} }

View File

@@ -18,7 +18,7 @@ fun timeAgo(mills: Long?): String {
return "" + humanReadable return "" + humanReadable
.replace(" hr. ago", "h") .replace(" hr. ago", "h")
.replace(" min. ago", "m") .replace(" min. ago", "m")
.replace(" days. ago", "d") .replace(" days ago", "d")
} }
fun timeAgoLong(mills: Long?): String { fun timeAgoLong(mills: Long?): String {

View File

@@ -15,6 +15,7 @@ import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
@@ -26,7 +27,9 @@ import androidx.compose.ui.draw.clip
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
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
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
@@ -54,13 +57,11 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, navCon
val feedViewModel: FeedViewModel = viewModel { FeedViewModel( NostrChannelDataSource ) } val feedViewModel: FeedViewModel = viewModel { FeedViewModel( NostrChannelDataSource ) }
Column(Modifier.fillMaxHeight()) { Column(Modifier.fillMaxHeight()) {
channel?.let { ChannelHeader(
ChannelHeader( channel,
it, accountViewModel = accountViewModel,
accountViewModel = accountViewModel, navController = navController
navController = navController )
)
}
Column( Column(
modifier = Modifier.fillMaxHeight().padding(vertical = 0.dp).weight(1f, true) modifier = Modifier.fillMaxHeight().padding(vertical = 0.dp).weight(1f, true)
@@ -73,7 +74,7 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, navCon
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
OutlinedTextField( TextField(
value = newPost.value, value = newPost.value,
onValueChange = { newPost.value = it }, onValueChange = { newPost.value = it },
keyboardOptions = KeyboardOptions.Default.copy( keyboardOptions = KeyboardOptions.Default.copy(
@@ -85,60 +86,63 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, navCon
text = "reply here.. ", text = "reply here.. ",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
) )
}
)
PostButton(
onPost = {
account.sendChannelMeesage(newPost.value.text, channel.idHex)
newPost.value = TextFieldValue("")
}, },
newPost.value.text.isNotBlank() trailingIcon = {
PostButton(
onPost = {
account.sendChannelMeesage(newPost.value.text, channel.idHex)
newPost.value = TextFieldValue("")
},
newPost.value.text.isNotBlank(),
modifier = Modifier.padding(end = 10.dp)
)
}
) )
} }
} }
} }
} }
@Composable @Composable
fun ChannelHeader(baseChannel: Channel, accountViewModel: AccountViewModel, navController: NavController) { fun ChannelHeader(baseChannel: Channel, accountViewModel: AccountViewModel, navController: NavController) {
val channelState by baseChannel.live.observeAsState() val channelState by baseChannel.live.observeAsState()
val channel = channelState?.channel val channel = channelState?.channel
Column(modifier = Column() {
Modifier Column(modifier = Modifier.padding(12.dp)) {
.padding(12.dp) Row(verticalAlignment = Alignment.CenterVertically) {
) {
Row(verticalAlignment = Alignment.CenterVertically) {
AsyncImage( AsyncImage(
model = channel?.profilePicture(), model = channel?.profilePicture(),
contentDescription = "Profile Image", contentDescription = "Profile Image",
modifier = Modifier modifier = Modifier
.width(35.dp).height(35.dp) .width(35.dp).height(35.dp)
.clip(shape = CircleShape) .clip(shape = CircleShape)
) )
Column(modifier = Modifier.padding(start = 10.dp)) { Column(modifier = Modifier.padding(start = 10.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text( Text(
"${channel?.info?.name}", "${channel?.info?.name}",
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
) )
} }
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
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,
overflow = TextOverflow.Ellipsis,
fontSize = 12.sp
)
}
} }
} }
} }
Divider( Divider(
modifier = Modifier.padding(top = 12.dp, start = 12.dp, end = 12.dp), modifier = Modifier.padding(start = 12.dp, end = 12.dp),
thickness = 0.25.dp thickness = 0.25.dp
) )
} }

View File

@@ -15,6 +15,7 @@ import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
@@ -67,27 +68,29 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
OutlinedTextField( TextField(
value = newPost.value, value = newPost.value,
onValueChange = { newPost.value = it }, onValueChange = { newPost.value = it },
keyboardOptions = KeyboardOptions.Default.copy( keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences capitalization = KeyboardCapitalization.Sentences
), ),
modifier = Modifier.weight(1f, true).padding(end = 10.dp), modifier = Modifier.weight(1f, true),
placeholder = { placeholder = {
Text( Text(
text = "reply here.. ", text = "reply here.. ",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
) )
}
)
PostButton(
onPost = {
account.sendPrivateMeesage(newPost.value.text, userId)
newPost.value = TextFieldValue("")
}, },
newPost.value.text.isNotBlank() trailingIcon = {
PostButton(
onPost = {
account.sendPrivateMeesage(newPost.value.text, userId)
newPost.value = TextFieldValue("")
},
newPost.value.text.isNotBlank(),
modifier = Modifier.padding(end = 10.dp)
)
}
) )
} }
} }
@@ -100,33 +103,32 @@ fun ChatroomHeader(baseUser: User, accountViewModel: AccountViewModel, navContro
val authorState by baseUser.live.observeAsState() val authorState by baseUser.live.observeAsState()
val author = authorState?.user val author = authorState?.user
Column(modifier = Column(modifier = Modifier.clickable(
Modifier onClick = { navController.navigate("User/${author?.pubkeyHex}") }
.padding(12.dp) )
.clickable(
onClick = { navController.navigate("User/${author?.pubkeyHex}") }
)
) { ) {
Row(verticalAlignment = Alignment.CenterVertically) { Column(modifier = Modifier.padding(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
AsyncImage( AsyncImage(
model = author?.profilePicture(), model = author?.profilePicture(),
contentDescription = "Profile Image", contentDescription = "Profile Image",
modifier = Modifier modifier = Modifier
.width(35.dp).height(35.dp) .width(35.dp).height(35.dp)
.clip(shape = CircleShape) .clip(shape = CircleShape)
) )
Column(modifier = Modifier.padding(start = 10.dp)) { Column(modifier = Modifier.padding(start = 10.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
if (author != null) if (author != null)
UserDisplay(author) UserDisplay(author)
}
} }
} }
} }
Divider( Divider(
modifier = Modifier.padding(top = 12.dp, start = 12.dp, end = 12.dp), modifier = Modifier.padding(start = 12.dp, end = 12.dp),
thickness = 0.25.dp thickness = 0.25.dp
) )
} }