mobile: message update (restore #460) (#469)

This commit is contained in:
JRoberts
2022-03-25 22:26:05 +04:00
committed by GitHub
parent 897c64e0ba
commit 0b45ddfc79
24 changed files with 599 additions and 231 deletions
@@ -102,6 +102,37 @@ class ChatModel(val controller: ChatController) {
}
}
fun upsertChatItem(cInfo: ChatInfo, cItem: ChatItem): Boolean {
// update previews
val i = getChatIndex(cInfo.id)
val chat: Chat
val res: Boolean
if (i >= 0) {
chat = chats[i]
val pItem = chat.chatItems.last()
if (pItem.id == cItem.id) {
chats[i] = chat.copy(chatItems = arrayListOf(cItem))
}
res = false
} else {
addChat(Chat(chatInfo = cInfo, chatItems = arrayListOf(cItem)))
res = true
}
// update current chat
if (chatId.value == cInfo.id) {
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
if (itemIndex >= 0) {
chatItems[itemIndex] = cItem
return false
} else {
chatItems.add(cItem)
return true
}
} else {
return res
}
}
fun markChatItemsRead(cInfo: ChatInfo) {
val chatIdx = getChatIndex(cInfo.id)
// update current chat
@@ -122,42 +153,13 @@ class ChatModel(val controller: ChatController) {
}
}
//
// func upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool {
// // update previews
// var res: Bool
// if let chat = getChat(cInfo.id) {
// if let pItem = chat.chatItems.last, pItem.id == cItem.id {
// chat.chatItems = [cItem]
// }
// res = false
// } else {
// addChat(Chat(chatInfo: cInfo, chatItems: [cItem]))
// res = true
// }
// // update current chat
// if chatId == cInfo.id {
// if let i = chatItems.firstIndex(where: { $0.id == cItem.id }) {
// withAnimation(.default) {
// self.chatItems[i] = cItem
// }
// return false
// } else {
// withAnimation { chatItems.append(cItem) }
// return true
// }
// } else {
// return res
// }
// }
//
//
// func popChat(_ id: String) {
// if let i = getChatIndex(id) {
// popChat_(i)
// }
// }
//
private fun popChat_(i: Int) {
val chat = chats.removeAt(i)
chats.add(index = 0, chat)
@@ -494,11 +496,14 @@ data class ChatItem (
ts: Instant = Clock.System.now(),
text: String = "hello\nthere",
status: CIStatus = CIStatus.SndNew(),
quotedItem: CIQuote? = null
quotedItem: CIQuote? = null,
itemDeleted: Boolean = false,
itemEdited: Boolean = false,
editable: Boolean = true
) =
ChatItem(
chatDir = dir,
meta = CIMeta.getSample(id, ts, text, status),
meta = CIMeta.getSample(id, ts, text, status, itemDeleted, itemEdited, editable),
content = CIContent.SndMsgContent(msgContent = MsgContent.MCText(text)),
quotedItem = quotedItem
)
@@ -536,18 +541,27 @@ data class CIMeta (
val itemTs: Instant,
val itemText: String,
val itemStatus: CIStatus,
val createdAt: Instant
val createdAt: Instant,
val itemDeleted: Boolean,
val itemEdited: Boolean,
val editable: Boolean
) {
val timestampText: String get() = getTimestampText(itemTs)
companion object {
fun getSample(id: Long, ts: Instant, text: String, status: CIStatus = CIStatus.SndNew()): CIMeta =
fun getSample(
id: Long, ts: Instant, text: String, status: CIStatus = CIStatus.SndNew(),
itemDeleted: Boolean = false, itemEdited: Boolean = false, editable: Boolean = true
): CIMeta =
CIMeta(
itemId = id,
itemTs = ts,
itemText = text,
itemStatus = status,
createdAt = ts
createdAt = ts,
itemDeleted = itemDeleted,
itemEdited = itemEdited,
editable = editable
)
}
}
@@ -131,6 +131,20 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap
return null
}
suspend fun apiUpdateMessage(type: ChatType, id: Long, itemId: Long, mc: MsgContent): AChatItem? {
val r = sendCmd(CC.ApiUpdateMessage(type, id, itemId, mc))
if (r is CR.ChatItemUpdated) return r.chatItem
Log.e(TAG, "apiUpdateMessage bad response: ${r.responseType} ${r.details}")
return null
}
suspend fun apiDeleteMessage(type: ChatType, id: Long, itemId: Long, mode: MsgDeleteMode): AChatItem? {
val r = sendCmd(CC.ApiDeleteMessage(type, id, itemId, mode))
if (r is CR.ChatItemDeleted) return r.chatItem
Log.e(TAG, "apiDeleteMessage bad response: ${r.responseType} ${r.details}")
return null
}
suspend fun getUserSMPServers(): List<String>? {
val r = sendCmd(CC.GetUserSMPServers())
if (r is CR.UserSMPServers) return r.smpServers
@@ -303,12 +317,23 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap
ntfManager.notifyMessageReceived(cInfo, cItem)
}
}
// case let .chatItemUpdated(aChatItem):
// let cInfo = aChatItem.chatInfo
// let cItem = aChatItem.chatItem
// if chatModel.upsertChatItem(cInfo, cItem) {
// NtfManager.shared.notifyMessageReceived(cInfo, cItem)
// }
is CR.ChatItemStatusUpdated -> {
val cInfo = r.chatItem.chatInfo
val cItem = r.chatItem.chatItem
if (chatModel.upsertChatItem(cInfo, cItem)) {
ntfManager.notifyMessageReceived(cInfo, cItem)
}
}
is CR.ChatItemUpdated -> {
val cInfo = r.chatItem.chatInfo
val cItem = r.chatItem.chatItem
if (chatModel.upsertChatItem(cInfo, cItem)) {
ntfManager.notifyMessageReceived(cInfo, cItem)
}
}
is CR.ChatItemDeleted -> {
// TODO
}
else ->
Log.d(TAG , "unsupported event: ${r.responseType}")
}
@@ -336,6 +361,11 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap
}
}
enum class MsgDeleteMode(val mode: String) {
Broadcast("broadcast"),
Internal("internal");
}
// ChatCommand
sealed class CC {
class Console(val cmd: String): CC()
@@ -346,6 +376,8 @@ sealed class CC {
class ApiGetChat(val type: ChatType, val id: Long): CC()
class ApiSendMessage(val type: ChatType, val id: Long, val mc: MsgContent): CC()
class ApiSendMessageQuote(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent): CC()
class ApiUpdateMessage(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent): CC()
class ApiDeleteMessage(val type: ChatType, val id: Long, val itemId: Long, val mode: MsgDeleteMode): CC()
class GetUserSMPServers(): CC()
class SetUserSMPServers(val smpServers: List<String>): CC()
class AddContact: CC()
@@ -368,6 +400,8 @@ sealed class CC {
is ApiGetChat -> "/_get chat ${chatRef(type, id)} count=100"
is ApiSendMessage -> "/_send ${chatRef(type, id)} ${mc.cmdString}"
is ApiSendMessageQuote -> "/_send_quote ${chatRef(type, id)} $itemId ${mc.cmdString}"
is ApiUpdateMessage -> "/_update item ${chatRef(type, id)} $itemId ${mc.cmdString}"
is ApiDeleteMessage -> "/_delete item ${chatRef(type, id)} $itemId $mode"
is GetUserSMPServers -> "/smp_servers"
is SetUserSMPServers -> "/smp_servers ${smpServersStr(smpServers)}"
is AddContact -> "/connect"
@@ -391,6 +425,8 @@ sealed class CC {
is ApiGetChat -> "apiGetChat"
is ApiSendMessage -> "apiSendMessage"
is ApiSendMessageQuote -> "apiSendMessageQuote"
is ApiUpdateMessage -> "apiUpdateMessage"
is ApiDeleteMessage -> "apiDeleteMessage"
is GetUserSMPServers -> "getUserSMPServers"
is SetUserSMPServers -> "setUserSMPServers"
is AddContact -> "addContact"
@@ -474,7 +510,9 @@ sealed class CR {
@Serializable @SerialName("groupEmpty") class GroupEmpty(val group: GroupInfo): CR()
@Serializable @SerialName("userContactLinkSubscribed") class UserContactLinkSubscribed: CR()
@Serializable @SerialName("newChatItem") class NewChatItem(val chatItem: AChatItem): CR()
@Serializable @SerialName("chatItemStatusUpdated") class ChatItemStatusUpdated(val chatItem: AChatItem): CR()
@Serializable @SerialName("chatItemUpdated") class ChatItemUpdated(val chatItem: AChatItem): CR()
@Serializable @SerialName("chatItemDeleted") class ChatItemDeleted(val chatItem: AChatItem): CR()
@Serializable @SerialName("cmdOk") class CmdOk: CR()
@Serializable @SerialName("chatCmdError") class ChatCmdError(val chatError: ChatError): CR()
@Serializable @SerialName("chatError") class ChatRespError(val chatError: ChatError): CR()
@@ -512,7 +550,9 @@ sealed class CR {
is GroupEmpty -> "groupEmpty"
is UserContactLinkSubscribed -> "userContactLinkSubscribed"
is NewChatItem -> "newChatItem"
is ChatItemStatusUpdated -> "chatItemStatusUpdated"
is ChatItemUpdated -> "chatItemUpdated"
is ChatItemDeleted -> "chatItemDeleted"
is CmdOk -> "cmdOk"
is ChatCmdError -> "chatCmdError"
is ChatRespError -> "chatError"
@@ -551,7 +591,9 @@ sealed class CR {
is GroupEmpty -> json.encodeToString(group)
is UserContactLinkSubscribed -> noDetails()
is NewChatItem -> json.encodeToString(chatItem)
is ChatItemStatusUpdated -> json.encodeToString(chatItem)
is ChatItemUpdated -> json.encodeToString(chatItem)
is ChatItemDeleted -> json.encodeToString(chatItem)
is CmdOk -> noDetails()
is ChatCmdError -> chatError.string
is ChatRespError -> chatError.string
@@ -1,5 +1,6 @@
package chat.simplex.app.views
import android.annotation.SuppressLint
import android.content.res.Configuration
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.*
@@ -8,8 +9,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.*
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
@@ -40,11 +40,11 @@ fun TerminalView(chatModel: ChatModel, close: () -> Unit) {
}
@Composable
fun TerminalLayout(terminalItems: List<TerminalItem> , close: () -> Unit, sendCommand: (String) -> Unit) {
fun TerminalLayout(terminalItems: List<TerminalItem>, close: () -> Unit, sendCommand: (String) -> Unit) {
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
Scaffold(
topBar = { CloseSheetBar(close) },
bottomBar = { SendMsgView(sendCommand) },
bottomBar = { SendMsgView(msg = remember { mutableStateOf("") }, sendCommand) },
modifier = Modifier.navigationBarsWithImePadding()
) { contentPadding ->
Surface(
@@ -41,6 +41,8 @@ fun ChatView(chatModel: ChatModel) {
chatModel.chatId.value = null
} else {
val quotedItem = remember { mutableStateOf<ChatItem?>(null) }
val editingItem = remember { mutableStateOf<ChatItem?>(null) }
var msg = remember { mutableStateOf("") }
BackHandler { chatModel.chatId.value = null }
// TODO a more advanced version would mark as read only if in view
LaunchedEffect(chat.chatItems) {
@@ -57,24 +59,37 @@ fun ChatView(chatModel: ChatModel) {
}
}
}
ChatLayout(user, chat, chatModel.chatItems, quotedItem,
ChatLayout(user, chat, chatModel.chatItems, msg, quotedItem, editingItem,
back = { chatModel.chatId.value = null },
info = { ModalManager.shared.showCustomModal { close -> ChatInfoView(chatModel, close) } },
sendMessage = { msg ->
withApi {
// show "in progress"
val cInfo = chat.chatInfo
val newItem = chatModel.controller.apiSendMessage(
type = cInfo.chatType,
id = cInfo.apiId,
quotedItemId = quotedItem.value?.meta?.itemId,
mc = MsgContent.MCText(msg)
)
quotedItem.value = null
val ei = editingItem.value
if (ei != null) {
val updatedItem = chatModel.controller.apiUpdateMessage(
type = cInfo.chatType,
id = cInfo.apiId,
itemId = ei.meta.itemId,
mc = MsgContent.MCText(msg)
)
if (updatedItem != null) chatModel.upsertChatItem(cInfo, updatedItem.chatItem)
} else {
val newItem = chatModel.controller.apiSendMessage(
type = cInfo.chatType,
id = cInfo.apiId,
quotedItemId = quotedItem.value?.meta?.itemId,
mc = MsgContent.MCText(msg)
)
if (newItem != null) chatModel.addChatItem(cInfo, newItem.chatItem)
}
// hide "in progress"
if (newItem != null) chatModel.addChatItem(cInfo, newItem.chatItem)
editingItem.value = null
quotedItem.value = null
}
}
},
resetMessage = { msg.value = "" }
)
}
}
@@ -84,23 +99,27 @@ fun ChatLayout(
user: User,
chat: Chat,
chatItems: List<ChatItem>,
msg: MutableState<String>,
quotedItem: MutableState<ChatItem?>,
editingItem: MutableState<ChatItem?>,
back: () -> Unit,
info: () -> Unit,
sendMessage: (String) -> Unit
sendMessage: (String) -> Unit,
resetMessage: () -> Unit
) {
Surface(
Modifier
.fillMaxWidth()
.background(MaterialTheme.colors.background)) {
.background(MaterialTheme.colors.background)
) {
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
Scaffold(
topBar = { ChatInfoToolbar(chat, back, info) },
bottomBar = { ComposeView(quotedItem, sendMessage) },
bottomBar = { ComposeView(msg, quotedItem, editingItem, sendMessage, resetMessage) },
modifier = Modifier.navigationBarsWithImePadding()
) { contentPadding ->
Box(Modifier.padding(contentPadding)) {
ChatItemsList(user, chatItems, quotedItem)
ChatItemsList(user, chatItems, msg, quotedItem, editingItem)
}
}
}
@@ -133,14 +152,19 @@ fun ChatInfoToolbar(chat: Chat, back: () -> Unit, info: () -> Unit) {
) {
val cInfo = chat.chatInfo
ChatInfoImage(chat, size = 40.dp)
Column(Modifier.padding(start = 8.dp),
Column(
Modifier.padding(start = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(cInfo.displayName, fontWeight = FontWeight.Bold,
maxLines = 1, overflow = TextOverflow.Ellipsis)
Text(
cInfo.displayName, fontWeight = FontWeight.Bold,
maxLines = 1, overflow = TextOverflow.Ellipsis
)
if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName) {
Text(cInfo.fullName,
maxLines = 1, overflow = TextOverflow.Ellipsis)
Text(
cInfo.fullName,
maxLines = 1, overflow = TextOverflow.Ellipsis
)
}
}
}
@@ -160,7 +184,13 @@ val CIListStateSaver = run {
}
@Composable
fun ChatItemsList(user: User, chatItems: List<ChatItem>, quotedItem: MutableState<ChatItem?>) {
fun ChatItemsList(
user: User,
chatItems: List<ChatItem>,
msg: MutableState<String>,
quotedItem: MutableState<ChatItem?>,
editingItem: MutableState<ChatItem?>
) {
val listState = rememberLazyListState()
val keyboardState by getKeyboardState()
val ciListState = rememberSaveable(stateSaver = CIListStateSaver) {
@@ -171,7 +201,7 @@ fun ChatItemsList(user: User, chatItems: List<ChatItem>, quotedItem: MutableStat
val cxt = LocalContext.current
LazyColumn(state = listState) {
items(chatItems) { cItem ->
ChatItemView(user, cItem, quotedItem, cxt, uriHandler)
ChatItemView(user, cItem, msg, quotedItem, editingItem, cxt, uriHandler)
}
val len = chatItems.count()
if (len > 1 && (keyboardState != ciListState.value.keyboardState || !ciListState.value.scrolled || len != ciListState.value.itemCount)) {
@@ -217,10 +247,13 @@ fun PreviewChatLayout() {
chatStats = Chat.ChatStats()
),
chatItems = chatItems,
msg = remember { mutableStateOf("") },
quotedItem = remember { mutableStateOf(null) },
editingItem = remember { mutableStateOf(null) },
back = {},
info = {},
sendMessage = {}
sendMessage = {},
resetMessage = {}
)
}
}
@@ -1,14 +1,29 @@
package chat.simplex.app.views.chat
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.*
import chat.simplex.app.model.ChatItem
// TODO ComposeState
@Composable
fun ComposeView(quotedItem: MutableState<ChatItem?>, sendMessage: (String) -> Unit) {
fun ComposeView(
msg: MutableState<String>,
quotedItem: MutableState<ChatItem?>,
editingItem: MutableState<ChatItem?>,
sendMessage: (String) -> Unit,
resetMessage: () -> Unit
) {
Column {
QuotedItemView(quotedItem)
SendMsgView(sendMessage)
when {
quotedItem.value != null -> {
ContextItemView(quotedItem)
}
editingItem.value != null -> {
ContextItemView(editingItem, editing = editingItem.value != null, resetMessage)
}
else -> {}
}
SendMsgView(msg, sendMessage, editing = editingItem.value != null)
}
}
@@ -19,27 +19,38 @@ import chat.simplex.app.views.chat.item.*
import kotlinx.datetime.Clock
@Composable
fun QuotedItemView(quotedItem: MutableState<ChatItem?>) {
val qi = quotedItem.value
if (qi != null) {
val sent = qi.chatDir.sent
fun ContextItemView(
contextItem: MutableState<ChatItem?>,
editing: Boolean = false,
resetMessage: () -> Unit = {}
) {
val cxtItem = contextItem.value
if (cxtItem != null) {
val sent = cxtItem.chatDir.sent
Row(
Modifier.padding(top = 8.dp)
Modifier
.padding(top = 8.dp)
.background(if (sent) SentColorLight else ReceivedColorLight),
verticalAlignment = Alignment.CenterVertically
) {
Box(
Modifier.padding(start = 16.dp)
Modifier
.padding(start = 16.dp)
.padding(vertical = 12.dp)
.fillMaxWidth()
.weight(1F)
) {
QuoteText(qi)
ContextItemText(cxtItem)
}
IconButton(onClick = { quotedItem.value = null }) {
IconButton(onClick = {
contextItem.value = null
if (editing) {
resetMessage()
}
}) {
Icon(
Icons.Outlined.Close,
"Remove quote",
contentDescription = "Cancel",
tint = MaterialTheme.colors.primary,
modifier = Modifier.padding(10.dp)
)
@@ -49,14 +60,14 @@ fun QuotedItemView(quotedItem: MutableState<ChatItem?>) {
}
@Composable
private fun QuoteText(qi: ChatItem) {
val member = qi.memberDisplayName
private fun ContextItemText(cxtItem: ChatItem) {
val member = cxtItem.memberDisplayName
if (member == null) {
Text(qi.content.text, maxLines = 3)
Text(cxtItem.content.text, maxLines = 3)
} else {
val annotatedText = buildAnnotatedString {
withStyle(boldFont) { append(member) }
append(": ${qi.content.text}")
append(": ${cxtItem.content.text}")
}
Text(annotatedText, maxLines = 3)
}
@@ -64,13 +75,15 @@ private fun QuoteText(qi: ChatItem) {
@Preview
@Composable
fun PreviewTextItemViewEmoji() {
fun PreviewContextItemView() {
SimpleXTheme {
QuotedItemView(
quotedItem = remember {
mutableStateOf(ChatItem.getSampleData(
1, CIDirection.DirectRcv(), Clock.System.now(), "hello"
))
ContextItemView(
contextItem = remember {
mutableStateOf(
ChatItem.getSampleData(
1, CIDirection.DirectRcv(), Clock.System.now(), "hello"
)
)
}
)
}
@@ -9,6 +9,7 @@ import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.outlined.ArrowUpward
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@@ -24,15 +25,14 @@ import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.chat.item.*
@Composable
fun SendMsgView(sendMessage: (String) -> Unit) {
var msg by remember { mutableStateOf("") }
fun SendMsgView(msg: MutableState<String>, sendMessage: (String) -> Unit, editing: Boolean = false) {
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
var textStyle by remember { mutableStateOf(smallFont) }
BasicTextField(
value = msg,
value = msg.value,
onValueChange = {
msg = it
textStyle = if(isShortEmoji(it)) {
msg.value = it
textStyle = if (isShortEmoji(it)) {
if (it.codePoints().count() < 4) largeEmojiFont else mediumEmojiFont
} else {
smallFont
@@ -64,9 +64,9 @@ fun SendMsgView(sendMessage: (String) -> Unit) {
) {
innerTextField()
}
val color = if (msg.isNotEmpty()) MaterialTheme.colors.primary else Color.Gray
val color = if (msg.value.isNotEmpty()) MaterialTheme.colors.primary else Color.Gray
Icon(
Icons.Outlined.ArrowUpward,
if (editing) Icons.Filled.Check else Icons.Outlined.ArrowUpward,
"Send Message",
tint = Color.White,
modifier = Modifier
@@ -75,9 +75,9 @@ fun SendMsgView(sendMessage: (String) -> Unit) {
.clip(CircleShape)
.background(color)
.clickable {
if (msg.isNotEmpty()) {
sendMessage(msg)
msg = ""
if (msg.value.isNotEmpty()) {
sendMessage(msg.value)
msg.value = ""
textStyle = smallFont
}
}
@@ -98,7 +98,25 @@ fun SendMsgView(sendMessage: (String) -> Unit) {
fun PreviewSendMsgView() {
SimpleXTheme {
SendMsgView(
msg = remember { mutableStateOf("") },
sendMessage = { msg -> println(msg) }
)
}
}
@Preview(showBackground = true)
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
showBackground = true,
name = "Dark Mode"
)
@Composable
fun PreviewSendMsgViewEditing() {
SimpleXTheme {
SendMsgView(
msg = remember { mutableStateOf("") },
sendMessage = { msg -> println(msg) },
editing = true
)
}
}
@@ -1,8 +1,15 @@
package chat.simplex.app.views.chat.item
import androidx.compose.foundation.layout.*
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.model.CIDirection
import chat.simplex.app.model.ChatItem
@@ -11,11 +18,24 @@ import kotlinx.datetime.Clock
@Composable
fun CIMetaView(chatItem: ChatItem) {
Text(
chatItem.timestampText,
color = HighOrLowlight,
fontSize = 14.sp
)
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (chatItem.meta.itemEdited) {
Icon(
Icons.Filled.Edit,
modifier = Modifier.height(12.dp),
contentDescription = "Edited",
tint = HighOrLowlight,
)
}
Text(
chatItem.timestampText,
color = HighOrLowlight,
fontSize = 14.sp
)
}
}
@Preview
@@ -27,3 +47,14 @@ fun PreviewCIMetaView() {
)
)
}
@Preview
@Composable
fun PreviewCIMetaViewEdited() {
CIMetaView(
chatItem = ChatItem.getSampleData(
1, CIDirection.DirectSnd(), Clock.System.now(), "hello",
itemEdited = true
)
)
}
@@ -5,6 +5,7 @@ import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@@ -22,7 +23,15 @@ import chat.simplex.app.views.helpers.shareText
import kotlinx.datetime.Clock
@Composable
fun ChatItemView(user: User, cItem: ChatItem, quotedItem: MutableState<ChatItem?>, cxt: Context, uriHandler: UriHandler? = null) {
fun ChatItemView(
user: User,
cItem: ChatItem,
msg: MutableState<String>,
quotedItem: MutableState<ChatItem?>,
editingItem: MutableState<ChatItem?>,
cxt: Context,
uriHandler: UriHandler? = null
) {
val sent = cItem.chatDir.sent
val alignment = if (sent) Alignment.CenterEnd else Alignment.CenterStart
var showMenu by remember { mutableStateOf(false) }
@@ -44,6 +53,7 @@ fun ChatItemView(user: User, cItem: ChatItem, quotedItem: MutableState<ChatItem?
}
DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
ItemAction("Reply", Icons.Outlined.Reply, onClick = {
editingItem.value = null
quotedItem.value = cItem
showMenu = false
})
@@ -51,10 +61,18 @@ fun ChatItemView(user: User, cItem: ChatItem, quotedItem: MutableState<ChatItem?
shareText(cxt, cItem.content.text)
showMenu = false
})
ItemAction("Copy", Icons.Outlined.ContentCopy, onClick = {
ItemAction("Copy", Icons.Outlined.ContentCopy, onClick = {
copyText(cxt, cItem.content.text)
showMenu = false
})
if (cItem.chatDir.sent && cItem.meta.editable) {
ItemAction("Edit", Icons.Filled.Edit, onClick = {
quotedItem.value = null
editingItem.value = cItem
msg.value = cItem.content.text
showMenu = false
})
}
}
}
}
@@ -64,9 +82,11 @@ fun ChatItemView(user: User, cItem: ChatItem, quotedItem: MutableState<ChatItem?
private fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit) {
DropdownMenuItem(onClick) {
Row {
Text(text, modifier = Modifier
.fillMaxWidth()
.weight(1F))
Text(
text, modifier = Modifier
.fillMaxWidth()
.weight(1F)
)
Icon(icon, text, tint = HighOrLowlight)
}
}
@@ -81,7 +101,9 @@ fun PreviewChatItemView() {
ChatItem.getSampleData(
1, CIDirection.DirectSnd(), Clock.System.now(), "hello"
),
msg = remember { mutableStateOf("") },
quotedItem = remember { mutableStateOf(null) },
editingItem = remember { mutableStateOf(null) },
cxt = LocalContext.current
)
}
@@ -10,7 +10,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.*
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.model.*
@@ -48,7 +48,9 @@ fun FramedItemView(user: User, ci: ChatItem, uriHandler: UriHandler? = null) {
Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) {
if (ci.formattedText == null && isShortEmoji(ci.content.text)) {
Column(
Modifier.padding(bottom = 2.dp).fillMaxWidth(),
Modifier
.padding(bottom = 2.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
EmojiText(ci.content.text)
@@ -57,7 +59,7 @@ fun FramedItemView(user: User, ci: ChatItem, uriHandler: UriHandler? = null) {
} else {
MarkdownText(
ci.content, ci.formattedText, ci.memberDisplayName,
metaText = ci.timestampText, uriHandler = uriHandler, senderBold = true
metaText = ci.timestampText, edited = ci.meta.itemEdited, uriHandler = uriHandler, senderBold = true
)
}
}
@@ -69,14 +71,18 @@ fun FramedItemView(user: User, ci: ChatItem, uriHandler: UriHandler? = null) {
}
}
class EditedProvider: PreviewParameterProvider<Boolean> {
override val values = listOf(false, true).asSequence()
}
@Preview
@Composable
fun PreviewTextItemViewSnd() {
fun PreviewTextItemViewSnd(@PreviewParameter(EditedProvider::class) edited: Boolean) {
SimpleXTheme {
FramedItemView(
User.sampleData,
ChatItem.getSampleData(
1, CIDirection.DirectSnd(), Clock.System.now(), "hello"
1, CIDirection.DirectSnd(), Clock.System.now(), "hello", itemEdited = edited
)
)
}
@@ -84,12 +90,12 @@ fun PreviewTextItemViewSnd() {
@Preview
@Composable
fun PreviewTextItemViewRcv() {
fun PreviewTextItemViewRcv(@PreviewParameter(EditedProvider::class) edited: Boolean) {
SimpleXTheme {
FramedItemView(
User.sampleData,
ChatItem.getSampleData(
1, CIDirection.DirectRcv(), Clock.System.now(), "hello"
1, CIDirection.DirectRcv(), Clock.System.now(), "hello", itemEdited = edited
)
)
}
@@ -97,7 +103,7 @@ fun PreviewTextItemViewRcv() {
@Preview
@Composable
fun PreviewTextItemViewLong() {
fun PreviewTextItemViewLong(@PreviewParameter(EditedProvider::class) edited: Boolean) {
SimpleXTheme {
FramedItemView(
User.sampleData,
@@ -105,7 +111,8 @@ fun PreviewTextItemViewLong() {
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."
"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.",
itemEdited = edited
)
)
}
@@ -113,7 +120,7 @@ fun PreviewTextItemViewLong() {
@Preview
@Composable
fun PreviewTextItemViewQuote() {
fun PreviewTextItemViewQuote(@PreviewParameter(EditedProvider::class) edited: Boolean) {
SimpleXTheme {
FramedItemView(
User.sampleData,
@@ -122,7 +129,8 @@ fun PreviewTextItemViewQuote() {
Clock.System.now(),
"https://simplex.chat",
CIStatus.SndSent(),
quotedItem = CIQuote.getSample(1, Clock.System.now(), "hi", chatDir = CIDirection.DirectRcv())
quotedItem = CIQuote.getSample(1, Clock.System.now(), "hi", chatDir = CIDirection.DirectRcv()),
itemEdited = edited
)
)
}
@@ -130,7 +138,7 @@ fun PreviewTextItemViewQuote() {
@Preview
@Composable
fun PreviewTextItemViewEmoji() {
fun PreviewTextItemViewEmoji(@PreviewParameter(EditedProvider::class) edited: Boolean) {
SimpleXTheme {
FramedItemView(
User.sampleData,
@@ -139,7 +147,8 @@ fun PreviewTextItemViewEmoji() {
Clock.System.now(),
"👍",
CIStatus.SndSent(),
quotedItem = CIQuote.getSample(1, Clock.System.now(), "Lorem ipsum dolor sit amet", chatDir = CIDirection.DirectRcv())
quotedItem = CIQuote.getSample(1, Clock.System.now(), "Lorem ipsum dolor sit amet", chatDir = CIDirection.DirectRcv()),
itemEdited = edited
)
)
}
@@ -39,6 +39,7 @@ fun MarkdownText (
formattedText: List<FormattedText>? = null,
sender: String? = null,
metaText: String? = null,
edited: Boolean = false,
style: TextStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onSurface, lineHeight = 22.sp),
maxLines: Int = Int.MAX_VALUE,
overflow: TextOverflow = TextOverflow.Clip,
@@ -46,11 +47,12 @@ fun MarkdownText (
senderBold: Boolean = false,
modifier: Modifier = Modifier
) {
val reserve = if (edited) " " else " "
if (formattedText == null) {
val annotatedText = buildAnnotatedString {
appendSender(this, sender, senderBold)
append(content.text)
if (metaText != null) withStyle(reserveTimestampStyle) { append(" $metaText") }
if (metaText != null) withStyle(reserveTimestampStyle) { append(reserve + metaText) }
}
Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow)
} else {
@@ -71,7 +73,7 @@ fun MarkdownText (
}
}
}
if (metaText != null) withStyle(reserveTimestampStyle) { append(" $metaText") }
if (metaText != null) withStyle(reserveTimestampStyle) { append(reserve + metaText) }
}
if (hasLinks && uriHandler != null) {
ClickableText(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow,