android, desktop: forward ui

This commit is contained in:
Avently
2024-04-17 06:15:03 +07:00
parent 77f5c678d5
commit d4d5643d3c
13 changed files with 243 additions and 29 deletions
@@ -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()
}
@@ -106,7 +107,9 @@ class MainActivity: FragmentActivity() {
if (!onBackPressedDispatcher.hasEnabledCallbacks()) {
// Drop shared content
SimplexApp.context.chatModel.sharedContent.value = null
finish()
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"
@@ -20,6 +20,7 @@ import androidx.compose.ui.text.AnnotatedString
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -29,6 +30,8 @@ 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.openDirectChat
import chat.simplex.common.views.chatlist.openGroupChat
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
@@ -36,6 +39,7 @@ 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
@@ -151,6 +155,76 @@ 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 ChatPreviewTitleText(text: String, color: Color, fontStyle: FontStyle = FontStyle.Normal) {
Text(
text,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.h3,
fontStyle = fontStyle,
color = color
)
}
Row(verticalAlignment = Alignment.CenterVertically) {
ChatInfoImage(forwardedFromItem.chatInfo, size = 72.dp)
Column(
modifier = Modifier
.padding(horizontal = 8.dp)
.weight(1F)
) {
Column {
if (forwardedFromItem.chatItem.chatDir.sent) {
ChatPreviewTitleText(text = stringResource(MR.strings.sender_you_pronoun), color = MaterialTheme.colors.primary, fontStyle = FontStyle.Italic)
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
Text(forwardedFromItem.chatInfo.chatViewName, maxLines = 1, style = MaterialTheme.typography.body1.copy(color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, lineHeight = 22.sp))
} else if (forwardedFromItem.chatItem.chatDir is CIDirection.GroupRcv) {
ChatPreviewTitleText(text = forwardedFromItem.chatItem.chatDir.groupMember.chatViewName, color = MaterialTheme.colors.primary)
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
Text(forwardedFromItem.chatInfo.chatViewName, maxLines = 1, style = MaterialTheme.typography.body1.copy(color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, lineHeight = 22.sp))
} else {
Text(forwardedFromItem.chatInfo.chatViewName, maxLines = 1, style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground, lineHeight = 22.sp))
}
}
}
}
}
@Composable
fun ForwardedFromView(forwardedFromItem: AChatItem) {
Column {
SectionItemView(
click = {
withBGApi {
if (forwardedFromItem.chatInfo is ChatInfo.Direct) {
openDirectChat(chatModel.remoteHostId(), forwardedFromItem.chatInfo.apiId, chatModel)
} else {
openGroupChat(chatModel.remoteHostId(), forwardedFromItem.chatInfo.apiId, chatModel)
}
}
ModalManager.end.closeModals()
},
padding = PaddingValues(start = 15.dp, end = DEFAULT_PADDING)
) {
ForwardedFromSender(forwardedFromItem)
}
if (!local) {
Divider(Modifier.padding(start = DEFAULT_PADDING, top = 40.dp, end = DEFAULT_PADDING, 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))
@@ -222,6 +296,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 = false)
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(
@@ -297,6 +387,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 +396,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 +408,9 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d
if (ci.quotedItem != null) {
numTabs += 1
}
if (ciInfo.forwardedFromChatItem != null) {
numTabs += 1
}
return numTabs
}
@@ -344,6 +439,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 +453,9 @@ 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))
}
TabRow(
selectedTabIndex = availableTabs.indexOfFirst { it::class == selection.value::class },
backgroundColor = Color.Transparent,
@@ -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,22 @@ 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(chat.remoteHostId, chat.chatInfo, chatItem)
return chatItem
}
return null
}
fun checkLinkPreview(): MsgContent {
return when (val composePreview = cs.preview) {
is ComposePreview.CLinkPreview -> {
@@ -465,6 +488,11 @@ fun ComposeView(
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 +591,12 @@ fun ComposeView(
sent = send(chat, MsgContent.MCText(msgText), quotedItemId, null, live, ttl)
}
}
val wasForwarding = cs.forwarding
clearState(live)
val draft = chatModel.draft.value
if (wasForwarding && chatModel.draftChatId.value == chat.chatInfo.id && draft != null) {
composeState.value = draft
}
return sent
}
@@ -745,6 +778,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 +800,7 @@ 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))
null -> {}
}
chatModel.sharedContent.value = null
@@ -24,6 +24,7 @@ import kotlinx.datetime.Clock
fun ContextItemView(
contextItem: ChatItem,
contextIcon: Painter,
showSender: Boolean = true,
cancelContextItem: () -> Unit
) {
val sent = contextItem.chatDir.sent
@@ -31,9 +32,10 @@ fun ContextItemView(
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
@Composable
fun msgContentView(lines: Int) {
fun MessageText(lines: Int) {
MarkdownText(
contextItem.text, contextItem.formattedText,
sender = null,
toggleSecrets = false,
maxLines = lines,
linkMode = SimplexLinkMode.DESCRIPTION,
@@ -41,6 +43,25 @@ fun ContextItemView(
)
}
@Composable
fun Attachment() {
when (contextItem.content.msgContent) {
is MsgContent.MCFile -> Icon(painterResource(MR.images.ic_draft_filled), null, tint = MaterialTheme.colors.secondary)
is MsgContent.MCImage -> Icon(painterResource(MR.images.ic_image), null, tint = MaterialTheme.colors.secondary)
is MsgContent.MCVoice -> Icon(painterResource(MR.images.ic_play_arrow_filled), null, tint = MaterialTheme.colors.secondary)
else -> {}
}
}
@Composable
fun ContextMsgPreview(lines: Int) {
Row {
Attachment()
Spacer(Modifier.width(4.dp))
MessageText(lines)
}
}
Row(
Modifier
.padding(top = 8.dp)
@@ -64,7 +85,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 +94,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
}
@@ -236,6 +236,13 @@ fun ChatItemView(
showMenu.value = false
})
}
if (cItem.meta.itemDeleted == null && !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 +465,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 -> {
@@ -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 = true) {
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)) {
@@ -74,7 +74,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 }
@@ -118,7 +118,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,
@@ -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,7 @@
<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>
<!-- CIMetaView.kt -->
<string name="icon_descr_edited">edited</string>
@@ -329,6 +337,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>