Chat UI improvements

This commit is contained in:
Juha Heinanen
2025-04-09 12:46:01 +03:00
parent f514f19755
commit f715b24ca1
3 changed files with 148 additions and 108 deletions
@@ -6,7 +6,6 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.SystemClock import android.os.SystemClock
import android.text.format.DateUtils.isToday import android.text.format.DateUtils.isToday
import android.view.Menu
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
@@ -15,7 +14,6 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -125,7 +123,7 @@ class ChatActivity : ComponentActivity() {
val userAgent = UserAgent.ofAor(aor) val userAgent = UserAgent.ofAor(aor)
if (userAgent == null) { if (userAgent == null) {
Log.w(TAG, "MessageActivity did not find ua of $aor") Log.w(TAG, "ChatActivity did not find ua of $aor")
MainActivity.activityAor = aor MainActivity.activityAor = aor
returnResult(RESULT_CANCELED) returnResult(RESULT_CANCELED)
return return
@@ -135,7 +133,7 @@ class ChatActivity : ComponentActivity() {
chatPeer = Utils.friendlyUri(this, peerUri, userAgent.account, true) chatPeer = Utils.friendlyUri(this, peerUri, userAgent.account, true)
imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
val title = String.format(getString(R.string.chat_with), chatPeer) val title = String.format(getString(R.string.chat_with), chatPeer)
@@ -169,7 +167,6 @@ class ChatActivity : ComponentActivity() {
.fillMaxHeight() .fillMaxHeight()
.imePadding() .imePadding()
.safeDrawingPadding(), .safeDrawingPadding(),
containerColor = LocalCustomColors.current.background,
topBar = { TopAppBar(ctx, title, navigateBack) }, topBar = { TopAppBar(ctx, title, navigateBack) },
bottomBar = { NewMessage(ctx, peerUri) }, bottomBar = { NewMessage(ctx, peerUri) },
content = { contentPadding -> content = { contentPadding ->
@@ -232,8 +229,6 @@ class ChatActivity : ComponentActivity() {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.fillMaxHeight()
.background(LocalCustomColors.current.background)
.padding(contentPadding), .padding(contentPadding),
verticalArrangement = Arrangement.Bottom verticalArrangement = Arrangement.Bottom
) { ) {
@@ -287,8 +282,7 @@ class ChatActivity : ComponentActivity() {
state = lazyListState, state = lazyListState,
width = 4.dp, width = 4.dp,
color = LocalCustomColors.current.gray color = LocalCustomColors.current.gray
) ),
.background(LocalCustomColors.current.background),
reverseLayout = true, reverseLayout = true,
state = lazyListState, state = lazyListState,
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
@@ -390,31 +384,48 @@ class ChatActivity : ComponentActivity() {
else else
RoundedCornerShape(20.dp, 10.dp, 50.dp, 20.dp), RoundedCornerShape(20.dp, 10.dp, 50.dp, 20.dp),
colors = ButtonDefaults.buttonColors(containerColor = colors = ButtonDefaults.buttonColors(containerColor =
if (message.direction == MESSAGE_DOWN) if (message.direction == MESSAGE_DOWN) {
LocalCustomColors.current.secondaryLight if (BaresipService.darkTheme.value)
LocalCustomColors.current.secondaryDark
else else
LocalCustomColors.current.primaryLight), LocalCustomColors.current.secondaryLight
}
else {
if (BaresipService.darkTheme.value)
LocalCustomColors.current.primaryDark
else
LocalCustomColors.current.primaryLight
}),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight() .wrapContentHeight()
.padding(start = if (message.direction == MESSAGE_DOWN) 0.dp else 24.dp,
end = if (message.direction == MESSAGE_DOWN) 24.dp else 0.dp)
) { ) {
Column { Column {
Row { Row {
Text( val textColor =
peer, color = LocalCustomColors.current.dark, if (message.direction == MESSAGE_DOWN) {
fontSize = 12.sp if (BaresipService.darkTheme.value)
) LocalCustomColors.current.secondaryLight
else
LocalCustomColors.current.secondaryDark
}
else {
if (BaresipService.darkTheme.value)
LocalCustomColors.current.primaryLight
else
LocalCustomColors.current.primaryDark
}
Text(text = peer, fontSize = 12.sp, color = textColor)
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Text( Text(text = info, fontSize = 12.sp, color = textColor)
info, color = LocalCustomColors.current.dark,
fontSize = 12.sp
)
} }
Row { Row {
SelectionContainer { SelectionContainer {
Text( Text(
text = message.message, text = message.message,
color = LocalCustomColors.current.dark, color = LocalCustomColors.current.itemText,
fontWeight = if (message.direction == MESSAGE_DOWN && message.new) fontWeight = if (message.direction == MESSAGE_DOWN && message.new)
FontWeight.Bold else FontWeight.Normal FontWeight.Bold else FontWeight.Normal
) )
@@ -505,7 +516,7 @@ class ChatActivity : ComponentActivity() {
}, },
onLongClick = { onLongClick = {
val clipboardManager = val clipboardManager =
ctx.getSystemService(Context.CLIPBOARD_SERVICE) as ctx.getSystemService(CLIPBOARD_SERVICE) as
android.content.ClipboardManager android.content.ClipboardManager
val clipData = clipboardManager.primaryClip val clipData = clipboardManager.primaryClip
if (clipData != null && clipData.itemCount > 0) { if (clipData != null && clipData.itemCount > 0) {
@@ -597,11 +608,6 @@ class ChatActivity : ComponentActivity() {
} }
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.call_icon, menu)
return true
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
val chatText = BaresipService.chatTexts["$aor::$peerUri"] val chatText = BaresipService.chatTexts["$aor::$peerUri"]
@@ -17,7 +17,6 @@ import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -41,14 +40,13 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.outlined.Clear import androidx.compose.material.icons.outlined.Clear
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@@ -75,6 +73,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
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.compose.ui.unit.sp
import coil.compose.AsyncImage import coil.compose.AsyncImage
@@ -233,13 +232,11 @@ class ChatsActivity: ComponentActivity() {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.fillMaxHeight()
.background(LocalCustomColors.current.background)
.padding(contentPadding), .padding(contentPadding),
verticalArrangement = Arrangement.Bottom verticalArrangement = Arrangement.Top
) { ) {
Account(account) Account(account)
Spacer(modifier = Modifier.weight(1f)) // Spacer(modifier = Modifier.weight(1f))
Chats(ctx, account) Chats(ctx, account)
} }
} }
@@ -269,7 +266,7 @@ class ChatsActivity: ComponentActivity() {
modifier = Modifier modifier = Modifier
.imePadding() .imePadding()
.fillMaxWidth() .fillMaxWidth()
.padding(start = 16.dp, end = 4.dp) .padding(start = 8.dp, end = 4.dp)
.verticalScrollbar( .verticalScrollbar(
state = lazyListState, state = lazyListState,
width = 4.dp, width = 4.dp,
@@ -281,9 +278,37 @@ class ChatsActivity: ComponentActivity() {
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
) { ) {
items(items = uaMessages, key = { message -> message.timeStamp }) { message -> items(items = uaMessages, key = { message -> message.timeStamp }) { message ->
Row( Row(verticalAlignment = Alignment.CenterVertically) {
verticalAlignment = Alignment.CenterVertically, when (val contact = Contact.findContact(message.peerUri)) {
modifier = Modifier.combinedClickable( is Contact.BaresipContact -> {
val avatarImage = contact.avatarImage
if (avatarImage != null)
ImageAvatar(avatarImage)
else
TextAvatar(contact.name, contact.color)
}
is Contact.AndroidContact -> {
val thumbNailUri = contact.thumbnailUri
if (thumbNailUri != null)
AsyncImage(
model = thumbNailUri,
contentDescription = "Avatar",
contentScale = ContentScale.Crop,
modifier = Modifier
.size(36.dp)
.clip(CircleShape),
)
else
TextAvatar(contact.name, contact.color)
}
null -> {
val avatarImage = BitmapFactory
.decodeResource(ctx.resources, R.drawable.person_image)
ImageAvatar(avatarImage)
}
}
Spacer(modifier = Modifier.width(6.dp))
CustomElements.Button(
onClick = { onClick = {
val i = Intent(ctx, ChatActivity::class.java) val i = Intent(ctx, ChatActivity::class.java)
val b = Bundle() val b = Bundle()
@@ -305,16 +330,13 @@ class ChatsActivity: ComponentActivity() {
i.putExtras(b) i.putExtras(b)
startActivity(i) startActivity(i)
} }
DialogInterface.BUTTON_NEGATIVE -> { DialogInterface.BUTTON_NEGATIVE -> {
Message.deleteAorPeerMessages(aor, message.peerUri) Message.deleteAorPeerMessages(aor, message.peerUri)
} }
DialogInterface.BUTTON_NEUTRAL -> { DialogInterface.BUTTON_NEUTRAL -> {
} }
} }
} }
val builder = MaterialAlertDialogBuilder( val builder = MaterialAlertDialogBuilder(
this@ChatsActivity, this@ChatsActivity,
R.style.AlertDialogTheme R.style.AlertDialogTheme
@@ -362,62 +384,28 @@ class ChatsActivity: ComponentActivity() {
) )
show() show()
} }
} },
)) {
when (val contact = Contact.findContact(message.peerUri)) {
is Contact.BaresipContact -> {
val avatarImage = contact.avatarImage
if (avatarImage != null)
ImageAvatar(avatarImage)
else
TextAvatar(contact.name, contact.color)
}
is Contact.AndroidContact -> {
val thumbNailUri = contact.thumbnailUri
if (thumbNailUri != null)
AsyncImage(
model = thumbNailUri,
contentDescription = "Avatar",
contentScale = ContentScale.Crop,
modifier = Modifier
.size(36.dp)
.clip(CircleShape),
)
else
TextAvatar(contact.name, contact.color)
}
null -> {
val avatarImage = BitmapFactory
.decodeResource(ctx.resources, R.drawable.person_image)
ImageAvatar(avatarImage)
}
}
Spacer(modifier = Modifier.width(6.dp))
Button(
onClick = {
val i = Intent(this@ChatsActivity, ChatActivity::class.java)
val b = Bundle()
b.putString("aor", aor)
b.putString("peer", message.peerUri)
i.putExtras(b)
chatRequest.launch(i) },
shape = if (message.direction == MESSAGE_DOWN)
RoundedCornerShape(50.dp, 20.dp, 20.dp, 10.dp)
else
RoundedCornerShape(20.dp, 50.dp, 20.dp, 10.dp),
colors = ButtonDefaults.buttonColors(containerColor =
if (message.direction == MESSAGE_DOWN)
LocalCustomColors.current.secondaryLight
else
LocalCustomColors.current.primaryLight),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight() .wrapContentHeight()
.padding(start = 8.dp, end = 16.dp) .padding(end = 6.dp),
shape = if (message.direction == MESSAGE_DOWN)
RoundedCornerShape(50.dp, 20.dp, 20.dp, 10.dp)
else
RoundedCornerShape(20.dp, 10.dp, 50.dp, 20.dp),
color = //ButtonDefaults.buttonColors(containerColor =
if (message.direction == MESSAGE_DOWN) {
if (BaresipService.darkTheme.value)
LocalCustomColors.current.secondaryDark
else
LocalCustomColors.current.secondaryLight
}
else {
if (BaresipService.darkTheme.value)
LocalCustomColors.current.primaryDark
else
LocalCustomColors.current.primaryLight
},
) { ) {
val peer = Utils.friendlyUri(ctx, message.peerUri, account) val peer = Utils.friendlyUri(ctx, message.peerUri, account)
val cal = GregorianCalendar() val cal = GregorianCalendar()
@@ -429,20 +417,34 @@ class ChatsActivity: ComponentActivity() {
val info = fmt.format(cal.time) val info = fmt.format(cal.time)
Column { Column {
Row { Row {
Text( val textColor =
peer, color = LocalCustomColors.current.dark, if (message.direction == MESSAGE_DOWN) {
fontSize = 12.sp if (BaresipService.darkTheme.value)
) LocalCustomColors.current.secondaryLight
else
LocalCustomColors.current.secondaryDark
}
else {
if (BaresipService.darkTheme.value)
LocalCustomColors.current.primaryLight
else
LocalCustomColors.current.primaryDark
}
Text(text = peer, color = textColor, fontSize = 12.sp)
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Text( Text(text = info, color = textColor, fontSize = 12.sp)
info, color = LocalCustomColors.current.dark,
fontSize = 12.sp
)
} }
Row { Row {
Text( BasicText(
text = message.message, color = LocalCustomColors.current.dark, text = message.message,
maxLines = 1 maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TextStyle(
color = LocalCustomColors.current.itemText,
fontWeight = if (message.direction == MESSAGE_DOWN && message.new)
FontWeight.Bold else FontWeight.Normal,
fontSize = 16.sp
)
) )
} }
} }
@@ -666,7 +668,7 @@ class ChatsActivity: ComponentActivity() {
private fun uaMessages(aor: String) : List<Message> { private fun uaMessages(aor: String) : List<Message> {
val res = mutableListOf<Message>() val res = mutableListOf<Message>()
account.unreadMessages = false account.unreadMessages = false
for (m in BaresipService.messages) { for (m in BaresipService.messages.reversed()) {
if (m.aor != aor) continue if (m.aor != aor) continue
var found = false var found = false
for (r in res) for (r in res)
@@ -675,7 +677,7 @@ class ChatsActivity: ComponentActivity() {
break break
} }
if (!found) { if (!found) {
res.add(m) res.add(0, m)
if (m.new) if (m.new)
account.unreadMessages = true account.unreadMessages = true
} }
@@ -9,11 +9,13 @@ import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.ScrollState import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.BoxScope
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.RowScope
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@@ -26,6 +28,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
@@ -59,9 +62,11 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.SoftwareKeyboardController import androidx.compose.ui.platform.SoftwareKeyboardController
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
@@ -103,6 +108,33 @@ object CustomElements {
) )
} }
@Composable
fun Button(
onClick: () -> Unit,
onLongClick: () -> Unit,
modifier: Modifier = Modifier,
shape: Shape,
color: Color,
content: @Composable RowScope.() -> Unit
) {
Surface(
shape = shape,
color = color,
modifier = modifier.pointerInput(Unit) {
detectTapGestures(
onTap = { onClick() },
onLongPress = { onLongClick() },
) }
.then(modifier),
) {
Row(
modifier = Modifier.padding(ButtonDefaults.ContentPadding),
verticalAlignment = Alignment.CenterVertically,
content = content
)
}
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun PullToRefreshBox( fun PullToRefreshBox(