mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-24 19:35:33 +00:00
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:
committed by
GitHub
parent
a35d392f76
commit
f6ceb89c4b
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+22
-4
@@ -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
|
||||
|
||||
+15
-2
@@ -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"
|
||||
|
||||
+109
-11
@@ -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,
|
||||
|
||||
+6
-4
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+58
-3
@@ -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 =
|
||||
|
||||
+41
-7
@@ -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) {
|
||||
|
||||
+2
-2
@@ -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 &&
|
||||
|
||||
+2
-2
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+4
-9
@@ -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? ->
|
||||
|
||||
+34
-5
@@ -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,
|
||||
|
||||
+3
-3
@@ -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)) {
|
||||
|
||||
+5
-3
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-5
@@ -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
|
||||
}
|
||||
|
||||
+18
-9
@@ -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(
|
||||
|
||||
+3
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user