From f57a7009a3bb43a19f1d188125352cad434b5e09 Mon Sep 17 00:00:00 2001 From: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com> Date: Sat, 19 Feb 2022 17:22:52 +0400 Subject: [PATCH] chat view layout (#335) Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../java/chat/simplex/app/model/ChatModel.kt | 16 ++- .../chat/simplex/app/views/chat/ChatView.kt | 134 +++++++++++++----- .../simplex/app/views/chat/SendMsgView.kt | 50 ++++--- .../simplex/app/views/chat/item/CIMetaView.kt | 29 ++++ .../app/views/chat/item/ChatItemView.kt | 40 +++++- .../app/views/chat/item/TextItemView.kt | 71 +++++++++- 6 files changed, 280 insertions(+), 60 deletions(-) create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 79dbc612bc..8c64d423f0 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -1,8 +1,10 @@ package chat.simplex.app.model +import android.annotation.SuppressLint import androidx.compose.runtime.* import kotlinx.datetime.* import kotlinx.serialization.* +import java.time.format.DateTimeFormatter class ChatModel(val controller: ChatController) { var currentUser = mutableStateOf(null) @@ -438,7 +440,7 @@ class ChatItem ( val content: CIContent ) { val id: Long get() = meta.itemId - // val timestampText: String get() = meta.timestampText + val timestampText: String get() = meta.timestampText val isRcvNew: Boolean get() = meta.itemStatus is CIStatus.RcvNew companion object { @@ -484,7 +486,7 @@ class CIMeta ( val itemStatus: CIStatus, val createdAt: Instant ) { -// val timestampText: String get() = getTimestampText(itemTs) + val timestampText: String get() = getTimestampText(itemTs) companion object { fun getSample(id: Long, ts: Instant, text: String, status: CIStatus = CIStatus.SndNew()): CIMeta = @@ -498,8 +500,14 @@ class CIMeta ( } } -// TODO -fun getTimestampText(d: Instant): String = "" +// TODO use old api? +// TODO date for older timestamps +@SuppressLint("NewApi") +fun getTimestampText(d: Instant): String { + val dt = d.toLocalDateTime(TimeZone.currentSystemDefault()).toJavaLocalDateTime() + val formatter = DateTimeFormatter.ofPattern("HH:mm") + return dt.format(formatter) +} @Serializable sealed class CIStatus { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index cf4cea564b..72111b9d5b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -1,55 +1,87 @@ package chat.simplex.app.views.chat -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.Button -import androidx.compose.material.Text +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.material.Icon +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.navigation.NavController import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.chat.item.ChatItemView -import chat.simplex.app.views.chatlist.ChatPreviewView -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import chat.simplex.app.views.helpers.withApi +import kotlinx.datetime.Clock @Composable fun ChatView(chatModel: ChatModel, nav: NavController) { if (chatModel.chatId.value != null && chatModel.chats.count() > 0) { val chat: Chat = chatModel.chats.first { chat -> chat.chatInfo.id == chatModel.chatId.value } - Column { - ChatInfoToolbar(chat, nav) - ChatItemsList(chatModel.chatItems) - SendMsgView(sendMessage = { msg -> - GlobalScope.launch { - withContext(Dispatchers.Main) { - // show "in progress" - val cInfo = chat.chatInfo - val newItem = chatModel.controller.apiSendMessage( - type = cInfo.chatType, - id = cInfo.apiId, - mc = MsgContent.MCText(msg) - ) - // hide "in progress" - if (newItem != null) chatModel.addChatItem(cInfo, newItem.chatItem) - } - } - }) + ChatLayout(chat, chatModel.chatItems, back = { nav.popBackStack() }, sendMessage = { msg -> + withApi { + // show "in progress" + val cInfo = chat.chatInfo + val newItem = chatModel.controller.apiSendMessage( + type = cInfo.chatType, + id = cInfo.apiId, + mc = MsgContent.MCText(msg) + ) + // hide "in progress" + if (newItem != null) chatModel.addChatItem(cInfo, newItem.chatItem) + } + }) + } +} + +@Composable +fun ChatLayout( + chat: Chat, chatItems: List, + back: () -> Unit, + sendMessage: (String) -> Unit +) { + Scaffold( + topBar = { ChatInfoToolbar(chat, back) }, + bottomBar = { SendMsgView(sendMessage) } + ) { contentPadding -> + Box( + modifier = Modifier + .padding(contentPadding) + .fillMaxWidth() + .background(MaterialTheme.colors.background) + ) { + ChatItemsList(chatItems) } } } @Composable -fun ChatInfoToolbar(chat: Chat, nav: NavController) { - Row { - Button(onClick = { nav.popBackStack() }) { - Text("Back") - } - Column { +fun ChatInfoToolbar(chat: Chat, back: () -> Unit) { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.CenterStart + ) { + Icon( + Icons.Outlined.ArrowBack, + "Back", + tint = MaterialTheme.colors.primary, + modifier = Modifier + .clickable(onClick = back) + .padding(start = 16.dp) + ) + Column( + Modifier + .padding(horizontal = 40.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { Text(chat.chatInfo.displayName) Text(chat.chatInfo.fullName) } @@ -64,3 +96,37 @@ fun ChatItemsList(chatItems: List) { } } } + +@Preview +@Composable +fun PreviewChatViewLayout() { + SimpleXTheme { + val chatItems = listOf( + ChatItem.getSampleData( + 1, CIDirection.DirectSnd(), Clock.System.now(), "hello" + ), + ChatItem.getSampleData( + 2, CIDirection.DirectRcv(), Clock.System.now(), "hello" + ), + ChatItem.getSampleData( + 3, CIDirection.DirectSnd(), Clock.System.now(), "hello" + ), + ChatItem.getSampleData( + 4, CIDirection.DirectSnd(), Clock.System.now(), "hello" + ), + ChatItem.getSampleData( + 5, CIDirection.DirectRcv(), Clock.System.now(), "hello" + ) + ) + ChatLayout( + chat = Chat( + chatInfo = ChatInfo.Direct.sampleData, + chatItems = chatItems, + chatStats = Chat.ChatStats() + ), + chatItems = chatItems, + back = {}, + sendMessage = {} + ) + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt index 0573b30e7d..8c51172d82 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt @@ -1,31 +1,47 @@ package chat.simplex.app.views.chat +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.material.Button -import androidx.compose.material.Text -import androidx.compose.material.TextField +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowUpward import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import chat.simplex.app.ui.theme.SimpleXTheme @Composable -fun SendMsgView (sendMessage: (String) -> Unit) { +fun SendMsgView(sendMessage: (String) -> Unit) { var cmd by remember { mutableStateOf("") } - Row { - TextField(value = cmd, onValueChange = { cmd = it }, modifier = Modifier.height(60.dp)) - Spacer(Modifier.height(10.dp)) - Button( - onClick = { - sendMessage(cmd) - cmd = "" - }, - modifier = Modifier.width(40.dp).height(60.dp), - enabled = cmd.isNotEmpty() - ) { - Text("Go") - } + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.CenterEnd + ) { + TextField( + value = cmd, + onValueChange = { cmd = it }, + modifier = Modifier + .height(60.dp) + .fillMaxWidth() + .padding(end = 40.dp) + .background(MaterialTheme.colors.background) + ) + val tint = if (cmd.isNotEmpty()) MaterialTheme.colors.primary else Color.Gray + Icon( + Icons.Outlined.ArrowUpward, + "Send Message", + tint = tint, + modifier = Modifier + .width(40.dp) + .clickable { + sendMessage(cmd) + cmd = "" + } + ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt new file mode 100644 index 0000000000..73fa80ec01 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt @@ -0,0 +1,29 @@ +package chat.simplex.app.views.chat.item + +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import chat.simplex.app.model.CIDirection +import chat.simplex.app.model.ChatItem +import kotlinx.datetime.* + +@Composable +fun CIMetaView(chatItem: ChatItem) { + Text( + chatItem.timestampText, + color = Color.Gray, + style = MaterialTheme.typography.caption + ) +} + +@Preview +@Composable +fun PreviewCIMetaView() { + CIMetaView( + chatItem = ChatItem.getSampleData( + 1, CIDirection.DirectSnd(), Clock.System.now(), "hello" + ) + ) +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt index 1aba1845fc..9ad9f16ad4 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt @@ -1,10 +1,44 @@ package chat.simplex.app.views.chat.item -import androidx.compose.material.Text +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import chat.simplex.app.model.ChatItem +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.SimpleXTheme +import kotlinx.datetime.Clock @Composable fun ChatItemView(chatItem: ChatItem) { - Text(chatItem.content.text) + val sent = chatItem.chatDir.sent + val alignment = if (sent) Alignment.CenterEnd else Alignment.CenterStart + + Box( + modifier = Modifier + .padding(bottom = 4.dp) + .fillMaxWidth() + .padding( + start = if (sent) 60.dp else 16.dp, + end = if (sent) 16.dp else 60.dp, + ), + contentAlignment = alignment, + ) { + TextItemView(chatItem) + } +} + +@Preview +@Composable +fun PreviewChatItemView() { + SimpleXTheme { + ChatItemView( + chatItem = ChatItem.getSampleData( + 1, CIDirection.DirectSnd(), Clock.System.now(), "hello" + ) + ) + } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt index 22bbdbb005..6faafba256 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt @@ -1,10 +1,77 @@ package chat.simplex.app.views.chat.item +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable -import chat.simplex.app.model.ChatItem +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.SimpleXTheme +import kotlinx.datetime.* + +// TODO move to theme +val SentColorLight = Color(0x1E45B8FF) +val ReceivedColorLight = Color(0x1EF1F0F5) @Composable fun TextItemView(chatItem: ChatItem) { - Text("TextItemView") + val sent = chatItem.chatDir.sent + val color = if (sent) SentColorLight else ReceivedColorLight + + Surface( + shape = RoundedCornerShape(18.dp), + color = color + ) { + Box( + modifier = Modifier.padding(vertical = 6.dp, horizontal = 12.dp) + ) { + Column { + Text(text = chatItem.content.text) + CIMetaView(chatItem) + } + } + } +} + +@Preview +@Composable +fun PreviewTextItemViewSnd() { + SimpleXTheme { + TextItemView( + chatItem = ChatItem.getSampleData( + 1, CIDirection.DirectSnd(), Clock.System.now(), "hello" + ) + ) + } +} + +@Preview +@Composable +fun PreviewTextItemViewRcv() { + SimpleXTheme { + TextItemView( + chatItem = ChatItem.getSampleData( + 1, CIDirection.DirectRcv(), Clock.System.now(), "hello" + ) + ) + } +} + +@Preview +@Composable +fun PreviewTextItemViewLong() { + SimpleXTheme { + TextItemView( + chatItem = ChatItem.getSampleData( + 1, + CIDirection.DirectSnd(), + Clock.System.now(), + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + ) + ) + } }