android, desktop: forward ui (#4039)

* android, desktop: forward ui

* changes

* attachment icon

* reorder lines

* reorder lines

* change

* inline content + formatted text

* fix header padding

* fix

* fix2

* Revert "fix header padding"

This reverts commit 63f2b15f02.

* broken default parameter

* changes

* more checks for file forwarding

* paddings

* padding

* padding and fix

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
This commit is contained in:
Stanislav Dmitrenko
2024-04-19 23:36:32 +07:00
committed by GitHub
parent a35d392f76
commit f6ceb89c4b
18 changed files with 351 additions and 77 deletions
@@ -165,7 +165,7 @@ struct FramedItemView: View {
)
}
@ViewBuilder func framedItemHeader(icon: String? = nil, caption: Text, pad: Bool = true) -> some View {
@ViewBuilder func framedItemHeader(icon: String? = nil, caption: Text, pad: Bool = false) -> some View {
let v = HStack(spacing: 6) {
if let icon = icon {
Image(systemName: icon)
@@ -89,11 +89,12 @@ class MainActivity: FragmentActivity() {
}
override fun onBackPressed() {
if (
onBackPressedDispatcher.hasEnabledCallbacks() // Has something to do in a backstack
|| Build.VERSION.SDK_INT >= Build.VERSION_CODES.R // Android 11 or above
|| isTaskRoot // there are still other tasks after we reach the main (home) activity
) {
val canFinishActivity = (
onBackPressedDispatcher.hasEnabledCallbacks() // Has something to do in a backstack
|| Build.VERSION.SDK_INT >= Build.VERSION_CODES.R // Android 11 or above
|| isTaskRoot // there are still other tasks after we reach the main (home) activity
) && SimplexApp.context.chatModel.sharedContent.value !is SharedContent.Forward
if (canFinishActivity) {
// https://medium.com/mobile-app-development-publication/the-risk-of-android-strandhogg-security-issue-and-how-it-can-be-mitigated-80d2ddb4af06
super.onBackPressed()
}
@@ -104,9 +105,15 @@ class MainActivity: FragmentActivity() {
AppLock.laFailed.value = true
}
if (!onBackPressedDispatcher.hasEnabledCallbacks()) {
val sharedContent = chatModel.sharedContent.value
// Drop shared content
SimplexApp.context.chatModel.sharedContent.value = null
finish()
chatModel.sharedContent.value = null
if (sharedContent is SharedContent.Forward) {
chatModel.chatId.value = sharedContent.fromChatInfo.id
}
if (canFinishActivity) {
finish()
}
}
}
}
@@ -111,7 +111,7 @@ object ChatModel {
var draft = mutableStateOf(null as ComposeState?)
var draftChatId = mutableStateOf(null as String?)
// working with external intents
// working with external intents or internal forwarding of chat items
val sharedContent = mutableStateOf(null as SharedContent?)
val filesToDelete = mutableSetOf<File>()
@@ -1753,7 +1753,7 @@ data class ChatItem (
val allowAddReaction: Boolean get() =
meta.itemDeleted == null && !isLiveDummy && (reactions.count { it.userReacted } < 3)
private val isLiveDummy: Boolean get() = meta.itemId == TEMP_LIVE_CHAT_ITEM_ID
val isLiveDummy: Boolean get() = meta.itemId == TEMP_LIVE_CHAT_ITEM_ID
val encryptedFile: Boolean? = if (file?.fileSource == null) null else file.fileSource.cryptoArgs != null
@@ -2298,8 +2298,26 @@ enum class MsgDirection {
@Serializable
sealed class CIForwardedFrom {
@Serializable @SerialName("unknown") object Unknown: CIForwardedFrom()
@Serializable @SerialName("contact") class Contact(val chatName: String, val msgDir: MsgDirection, val contactId: Long? = null, val chatItemId: Long? = null): CIForwardedFrom()
@Serializable @SerialName("group") class Group(val chatName: String, val msgDir: MsgDirection, val groupId: Long? = null, val chatItemId: Long? = null): CIForwardedFrom()
@Serializable @SerialName("contact") class Contact(override val chatName: String, val msgDir: MsgDirection, val contactId: Long? = null, val chatItemId: Long? = null): CIForwardedFrom()
@Serializable @SerialName("group") class Group(override val chatName: String, val msgDir: MsgDirection, val groupId: Long? = null, val chatItemId: Long? = null): CIForwardedFrom()
open val chatName: String
get() = when (this) {
Unknown -> ""
is Contact -> chatName
is Group -> chatName
}
fun text(chatType: ChatType): String =
if (chatType == ChatType.Local) {
if (chatName.isEmpty()) {
generalGetString(MR.strings.saved_description)
} else {
generalGetString(MR.strings.saved_from_description).format(chatName)
}
} else {
generalGetString(MR.strings.forwarded_description)
}
}
@Serializable
@@ -738,12 +738,16 @@ object ChatController {
suspend fun apiSendMessage(rh: Long?, type: ChatType, id: Long, file: CryptoFile? = null, quotedItemId: Long? = null, mc: MsgContent, live: Boolean = false, ttl: Int? = null): AChatItem? {
val cmd = CC.ApiSendMessage(type, id, file, quotedItemId, mc, live, ttl)
return processSendMessageCmd(rh, cmd)
}
private suspend fun processSendMessageCmd(rh: Long?, cmd: CC): AChatItem? {
val r = sendCmd(rh, cmd)
return when (r) {
is CR.NewChatItem -> r.chatItem
else -> {
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiSendMessage", generalGetString(MR.strings.error_sending_message), r)
apiErrorAlert("processSendMessageCmd", generalGetString(MR.strings.error_sending_message), r)
}
null
}
@@ -771,6 +775,13 @@ object ChatController {
}
}
suspend fun apiForwardChatItem(rh: Long?, toChatType: ChatType, toChatId: Long, fromChatType: ChatType, fromChatId: Long, itemId: Long): ChatItem? {
val cmd = CC.ApiForwardChatItem(toChatType, toChatId, fromChatType, fromChatId, itemId)
return processSendMessageCmd(rh, cmd)?.chatItem
}
suspend fun apiUpdateChatItem(rh: Long?, type: ChatType, id: Long, itemId: Long, mc: MsgContent, live: Boolean = false): AChatItem? {
val r = sendCmd(rh, CC.ApiUpdateChatItem(type, id, itemId, mc, live))
if (r is CR.ChatItemUpdated) return r.chatItem
@@ -1971,7 +1982,6 @@ object ChatController {
}
is CR.SndFileCompleteXFTP -> {
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
cleanupFile(r.chatItem)
}
is CR.SndFileError -> {
if (r.chatItem_ != null) {
@@ -2396,6 +2406,7 @@ sealed class CC {
class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemId: Long, val mode: CIDeleteMode): CC()
class ApiDeleteMemberChatItem(val groupId: Long, val groupMemberId: Long, val itemId: Long): CC()
class ApiChatItemReaction(val type: ChatType, val id: Long, val itemId: Long, val add: Boolean, val reaction: MsgReaction): CC()
class ApiForwardChatItem(val toChatType: ChatType, val toChatId: Long, val fromChatType: ChatType, val fromChatId: Long, val itemId: Long): CC()
class ApiNewGroup(val userId: Long, val incognito: Boolean, val groupProfile: GroupProfile): CC()
class ApiAddMember(val groupId: Long, val contactId: Long, val memberRole: GroupMemberRole): CC()
class ApiJoinGroup(val groupId: Long): CC()
@@ -2539,6 +2550,7 @@ sealed class CC {
is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} $itemId ${mode.deleteMode}"
is ApiDeleteMemberChatItem -> "/_delete member item #$groupId $groupMemberId $itemId"
is ApiChatItemReaction -> "/_reaction ${chatRef(type, id)} $itemId ${onOff(add)} ${json.encodeToString(reaction)}"
is ApiForwardChatItem -> "/_forward ${chatRef(toChatType, toChatId)} ${chatRef(fromChatType, fromChatId)} $itemId"
is ApiNewGroup -> "/_group $userId incognito=${onOff(incognito)} ${json.encodeToString(groupProfile)}"
is ApiAddMember -> "/_add #$groupId $contactId ${memberRole.memberRole}"
is ApiJoinGroup -> "/_join #$groupId"
@@ -2677,6 +2689,7 @@ sealed class CC {
is ApiDeleteChatItem -> "apiDeleteChatItem"
is ApiDeleteMemberChatItem -> "apiDeleteMemberChatItem"
is ApiChatItemReaction -> "apiChatItemReaction"
is ApiForwardChatItem -> "apiForwardChatItem"
is ApiNewGroup -> "apiNewGroup"
is ApiAddMember -> "apiAddMember"
is ApiJoinGroup -> "apiJoinGroup"
@@ -13,9 +13,8 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.*
import androidx.compose.ui.text.AnnotatedString
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@@ -29,6 +28,7 @@ import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.common.views.chat.item.MarkdownText
import chat.simplex.common.views.helpers.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chatlist.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
@@ -36,10 +36,11 @@ sealed class CIInfoTab {
class Delivery(val memberDeliveryStatuses: List<MemberDeliveryStatus>): CIInfoTab()
object History: CIInfoTab()
class Quote(val quotedItem: CIQuote): CIInfoTab()
class Forwarded(val forwardedFromChatItem: AChatItem): CIInfoTab()
}
@Composable
fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) {
fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) {
val sent = ci.chatDir.sent
val appColors = CurrentColors.collectAsState().value.appColors
val uriHandler = LocalUriHandler.current
@@ -151,6 +152,70 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d
}
}
val local = when (ci.chatDir) {
is CIDirection.LocalSnd -> true
is CIDirection.LocalRcv -> true
else -> false
}
@Composable
fun ForwardedFromSender(forwardedFromItem: AChatItem) {
@Composable
fun ItemText(text: String, fontStyle: FontStyle = FontStyle.Normal, color: Color = MaterialTheme.colors.onBackground) {
Text(
text,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.body1,
fontStyle = fontStyle,
color = color,
)
}
Row(verticalAlignment = Alignment.CenterVertically) {
ChatInfoImage(forwardedFromItem.chatInfo, size = 57.dp)
Column(
modifier = Modifier
.padding(start = 15.dp)
.weight(1F)
) {
if (forwardedFromItem.chatItem.chatDir.sent) {
ItemText(text = stringResource(MR.strings.sender_you_pronoun), fontStyle = FontStyle.Italic)
Spacer(Modifier.height(7.dp))
ItemText(forwardedFromItem.chatInfo.chatViewName, color = MaterialTheme.colors.secondary)
} else if (forwardedFromItem.chatItem.chatDir is CIDirection.GroupRcv) {
ItemText(text = forwardedFromItem.chatItem.chatDir.groupMember.chatViewName)
Spacer(Modifier.height(7.dp))
ItemText(forwardedFromItem.chatInfo.chatViewName, color = MaterialTheme.colors.secondary)
} else {
ItemText(forwardedFromItem.chatInfo.chatViewName, color = MaterialTheme.colors.onBackground)
}
}
}
}
@Composable
fun ForwardedFromView(forwardedFromItem: AChatItem) {
Column {
SectionItemView(
click = {
withBGApi {
openChat(chatRh, forwardedFromItem.chatInfo, chatModel)
ModalManager.end.closeModals()
}
},
padding = PaddingValues(start = 17.dp, end = DEFAULT_PADDING)
) {
ForwardedFromSender(forwardedFromItem)
}
if (!local) {
Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 41.dp, end = DEFAULT_PADDING_HALF, bottom = DEFAULT_PADDING_HALF))
Text(stringResource(MR.strings.recipients_can_not_see_who_message_from), Modifier.padding(horizontal = DEFAULT_PADDING), fontSize = 12.sp, color = MaterialTheme.colors.secondary)
}
}
}
@Composable
fun Details() {
AppBarTitle(stringResource(if (ci.localNote) MR.strings.saved_message_title else if (sent) MR.strings.sent_message else MR.strings.received_message))
@@ -188,7 +253,7 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d
// LALAL SCROLLBAR DOESN'T WORK
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
Details()
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false)
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
val versions = ciInfo.itemVersions
if (versions.isNotEmpty()) {
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
@@ -213,7 +278,7 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d
// LALAL SCROLLBAR DOESN'T WORK
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
Details()
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false)
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Text(stringResource(MR.strings.in_reply_to), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING))
QuotedMsgView(qi)
@@ -222,6 +287,22 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d
}
}
@Composable
fun ForwardedFromTab(forwardedFromItem: AChatItem) {
// LALAL SCROLLBAR DOESN'T WORK
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
Details()
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
SectionView {
Text(stringResource(if (local) MR.strings.saved_from_chat_item_info_title else MR.strings.forwarded_from_chat_item_info_title),
style = MaterialTheme.typography.h2,
modifier = Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING))
ForwardedFromView(forwardedFromItem)
}
SectionBottomSpacer()
}
}
@Composable
fun MemberDeliveryStatusView(member: GroupMember, status: CIStatus) {
SectionItemView(
@@ -271,7 +352,7 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d
// LALAL SCROLLBAR DOESN'T WORK
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
Details()
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false)
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
val mss = membersStatuses(chatModel, memberDeliveryStatuses)
if (mss.isNotEmpty()) {
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
@@ -297,6 +378,7 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d
is CIInfoTab.Delivery -> stringResource(MR.strings.delivery)
is CIInfoTab.History -> stringResource(MR.strings.edit_history)
is CIInfoTab.Quote -> stringResource(MR.strings.in_reply_to)
is CIInfoTab.Forwarded -> stringResource(if (local) MR.strings.saved_chat_item_info_tab else MR.strings.forwarded_chat_item_info_tab)
}
}
@@ -305,6 +387,7 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d
is CIInfoTab.Delivery -> MR.images.ic_double_check
is CIInfoTab.History -> MR.images.ic_history
is CIInfoTab.Quote -> MR.images.ic_reply
is CIInfoTab.Forwarded -> MR.images.ic_forward
}
}
@@ -316,6 +399,9 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d
if (ci.quotedItem != null) {
numTabs += 1
}
if (ciInfo.forwardedFromChatItem != null) {
numTabs += 1
}
return numTabs
}
@@ -326,11 +412,6 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d
.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween
) {
LaunchedEffect(ciInfo) {
if (ciInfo.memberDeliveryStatuses != null) {
selection.value = CIInfoTab.Delivery(ciInfo.memberDeliveryStatuses)
}
}
Column(Modifier.weight(1f)) {
when (val sel = selection.value) {
is CIInfoTab.Delivery -> {
@@ -344,6 +425,10 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d
is CIInfoTab.Quote -> {
QuoteTab(sel.quotedItem)
}
is CIInfoTab.Forwarded -> {
ForwardedFromTab(sel.forwardedFromChatItem)
}
}
}
val availableTabs = mutableListOf<CIInfoTab>()
@@ -354,6 +439,19 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d
if (ci.quotedItem != null) {
availableTabs.add(CIInfoTab.Quote(ci.quotedItem))
}
if (ciInfo.forwardedFromChatItem != null) {
availableTabs.add(CIInfoTab.Forwarded(ciInfo.forwardedFromChatItem))
}
if (availableTabs.none { it.javaClass == selection.value.javaClass }) {
selection.value = availableTabs.first()
}
LaunchedEffect(ciInfo) {
if (ciInfo.forwardedFromChatItem != null && selection.value is CIInfoTab.Forwarded) {
selection.value = CIInfoTab.Forwarded(ciInfo.forwardedFromChatItem)
} else if (ciInfo.memberDeliveryStatuses != null) {
selection.value = CIInfoTab.Delivery(ciInfo.memberDeliveryStatuses)
}
}
TabRow(
selectedTabIndex = availableTabs.indexOfFirst { it::class == selection.value::class },
backgroundColor = Color.Transparent,
@@ -52,9 +52,11 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
val user = chatModel.currentUser.value
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
val composeState = rememberSaveable(saver = ComposeState.saver()) {
val draft = chatModel.draft.value
val sharedContent = chatModel.sharedContent.value
mutableStateOf(
if (chatModel.draftChatId.value == chatId && chatModel.draft.value != null) {
chatModel.draft.value ?: ComposeState(useLinkPreviews = useLinkPreviews)
if (chatModel.draftChatId.value == chatId && draft != null && (sharedContent !is SharedContent.Forward || sharedContent.fromChatInfo.id == chatId)) {
draft
} else {
ComposeState(useLinkPreviews = useLinkPreviews)
}
@@ -408,7 +410,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
clipboard.shareText(itemInfoShareText(chatModel, cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get()))
}
}) { close ->
ChatItemInfoView(chatModel, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
ChatItemInfoView(chatRh, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
KeyChangeEffect(chatModel.chatId.value) {
close()
}
@@ -956,7 +958,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
tryOrShowError("${cItem.id}ChatItem", error = {
CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart)
}) {
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools)
ChatItemView(chat.remoteHostId, chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools)
}
}
@@ -46,6 +46,7 @@ sealed class ComposeContextItem {
@Serializable object NoContextItem: ComposeContextItem()
@Serializable class QuotedItem(val chatItem: ChatItem): ComposeContextItem()
@Serializable class EditingItem(val chatItem: ChatItem): ComposeContextItem()
@Serializable class ForwardingItem(val chatItem: ChatItem, val fromChatInfo: ChatInfo): ComposeContextItem()
}
@Serializable
@@ -79,13 +80,18 @@ data class ComposeState(
is ComposeContextItem.EditingItem -> true
else -> false
}
val forwarding: Boolean
get() = when (contextItem) {
is ComposeContextItem.ForwardingItem -> true
else -> false
}
val sendEnabled: () -> Boolean
get() = {
val hasContent = when (preview) {
is ComposePreview.MediaPreview -> true
is ComposePreview.VoicePreview -> true
is ComposePreview.FilePreview -> true
else -> message.isNotEmpty() || liveMessage != null
else -> message.isNotEmpty() || forwarding || liveMessage != null
}
hasContent && !inProgress
}
@@ -109,7 +115,7 @@ data class ComposeState(
val attachmentDisabled: Boolean
get() {
if (editing || liveMessage != null || inProgress) return true
if (editing || forwarding || liveMessage != null || inProgress) return true
return when (preview) {
ComposePreview.NoPreview -> false
is ComposePreview.CLinkPreview -> false
@@ -355,6 +361,7 @@ fun ComposeView(
is SharedContent.Media -> shared.uris.map { it.toString() }
is SharedContent.File -> listOf(shared.uri.toString())
is SharedContent.Text -> emptyList()
is SharedContent.Forward -> emptyList()
}
// When sharing a file and pasting it in SimpleX itself, the file shouldn't be deleted before sending or before leaving the chat after sharing
chatModel.filesToDelete.removeAll { file ->
@@ -401,6 +408,21 @@ fun ComposeView(
composeState.value = composeState.value.copy(inProgress = true)
}
suspend fun forwardItem(rhId: Long?, forwardedItem: ChatItem, fromChatInfo: ChatInfo): ChatItem? {
val chatItem = controller.apiForwardChatItem(
rh = rhId,
toChatType = chat.chatInfo.chatType,
toChatId = chat.chatInfo.apiId,
fromChatType = fromChatInfo.chatType,
fromChatId = fromChatInfo.apiId,
itemId = forwardedItem.id
)
if (chatItem != null) {
chatModel.addChatItem(rhId, chat.chatInfo, chatItem)
}
return chatItem
}
fun checkLinkPreview(): MsgContent {
return when (val composePreview = cs.preview) {
is ComposePreview.CLinkPreview -> {
@@ -460,11 +482,18 @@ fun ComposeView(
if (liveMessage != null) composeState.value = cs.copy(liveMessage = null)
sending()
}
clearCurrentDraft()
if (!cs.forwarding || chatModel.draft.value?.forwarding == true) {
clearCurrentDraft()
}
if (chat.nextSendGrpInv) {
sendMemberContactInvitation()
sent = null
} else if (cs.contextItem is ComposeContextItem.ForwardingItem) {
sent = forwardItem(chat.remoteHostId, cs.contextItem.chatItem, cs.contextItem.fromChatInfo)
if (cs.message.isNotEmpty()) {
sent = send(chat, checkLinkPreview(), quoted = sent?.id, live = false, ttl = null)
}
} else if (cs.contextItem is ComposeContextItem.EditingItem) {
val ei = cs.contextItem.chatItem
sent = updateMessage(ei, chat, live)
@@ -563,7 +592,15 @@ fun ComposeView(
sent = send(chat, MsgContent.MCText(msgText), quotedItemId, null, live, ttl)
}
}
val wasForwarding = cs.forwarding
val forwardingFromChatId = (cs.contextItem as? ComposeContextItem.ForwardingItem)?.fromChatInfo?.id
clearState(live)
val draft = chatModel.draft.value
if (wasForwarding && chatModel.draftChatId.value == chat.chatInfo.id && forwardingFromChatId != chat.chatInfo.id && draft != null) {
composeState.value = draft
} else {
clearCurrentDraft()
}
return sent
}
@@ -745,6 +782,9 @@ fun ComposeView(
is ComposeContextItem.EditingItem -> ContextItemView(contextItem.chatItem, painterResource(MR.images.ic_edit_filled)) {
clearState()
}
is ComposeContextItem.ForwardingItem -> ContextItemView(contextItem.chatItem, painterResource(MR.images.ic_forward), showSender = false) {
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.NoContextItem)
}
}
}
@@ -764,6 +804,10 @@ fun ComposeView(
is SharedContent.Text -> onMessageChange(shared.text)
is SharedContent.Media -> composeState.processPickedMedia(shared.uris, shared.text)
is SharedContent.File -> composeState.processPickedFile(shared.uri, shared.text)
is SharedContent.Forward -> composeState.value = composeState.value.copy(
contextItem = ComposeContextItem.ForwardingItem(shared.chatItem, shared.fromChatInfo),
preview = if (composeState.value.preview is ComposePreview.CLinkPreview) composeState.value.preview else ComposePreview.NoPreview
)
null -> {}
}
chatModel.sharedContent.value = null
@@ -903,6 +947,17 @@ fun ComposeView(
chatModel.removeLiveDummy()
CIFile.cachedRemoteFileRequests.clear()
}
if (appPlatform.isDesktop) {
// Don't enable this on Android, it breaks it, This method only works on desktop. For Android there is a `KeyChangeEffect(chatModel.chatId.value)`
DisposableEffect(Unit) {
onDispose {
if (chatModel.sharedContent.value is SharedContent.Forward && saveLastDraft && !composeState.value.empty) {
chatModel.draft.value = composeState.value
chatModel.draftChatId.value = chat.id
}
}
}
}
val timedMessageAllowed = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.TimedMessages) }
val sendButtonColor =
@@ -4,26 +4,29 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.ui.text.TextStyle
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.runtime.*
import androidx.compose.ui.text.*
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.item.*
import chat.simplex.common.model.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
import kotlinx.datetime.Clock
@Composable
fun ContextItemView(
contextItem: ChatItem,
contextIcon: Painter,
showSender: Boolean = true,
cancelContextItem: () -> Unit
) {
val sent = contextItem.chatDir.sent
@@ -31,16 +34,47 @@ fun ContextItemView(
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
@Composable
fun msgContentView(lines: Int) {
fun MessageText(attachment: ImageResource?, lines: Int) {
val inlineContent: Pair<AnnotatedString.Builder.() -> Unit, Map<String, InlineTextContent>>? = if (attachment != null) {
remember(contextItem.id) {
val inlineContentBuilder: AnnotatedString.Builder.() -> Unit = {
appendInlineContent(id = "attachmentIcon")
append(" ")
}
val inlineContent = mapOf(
"attachmentIcon" to InlineTextContent(
Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter)
) {
Icon(painterResource(attachment), null, tint = MaterialTheme.colors.secondary)
}
)
inlineContentBuilder to inlineContent
}
} else null
MarkdownText(
contextItem.text, contextItem.formattedText,
sender = null,
toggleSecrets = false,
maxLines = lines,
inlineContent = inlineContent,
linkMode = SimplexLinkMode.DESCRIPTION,
modifier = Modifier.fillMaxWidth(),
)
}
fun attachment(): ImageResource? =
when (contextItem.content.msgContent) {
is MsgContent.MCFile -> MR.images.ic_draft_filled
is MsgContent.MCImage -> MR.images.ic_image
is MsgContent.MCVoice -> MR.images.ic_play_arrow_filled
else -> null
}
@Composable
fun ContextMsgPreview(lines: Int) {
MessageText(remember(contextItem.id) { attachment() }, lines)
}
Row(
Modifier
.padding(top = 8.dp)
@@ -64,7 +98,7 @@ fun ContextItemView(
tint = MaterialTheme.colors.secondary,
)
val sender = contextItem.memberDisplayName
if (sender != null) {
if (showSender && sender != null) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(4.dp),
@@ -73,10 +107,10 @@ fun ContextItemView(
sender,
style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary)
)
msgContentView(lines = 2)
ContextMsgPreview(lines = 2)
}
} else {
msgContentView(lines = 3)
ContextMsgPreview(lines = 3)
}
}
IconButton(onClick = cancelContextItem) {
@@ -72,7 +72,7 @@ fun SendMsgView(
}
}
val showVoiceButton = !nextSendGrpInv && cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing &&
cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started)
!composeState.value.forwarding && cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started)
val showDeleteTextButton = rememberSaveable { mutableStateOf(false) }
val sendMsgButtonDisabled = !sendMsgEnabled || !cs.sendEnabled() ||
(!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) ||
@@ -157,7 +157,7 @@ fun SendMsgView(
fun MenuItems(): List<@Composable () -> Unit> {
val menuItems = mutableListOf<@Composable () -> Unit>()
if (cs.liveMessage == null && !cs.editing && !nextSendGrpInv || sendMsgEnabled) {
if (cs.liveMessage == null && !cs.editing && !cs.forwarding && !nextSendGrpInv || sendMsgEnabled) {
if (
cs.preview !is ComposePreview.VoicePreview &&
cs.contextItem is ComposeContextItem.NoContextItem &&
@@ -95,8 +95,8 @@ private fun featureInfo(ci: ChatItem, chatInfo: ChatInfo): FeatureInfo? =
when (ci.content) {
is CIContent.RcvChatFeature -> ci.content.feature.toFeatureInfo(ci.content.enabled.iconColor, ci.content.param, ci.content.feature.name)
is CIContent.SndChatFeature -> ci.content.feature.toFeatureInfo(ci.content.enabled.iconColor, ci.content.param, ci.content.feature.name)
is CIContent.RcvGroupFeature -> ci.content.groupFeature.toFeatureInfo(ci.content.preference.enabled(ci.content.memberRole_, (chatInfo as ChatInfo.Group).groupInfo.membership).iconColor, ci.content.param, ci.content.groupFeature.name)
is CIContent.SndGroupFeature -> ci.content.groupFeature.toFeatureInfo(ci.content.preference.enabled(ci.content.memberRole_, (chatInfo as ChatInfo.Group).groupInfo.membership).iconColor, ci.content.param, ci.content.groupFeature.name)
is CIContent.RcvGroupFeature -> ci.content.groupFeature.toFeatureInfo(ci.content.preference.enabled(ci.content.memberRole_, (chatInfo as? ChatInfo.Group)?.groupInfo?.membership).iconColor, ci.content.param, ci.content.groupFeature.name)
is CIContent.SndGroupFeature -> ci.content.groupFeature.toFeatureInfo(ci.content.preference.enabled(ci.content.memberRole_, (chatInfo as? ChatInfo.Group)?.groupInfo?.membership).iconColor, ci.content.param, ci.content.groupFeature.name)
else -> null
}
@@ -59,18 +59,11 @@ fun CIFileView(
}
}
fun fileSizeValid(): Boolean {
if (file != null) {
return file.fileSize <= getMaxFileSize(file.fileProtocol)
}
return false
}
fun fileAction() {
if (file != null) {
when {
file.fileStatus is CIFileStatus.RcvInvitation -> {
if (fileSizeValid()) {
if (fileSizeValid(file)) {
receiveFile(file.fileId)
} else {
AlertManager.shared.showAlertMsg(
@@ -165,7 +158,7 @@ fun CIFileView(
is CIFileStatus.SndCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
is CIFileStatus.SndError -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
is CIFileStatus.RcvInvitation ->
if (fileSizeValid())
if (fileSizeValid(file))
fileIcon(innerIcon = painterResource(MR.images.ic_arrow_downward), color = MaterialTheme.colors.primary)
else
fileIcon(innerIcon = painterResource(MR.images.ic_priority_high), color = WarningOrange)
@@ -216,6 +209,8 @@ fun CIFileView(
}
}
fun fileSizeValid(file: CIFile): Boolean = file.fileSize <= getMaxFileSize(file.fileProtocol)
@Composable
fun rememberSaveFileLauncher(ciFile: CIFile?): FileChooserLauncher =
rememberFileChooserLauncher(false, ciFile) { to: URI? ->
@@ -19,6 +19,7 @@ import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.*
@@ -40,6 +41,7 @@ fun chatEventText(eventText: String, ts: String): AnnotatedString =
@Composable
fun ChatItemView(
rhId: Long?,
cInfo: ChatInfo,
cItem: ChatItem,
composeState: MutableState<ComposeState>,
@@ -195,12 +197,16 @@ fun ChatItemView(
}
val clipboard = LocalClipboardManager.current
val cachedRemoteReqs = remember { CIFile.cachedRemoteFileRequests }
val copyAndShareAllowed = when {
cItem.content.text.isNotEmpty() -> true
fun fileForwardingAllowed() = when {
cItem.file != null && chatModel.connectedToRemote() && cachedRemoteReqs[cItem.file.fileSource] != false && cItem.file.loaded -> true
getLoadedFilePath(cItem.file) != null -> true
else -> false
}
val copyAndShareAllowed = when {
cItem.content.text.isNotEmpty() -> true
fileForwardingAllowed() -> true
else -> false
}
if (copyAndShareAllowed) {
ItemAction(stringResource(MR.strings.share_verb), painterResource(MR.images.ic_share), onClick = {
@@ -227,8 +233,19 @@ fun ChatItemView(
showMenu.value = false
})
}
if ((cItem.content.msgContent is MsgContent.MCImage || cItem.content.msgContent is MsgContent.MCVideo || cItem.content.msgContent is MsgContent.MCFile || cItem.content.msgContent is MsgContent.MCVoice) && (getLoadedFilePath(cItem.file) != null || (chatModel.connectedToRemote() && cachedRemoteReqs[cItem.file?.fileSource] != false && cItem.file?.loaded == true))) {
if (cItem.file != null && (getLoadedFilePath(cItem.file) != null || (chatModel.connectedToRemote() && cachedRemoteReqs[cItem.file.fileSource] != false && cItem.file.loaded))) {
SaveContentItemAction(cItem, saveFileLauncher, showMenu)
} else if (cItem.file != null && cItem.file.fileStatus is CIFileStatus.RcvInvitation && fileSizeValid(cItem.file)) {
ItemAction(stringResource(MR.strings.download_file), painterResource(MR.images.ic_arrow_downward), onClick = {
withBGApi {
Log.d(TAG, "ChatItemView downloadFileAction")
val user = chatModel.currentUser.value
if (user != null) {
controller.receiveFile(rhId, user, cItem.file.fileId)
}
}
showMenu.value = false
})
}
if (cItem.meta.editable && cItem.content.msgContent !is MsgContent.MCVoice && !live) {
ItemAction(stringResource(MR.strings.edit_verb), painterResource(MR.images.ic_edit_filled), onClick = {
@@ -236,6 +253,16 @@ fun ChatItemView(
showMenu.value = false
})
}
if (cItem.meta.itemDeleted == null &&
(cItem.file == null || fileForwardingAllowed()) &&
!cItem.isLiveDummy && !live
) {
ItemAction(stringResource(MR.strings.forward_chat_item), painterResource(MR.images.ic_forward), onClick = {
chatModel.chatId.value = null
chatModel.sharedContent.value = SharedContent.Forward(cItem, cInfo)
showMenu.value = false
})
}
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
if (revealed.value) {
HideItemAction(revealed, showMenu)
@@ -458,11 +485,11 @@ fun ChatItemView(
MsgContentItemDropdownMenu()
}
is CIContent.RcvGroupFeature -> {
CIChatFeatureView(cInfo, cItem, c.groupFeature, c.preference.enabled(c.memberRole_, (cInfo as ChatInfo.Group).groupInfo.membership).iconColor, revealed = revealed, showMenu = showMenu)
CIChatFeatureView(cInfo, cItem, c.groupFeature, c.preference.enabled(c.memberRole_, (cInfo as? ChatInfo.Group)?.groupInfo?.membership).iconColor, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.SndGroupFeature -> {
CIChatFeatureView(cInfo, cItem, c.groupFeature, c.preference.enabled(c.memberRole_, (cInfo as ChatInfo.Group).groupInfo.membership).iconColor, revealed = revealed, showMenu = showMenu)
CIChatFeatureView(cInfo, cItem, c.groupFeature, c.preference.enabled(c.memberRole_, (cInfo as? ChatInfo.Group)?.groupInfo?.membership).iconColor, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.RcvChatFeatureRejected -> {
@@ -782,6 +809,7 @@ expect fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager)
fun PreviewChatItemView() {
SimpleXTheme {
ChatItemView(
rhId = null,
ChatInfo.Direct.sampleData,
ChatItem.getSampleData(
1, CIDirection.DirectSnd(), Clock.System.now(), "hello"
@@ -818,6 +846,7 @@ fun PreviewChatItemView() {
fun PreviewChatItemViewDeletedContent() {
SimpleXTheme {
ChatItemView(
rhId = null,
ChatInfo.Direct.sampleData,
ChatItem.getDeletedContentSampleData(),
useLinkPreviews = true,
@@ -87,14 +87,14 @@ fun FramedItemView(
}
@Composable
fun FramedItemHeader(caption: String, italic: Boolean, icon: Painter? = null) {
fun FramedItemHeader(caption: String, italic: Boolean, icon: Painter? = null, pad: Boolean = false) {
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
Row(
Modifier
.background(if (sent) sentColor.toQuote() else receivedColor.toQuote())
.fillMaxWidth()
.padding(start = 8.dp, top = 6.dp, end = 12.dp, bottom = if (ci.quotedItem == null) 6.dp else 0.dp),
.padding(start = 8.dp, top = 6.dp, end = 12.dp, bottom = if (pad || (ci.quotedItem == null && ci.meta.itemForwarded == null)) 6.dp else 0.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
@@ -223,7 +223,7 @@ fun FramedItemView(
if (ci.quotedItem != null) {
ciQuoteView(ci.quotedItem)
} else if (ci.meta.itemForwarded != null) {
FramedItemHeader(stringResource(MR.strings.forwarded_description), true, painterResource(MR.images.ic_forward))
FramedItemHeader(ci.meta.itemForwarded.text(chatInfo.chatType), true, painterResource(MR.images.ic_forward), pad = true)
}
if (ci.file == null && ci.formattedText == null && !ci.meta.isLive && isShortEmoji(ci.content.text)) {
Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) {
@@ -68,7 +68,7 @@ fun MarkdownText (
senderBold: Boolean = false,
modifier: Modifier = Modifier,
linkMode: SimplexLinkMode,
inlineContent: Map<String, InlineTextContent>? = null,
inlineContent: Pair<AnnotatedString.Builder.() -> Unit, Map<String, InlineTextContent>>? = null,
onLinkLongClick: (link: String) -> Unit = {}
) {
val textLayoutDirection = remember (text) {
@@ -119,6 +119,7 @@ fun MarkdownText (
}
if (formattedText == null) {
val annotatedText = buildAnnotatedString {
inlineContent?.first?.invoke(this)
appendSender(this, sender, senderBold)
if (text is String) append(text)
else if (text is AnnotatedString) append(text)
@@ -127,10 +128,11 @@ fun MarkdownText (
}
if (meta != null) withStyle(reserveTimestampStyle) { append(reserve) }
}
Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow, inlineContent = inlineContent ?: mapOf())
Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow, inlineContent = inlineContent?.second ?: mapOf())
} else {
var hasAnnotations = false
val annotatedText = buildAnnotatedString {
inlineContent?.first?.invoke(this)
appendSender(this, sender, senderBold)
for ((i, ft) in formattedText.withIndex()) {
if (ft.format == null) append(ft.text)
@@ -210,7 +212,7 @@ fun MarkdownText (
}
)
} else {
Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow)
Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow, inlineContent = inlineContent?.second ?: mapOf())
}
}
}
@@ -90,7 +90,7 @@ fun ChatPreviewView(
Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(19.dp).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary)
}
fun messageDraft(draft: ComposeState): Pair<AnnotatedString, Map<String, InlineTextContent>> {
fun messageDraft(draft: ComposeState): Pair<AnnotatedString.Builder.() -> Unit, Map<String, InlineTextContent>> {
fun attachment(): Pair<ImageResource, String?>? =
when (draft.preview) {
is ComposePreview.FilePreview -> MR.images.ic_draft_filled to draft.preview.fileName
@@ -100,7 +100,7 @@ fun ChatPreviewView(
}
val attachment = attachment()
val text = buildAnnotatedString {
val inlineContentBuilder: AnnotatedString.Builder.() -> Unit = {
appendInlineContent(id = "editIcon")
append(" ")
if (attachment != null) {
@@ -110,7 +110,6 @@ fun ChatPreviewView(
}
append(" ")
}
append(draft.message)
}
val inlineContent: Map<String, InlineTextContent> = mapOf(
"editIcon" to InlineTextContent(
@@ -124,7 +123,7 @@ fun ChatPreviewView(
Icon(if (attachment?.first != null) painterResource(attachment.first) else painterResource(MR.images.ic_edit_note), null, tint = MaterialTheme.colors.secondary)
}
)
return text to inlineContent
return inlineContentBuilder to inlineContent
}
@Composable
@@ -169,7 +168,7 @@ fun ChatPreviewView(
if (ci != null) {
if (showChatPreviews || (chatModelDraftChatId == chat.id && chatModelDraft != null)) {
val (text: CharSequence, inlineTextContent) = when {
chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { messageDraft(chatModelDraft) }
chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { chatModelDraft.message to messageDraft(chatModelDraft) }
ci.meta.itemDeleted == null -> ci.text to null
else -> markedDeletedText(ci.meta) to null
}
@@ -13,9 +13,8 @@ import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import chat.simplex.common.SettingsViewState
import chat.simplex.common.model.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.Chat
import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.*
import chat.simplex.res.MR
import kotlinx.coroutines.flow.MutableStateFlow
@@ -74,7 +73,7 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState
val navButton: @Composable RowScope.() -> Unit = {
when {
showSearch -> NavigationButtonBack(hideSearchOnBack)
users.size > 1 || chatModel.remoteHosts.isNotEmpty() -> {
(users.size > 1 || chatModel.remoteHosts.isNotEmpty()) && remember { chatModel.sharedContent }.value !is SharedContent.Forward -> {
val allRead = users
.filter { u -> !u.user.activeUser && !u.user.hidden }
.all { u -> u.unreadCount == 0 }
@@ -82,7 +81,14 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState
userPickerState.value = AnimatedViewState.VISIBLE
}
}
else -> NavigationButtonBack(onButtonClicked = { chatModel.sharedContent.value = null })
else -> NavigationButtonBack(onButtonClicked = {
val sharedContent = chatModel.sharedContent.value
// Drop shared content
chatModel.sharedContent.value = null
if (sharedContent is SharedContent.Forward) {
chatModel.chatId.value = sharedContent.fromChatInfo.id
}
})
}
}
if (chatModel.chats.size >= 8) {
@@ -118,7 +124,8 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState
is SharedContent.Text -> stringResource(MR.strings.share_message)
is SharedContent.Media -> stringResource(MR.strings.share_image)
is SharedContent.File -> stringResource(MR.strings.share_file)
else -> stringResource(MR.strings.share_message)
is SharedContent.Forward -> stringResource(MR.strings.forward_message)
null -> stringResource(MR.strings.share_message)
},
color = MaterialTheme.colors.onBackground,
fontWeight = FontWeight.SemiBold,
@@ -135,12 +142,14 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState
@Composable
private fun ShareList(chatModel: ChatModel, search: String) {
val filter: (Chat) -> Boolean = { chat: Chat ->
chat.chatInfo.chatViewName.lowercase().contains(search.lowercase())
}
val chats by remember(search) {
derivedStateOf {
if (search.isEmpty()) chatModel.chats.toList().filter { it.chatInfo.ready } else chatModel.chats.toList().filter { it.chatInfo.ready }.filter(filter)
val sorted = chatModel.chats.toList().sortedByDescending { it.chatInfo is ChatInfo.Local }
if (search.isEmpty()) {
sorted.filter { it.chatInfo.ready }
} else {
sorted.filter { it.chatInfo.ready && it.chatInfo.chatViewName.lowercase().contains(search.lowercase()) }
}
}
}
LazyColumnWithScrollBar(
@@ -2,6 +2,8 @@
package chat.simplex.common.views.helpers
import androidx.compose.runtime.saveable.Saver
import chat.simplex.common.model.ChatInfo
import chat.simplex.common.model.ChatItem
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
@@ -13,6 +15,7 @@ sealed class SharedContent {
data class Text(val text: String): SharedContent()
data class Media(val text: String, val uris: List<URI>): SharedContent()
data class File(val text: String, val uri: URI): SharedContent()
data class Forward(val chatItem: ChatItem, val fromChatInfo: ChatInfo): SharedContent()
}
enum class AnimatedViewState {
@@ -47,6 +47,8 @@
<string name="live">LIVE</string>
<string name="moderated_description">moderated</string>
<string name="forwarded_description">forwarded</string>
<string name="saved_description">saved</string>
<string name="saved_from_description">saved from %s</string>
<string name="invalid_chat">invalid chat</string>
<string name="invalid_data">invalid data</string>
<string name="error_showing_message">error showing message</string>
@@ -268,6 +270,11 @@
<string name="edit_history">History</string>
<string name="no_history">No history</string>
<string name="in_reply_to">In reply to</string>
<string name="saved_chat_item_info_tab">Saved</string>
<string name="forwarded_chat_item_info_tab">Forwarded</string>
<string name="saved_from_chat_item_info_title">Saved from</string>
<string name="forwarded_from_chat_item_info_title">Forwarded from</string>
<string name="recipients_can_not_see_who_message_from">Recipient(s) can\'t see who this message is from.</string>
<string name="delivery">Delivery</string>
<string name="no_info_on_delivery">No delivery information</string>
<string name="delete_verb">Delete</string>
@@ -295,6 +302,8 @@
<string name="revoke_file__title">Revoke file?</string>
<string name="revoke_file__message">File will be deleted from servers.</string>
<string name="revoke_file__confirm">Revoke</string>
<string name="forward_chat_item">Forward</string>
<string name="download_file">Download</string>
<!-- CIMetaView.kt -->
<string name="icon_descr_edited">edited</string>
@@ -329,6 +338,7 @@
<string name="share_message">Share message…</string>
<string name="share_image">Share media…</string>
<string name="share_file">Share file…</string>
<string name="forward_message">Forward message…</string>
<!-- ComposeView.kt, helpers -->
<string name="attach">Attach</string>