mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-04-26 21:45:52 +00:00
android, desktop: forward ui
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+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"
|
||||
|
||||
+102
@@ -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,
|
||||
|
||||
+39
-2
@@ -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
|
||||
|
||||
+25
-4
@@ -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) {
|
||||
|
||||
+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
|
||||
}
|
||||
|
||||
|
||||
+9
-2
@@ -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 -> {
|
||||
|
||||
+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 = 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)) {
|
||||
|
||||
+3
-2
@@ -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,
|
||||
|
||||
+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,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>
|
||||
|
||||
Reference in New Issue
Block a user